diff --git a/lemarche/siaes/models.py b/lemarche/siaes/models.py index 07e64a6f8..5346f9907 100644 --- a/lemarche/siaes/models.py +++ b/lemarche/siaes/models.py @@ -1006,8 +1006,12 @@ def presta_type_display(self) -> str: return "Intérim" if self.kind == siae_constants.KIND_AI: return "Mise à disposition du personnel" - if self.presta_type: - return choice_array_to_values(siae_constants.PRESTA_CHOICES, self.presta_type) + if self.activities.exists(): + presta_types = set() + for activity in self.activities.all(): + if activity.presta_type: + presta_types.update(activity.presta_type) + return choice_array_to_values(siae_constants.PRESTA_CHOICES, list(presta_types)) return "" @property diff --git a/lemarche/static/itou_marche/itou_marche.scss b/lemarche/static/itou_marche/itou_marche.scss index 1f87a85e5..135b7e304 100644 --- a/lemarche/static/itou_marche/itou_marche.scss +++ b/lemarche/static/itou_marche/itou_marche.scss @@ -151,6 +151,7 @@ ul.summary-grid-list { } .cmsfr-background-dark { + & h1, h2, h3, @@ -198,6 +199,7 @@ ul.summary-grid-list { width: 100%; border: 0; } + .autocomplete__menu { /* Style DSFR pour le menu */ border: 1px solid #000091; @@ -212,3 +214,12 @@ ul.summary-grid-list { .bg-gray { background-color: var(--g300); } + +span.fr-tag--green-emeraude, +p.fr-tag--green-emeraude { + --idle: transparent; + --hover: var(--background-action-low-green-emeraude-hover); + --active: var(--background-action-low-green-emeraude-active); + background-color: var(--background-action-low-green-emeraude); + color: var(--text-action-high-green-emeraude) +} \ No newline at end of file diff --git a/lemarche/templates/siaes/_card_detail.html b/lemarche/templates/siaes/_card_detail.html index f997604a4..11674db45 100644 --- a/lemarche/templates/siaes/_card_detail.html +++ b/lemarche/templates/siaes/_card_detail.html @@ -1,6 +1,5 @@ -{% load static siae_sectors_display %} +{% load static %} {% load theme_inclusion %} -
@@ -8,9 +7,15 @@
{% if siae.logo_url %} - Logo de la structure {{ siae.name }} + Logo de la structure {{ siae.name }} {% else %} - {{ siae.name }} + {{ siae.name }} {% endif %}
@@ -19,46 +24,45 @@

{{ siae.name_display }} {% if user.is_authenticated and user.is_admin and not siae.user_count %} - - pas encore inscrite - + pas encore inscrite {% endif %}

{% include "includes/_super_badge.html" with siae=siae %} -

(Dernière activité il y a {{ siae.latest_activity_at|timesince }})

+

+ (Dernière activité il y a {{ siae.latest_activity_at|timesince }}) +

{% if user.is_authenticated %} - - + + {% if siae.in_user_favorite_list_count_annotated %} - + {% else %} - {% endif %} {% else %} - {% endif %} @@ -67,12 +71,12 @@

    -
  • +
  • {{ siae.get_kind_display }}
  • {% if siae.legal_form %} -
  • +
  • {{ siae.get_legal_form_display }}
  • @@ -82,17 +86,12 @@

    {% if siae.is_qpv %} - QPV - {% endif %} - {% if siae.is_zrr %} - ZRR - {% endif %} - {% for group in siae.groups.all %} - {{ group.name }} - {% endfor %} - {% if siae.is_cocontracting %} - Ouvert à la co-traitance + QPV {% endif %} + {% if siae.is_zrr %}ZRR{% endif %} + {% for group in siae.groups.all %}{{ group.name }}{% endfor %} + {% if siae.is_cocontracting %}Ouvert à la co-traitance{% endif %}

@@ -103,15 +102,15 @@


-
-
-
+
+
+
+ alt="" + height="32" />
-
-

Présentation du prestataire

+
+

Présentation du prestataire

{% if siae.description %} {{ siae.description|linebreaks }} @@ -124,40 +123,38 @@

Présentation du prestataire


-
-
-
- +
+
+
+
-
-

Secteurs d'activité

- {% if user.is_authenticated and user.is_admin %} - {% for activity in siae.activities.all %} - {% include "siaes/_siae_activity_content.html" with activity=activity with_collapse=True %} - {% endfor %} - {% else %} - {% if not siae.sector_count %} -

Non renseigné

- {% else %} -
    - {% siae_sectors_display siae display_max=6 current_search_query=current_search_query output_format='li' %} -
- {% endif %} - {% endif %} +
+

Secteurs d'activité

+ {% for activity in siae.activities.all %} +
{% include "siaes/_siae_activity_content.html" with activity=activity %}
+ {% empty %} +

Non renseigné

+ {% endfor %}
{% if siae.client_reference_count %} -
-
- +
+
+
-
-

Références clients

+
+

Références clients

{% if siae.client_reference_count <= 6 %}
{% for image in siae.client_references.all %}
- {{ image.name }} + {{ image.name }}
{% endfor %}
@@ -165,7 +162,9 @@

Références clients

{% for image in siae.client_references.all|slice:":6" %}
- {{ image.name }} + {{ image.name }}
{% endfor %}
@@ -173,12 +172,20 @@

Références clients

{% for image in siae.client_references.all|slice:"6:" %}
- {{ image.name }} + {{ image.name }}
{% endfor %}
- + {% endif %}
@@ -188,21 +195,23 @@

Références clients

{% if siae.offer_count %}
-
-
+
+
+ alt="" + height="32" />
-
-

Détails des prestations effectuées (matériels, lieux, savoir-faire)

+
+

+ Détails des prestations effectuées (matériels, lieux, savoir-faire) +

{% for offer in siae.offers.all %}
-

+

{{ offer.name }} -

+

{{ offer.description|linebreaks }}
{% endfor %} @@ -216,21 +225,21 @@


{% if siae.network_count %} -
-
+
+
+ alt="" + height="32" />
-
-

Réseaux

+ {% endif %} {% if siae.label_count %} -
-
+
+
+ alt="" + height="32" />
-
-

Labels & certifications

+
+

Labels & certifications

    - {% for label in siae.labels_old.all %} -
  • {{ label.name }}
  • - {% endfor %} + {% for label in siae.labels_old.all %}
  • {{ label.name }}
  • {% endfor %}
@@ -263,14 +270,14 @@

Labels & certifications

{% if siae.images.count %}
-
-
+
+
+ alt="" + height="32" />
-
-

Nos réalisations

+
+

Nos réalisations

@@ -279,30 +286,30 @@

Nos réalisations

+ class="fr-responsive-img" + title="{{ image.name|default:'' }}" + loading="lazy" />
{% endfor %}
{% endif %} {% if not siae.user_count and not user.is_authenticated %}
-
-
-
-

C'est votre structure et vous souhaitez modifier ses informations ?

-
- +
- {{ siae.get_kind_display }} -
-
-

- {{ siae.presta_type_display }} -

-
-
-

{% siae_sectors_display siae display_max=3 current_search_query=current_search_query %}

+ {% siae_sector_groups_display siae display_max=3 current_sector_groups=current_sector_groups %} + {% if user.is_authenticated and user.is_admin and not siae.user_count %} +

pas encore inscrite

+ {% endif %}
-
-

+

+

{{ siae.city }} - - {{ siae.geo_range_pretty_display }}

- {% if user.is_authenticated and user.is_admin and not siae.user_count %} -

pas encore inscrite

- {% endif %}
diff --git a/lemarche/templates/siaes/_siae_activity_card.html b/lemarche/templates/siaes/_siae_activity_card.html index 5788c5305..abf9f9cca 100644 --- a/lemarche/templates/siaes/_siae_activity_card.html +++ b/lemarche/templates/siaes/_siae_activity_card.html @@ -10,7 +10,7 @@

{{ activity.sector_group }}

- Type(s) de prestation : + Type(s) de prestation : {{ activity.presta_type_display }}

diff --git a/lemarche/templates/siaes/_siae_activity_content.html b/lemarche/templates/siaes/_siae_activity_content.html index 9581f8daf..a25a9f6ce 100644 --- a/lemarche/templates/siaes/_siae_activity_content.html +++ b/lemarche/templates/siaes/_siae_activity_content.html @@ -1,28 +1,23 @@ {% load siae_sectors_display %} - -

- {% if with_collapse %} - - {% else %} - {{ activity.sector_group }} - {% endif %} -

- -
-
    - {% siae_sectors_display activity display_max=6 output_format='li' %} -
- -

- - Type(s) de prestation : - {{ activity.presta_type_display }} -

-

- - Intervient sur : {{ siae.geo_range_pretty_title }} - {{ siae.geo_range_pretty_display }} -

-
+
+

+ +

+
+
    + {% siae_sectors_display activity display_max=6 output_format='li' %} +
+

+ + Type(s) de prestation : + {{ activity.presta_type_display }} +

+

+ + Intervient sur : + {{ activity.geo_range_pretty_display }} +

+
+
diff --git a/lemarche/templates/utils/templatetags/siae_sectors_display.html b/lemarche/templates/utils/templatetags/siae_sectors_display.html new file mode 100644 index 000000000..840fba514 --- /dev/null +++ b/lemarche/templates/utils/templatetags/siae_sectors_display.html @@ -0,0 +1,12 @@ +
    + {% for sector_group in current_search_sector_groups %} +
  • + {{ sector_group }} +
  • + {% endfor %} + {% for sector_group in sector_groups %} +
  • + {{ sector_group }} +
  • + {% endfor %} +
diff --git a/lemarche/utils/templatetags/siae_sectors_display.py b/lemarche/utils/templatetags/siae_sectors_display.py index 00a087af7..6e2da3024 100644 --- a/lemarche/utils/templatetags/siae_sectors_display.py +++ b/lemarche/utils/templatetags/siae_sectors_display.py @@ -39,3 +39,44 @@ def siae_sectors_display(object, display_max=5, current_search_query="", output_ return mark_safe("".join([f"
  • {elem_name}
  • " for elem_name in values])) else: # "string" return ", ".join(values) + + +@register.inclusion_tag("utils/templatetags/siae_sectors_display.html") +def siae_sector_groups_display(object, display_max=5, current_sector_groups=[]): + """ + Pretty rendering of M2M field SectorGroup for Siae. + """ + + # to avoid duplicates and display current search values first + seen_slugs = set() + + current_values = [] + # Add sector groups from current_sector_groups if they are in object's activities + for sector_group in current_sector_groups: + if any(activity.sector_group.slug == sector_group.slug for activity in object.activities.all()): + if sector_group.slug not in seen_slugs: + current_values.append(sector_group.name) + seen_slugs.add(sector_group.slug) + + values = [] + + # Add remaining sector groups from object's activities + for activity in object.activities.all(): + if activity.sector_group.slug not in seen_slugs: + values.append(activity.sector_group.name) + seen_slugs.add(activity.sector_group.slug) + + # alphabetical order here to avoid N+1 queries + values = sorted(values) + + # filter number of displayed values + groups_count = len(seen_slugs) + if groups_count > display_max: + display_max_values = display_max - len(current_values) + if display_max_values > 0: + values = values[:display_max_values] + else: + values = [] + values.append(f"+{groups_count-display_max}") + + return {"current_search_sector_groups": sorted(current_values), "sector_groups": values} diff --git a/lemarche/utils/tests_templatetags.py b/lemarche/utils/tests_templatetags.py index 0c750aa9e..67bae1991 100644 --- a/lemarche/utils/tests_templatetags.py +++ b/lemarche/utils/tests_templatetags.py @@ -1,55 +1,72 @@ +from django.template import Context, Template from django.test import TestCase -from lemarche.sectors.factories import SectorFactory -from lemarche.siaes.factories import SiaeFactory +from lemarche.sectors.factories import SectorFactory, SectorGroupFactory +from lemarche.siaes.factories import SiaeActivityFactory, SiaeFactory from lemarche.utils.templatetags.siae_sectors_display import siae_sectors_display class SiaeSectorDisplayTest(TestCase): @classmethod def setUpTestData(cls): - cls.siae_with_one_sector = SiaeFactory() - cls.siae_with_two_sectors = SiaeFactory() - cls.siae_with_many_sectors = SiaeFactory() + siae_with_one_sector = SiaeFactory() + siae_with_two_sectors = SiaeFactory() + siae_with_many_sectors = SiaeFactory() cls.siae_etti = SiaeFactory(kind="ETTI") sector_1 = SectorFactory(name="Entretien") sector_2 = SectorFactory(name="Agro") sector_3 = SectorFactory(name="Hygiène") sector_4 = SectorFactory(name="Bâtiment") sector_5 = SectorFactory(name="Informatique") - cls.siae_with_one_sector.sectors.add(sector_1) - cls.siae_with_two_sectors.sectors.add(sector_1, sector_2) - cls.siae_with_many_sectors.sectors.add(sector_1, sector_2, sector_3, sector_4, sector_5) + + cls.siae_activity_with_one_sector = SiaeActivityFactory(siae=siae_with_one_sector) + cls.siae_activity_with_one_sector.sectors.add(sector_1) + cls.siae_activity_with_two_sectors = SiaeActivityFactory(siae=siae_with_two_sectors) + cls.siae_activity_with_two_sectors.sectors.add(sector_1, sector_2) + cls.siae_activity_with_many_sectors = SiaeActivityFactory(siae=siae_with_many_sectors) + cls.siae_activity_with_many_sectors.sectors.add(sector_1, sector_2, sector_3, sector_4, sector_5) + cls.siae_etti.sectors.add(sector_1, sector_2, sector_3, sector_4, sector_5) def test_should_return_list_of_siae_sector_strings(self): - self.assertEqual(siae_sectors_display(self.siae_with_one_sector), "Entretien") - self.assertEqual(siae_sectors_display(self.siae_with_two_sectors), "Agro, Entretien") # default ordering: name + self.assertEqual(siae_sectors_display(self.siae_activity_with_one_sector), "Entretien") + self.assertEqual( + siae_sectors_display(self.siae_activity_with_two_sectors), "Agro, Entretien" + ) # default ordering: name self.assertEqual( - siae_sectors_display(self.siae_with_many_sectors), "Agro, Bâtiment, Entretien, Hygiène, Informatique" + siae_sectors_display(self.siae_activity_with_many_sectors), + "Agro, Bâtiment, Entretien, Hygiène, Informatique", ) def test_should_filter_list_of_siae_sectors(self): self.assertEqual( - siae_sectors_display(self.siae_with_many_sectors, display_max=3), "Agro, Bâtiment, Entretien, …" + siae_sectors_display(self.siae_activity_with_many_sectors, display_max=3), "Agro, Bâtiment, Entretien, …" ) def test_should_return_list_in_the_specified_format(self): - self.assertEqual(siae_sectors_display(self.siae_with_two_sectors, output_format="list"), ["Agro", "Entretien"]) self.assertEqual( - siae_sectors_display(self.siae_with_two_sectors, output_format="li"), "
  • Agro
  • Entretien
  • " + siae_sectors_display(self.siae_activity_with_two_sectors, output_format="list"), ["Agro", "Entretien"] + ) + self.assertEqual( + siae_sectors_display(self.siae_activity_with_two_sectors, output_format="li"), + "
  • Agro
  • Entretien
  • ", ) def test_should_have_different_behavior_for_etti(self): self.assertEqual(siae_sectors_display(self.siae_etti), "Multisectoriel") def test_should_filter_list_on_current_search_query(self): - self.assertEqual(siae_sectors_display(self.siae_with_one_sector, current_search_query="sectors=agro"), "") self.assertEqual( - siae_sectors_display(self.siae_with_two_sectors, current_search_query="sectors=entretien"), "Entretien" + siae_sectors_display(self.siae_activity_with_one_sector, current_search_query="sectors=agro"), "" ) self.assertEqual( - siae_sectors_display(self.siae_with_many_sectors, current_search_query="sectors=entretien§ors=agro"), + siae_sectors_display(self.siae_activity_with_two_sectors, current_search_query="sectors=entretien"), + "Entretien", + ) + self.assertEqual( + siae_sectors_display( + self.siae_activity_with_many_sectors, current_search_query="sectors=entretien§ors=agro" + ), "Agro, Entretien", ) # priority is on current_search (over ETTI) @@ -57,3 +74,103 @@ def test_should_filter_list_on_current_search_query(self): siae_sectors_display(self.siae_etti, current_search_query="sectors=entretien§ors=agro"), "Agro, Entretien", ) + + +class SiaeSectorGroupsDisplayTest(TestCase): + @classmethod + def setUpTestData(cls): + cls.siae_with_one_sector = SiaeFactory() + cls.siae_with_three_sectors = SiaeFactory() + cls.siae_with_many_sectors = SiaeFactory() + + cls.sector_group_1 = SectorGroupFactory(name="Entretien du linge") + cls.sector_group_2 = SectorGroupFactory(name="Espaces verts") + cls.sector_group_3 = SectorGroupFactory(name="Bâtiment") + cls.sector_group_4 = SectorGroupFactory(name="Agro-Alimentaire") + + sector_1 = SectorFactory(name="Blanchisserie", group=cls.sector_group_1) + sector_2 = SectorFactory(name="Génie écologique", group=cls.sector_group_2) + sector_3 = SectorFactory(name="Menuiserie", group=cls.sector_group_3) + sector_4 = SectorFactory(name="Agriculture", group=cls.sector_group_4) + sector_5 = SectorFactory(name="Plomberie", group=cls.sector_group_3) + + siae_with_one_sector_activity = SiaeActivityFactory( + siae=cls.siae_with_one_sector, sector_group=cls.sector_group_1 + ) + siae_with_one_sector_activity.sectors.add(sector_1) + + siae_with_three_sectors_activity_1 = SiaeActivityFactory( + siae=cls.siae_with_three_sectors, sector_group=cls.sector_group_1 + ) + siae_with_three_sectors_activity_1.sectors.add(sector_1) + siae_with_three_sectors_activity_2 = SiaeActivityFactory( + siae=cls.siae_with_three_sectors, sector_group=cls.sector_group_2 + ) + siae_with_three_sectors_activity_2.sectors.add(sector_2) + siae_with_three_sectors_activity_3 = SiaeActivityFactory( + siae=cls.siae_with_three_sectors, sector_group=cls.sector_group_3 + ) + siae_with_three_sectors_activity_3.sectors.add(sector_3, sector_5) + + siae_with_many_sectors_activity_1 = SiaeActivityFactory( + siae=cls.siae_with_many_sectors, sector_group=cls.sector_group_1 + ) + siae_with_many_sectors_activity_1.sectors.add(sector_1) + siae_with_many_sectors_activity_2 = SiaeActivityFactory( + siae=cls.siae_with_many_sectors, sector_group=cls.sector_group_2 + ) + siae_with_many_sectors_activity_2.sectors.add(sector_2) + siae_with_many_sectors_activity_3 = SiaeActivityFactory( + siae=cls.siae_with_many_sectors, sector_group=cls.sector_group_3 + ) + siae_with_many_sectors_activity_3.sectors.add(sector_3) + siae_with_many_sectors_activity_4 = SiaeActivityFactory( + siae=cls.siae_with_many_sectors, sector_group=cls.sector_group_4 + ) + siae_with_many_sectors_activity_4.sectors.add(sector_4) + siae_with_many_sectors_activity_5 = SiaeActivityFactory( + siae=cls.siae_with_many_sectors, sector_group=cls.sector_group_3 + ) + siae_with_many_sectors_activity_5.sectors.add(sector_5) + + # Test siae_sector_groups_display if return only one sector group + def test_should_return_html_with_siae_sector_groups(self): + template = Template( + "{% load siae_sectors_display %}" + "{% siae_sector_groups_display siae display_max=3 current_sector_groups=current_sector_groups %}" + ) + + # Render the template with a context (if needed) + rendered = template.render(Context({"siae": self.siae_with_one_sector, "current_sector_groups": []})) + + self.assertInHTML("Entretien du linge", rendered) + self.assertNotIn("+", rendered) + + rendered = template.render(Context({"siae": self.siae_with_three_sectors, "current_sector_groups": []})) + self.assertInHTML("Bâtiment", rendered) + self.assertInHTML("Entretien du linge", rendered) + self.assertInHTML("Espaces verts", rendered) + self.assertNotIn("+", rendered) + + rendered = template.render(Context({"siae": self.siae_with_many_sectors, "current_sector_groups": []})) + + self.assertInHTML("Agro-Alimentaire", rendered) + self.assertInHTML("Bâtiment", rendered) + self.assertInHTML("Entretien du linge", rendered) + self.assertInHTML("+1", rendered) + + self.assertNotIn("Espaces verts", rendered) + + rendered = template.render( + Context( + { + "siae": self.siae_with_many_sectors, + "current_sector_groups": [self.sector_group_3], + } + ) + ) + self.assertInHTML('Bâtiment', rendered) + self.assertInHTML("Agro-Alimentaire", rendered) + self.assertInHTML("Entretien du linge", rendered) + self.assertInHTML("+1", rendered) + self.assertNotIn("Espaces verts", rendered) diff --git a/lemarche/www/dashboard_favorites/tests.py b/lemarche/www/dashboard_favorites/tests.py index 286a3ae48..9e39909ea 100644 --- a/lemarche/www/dashboard_favorites/tests.py +++ b/lemarche/www/dashboard_favorites/tests.py @@ -34,5 +34,8 @@ def test_only_favorite_list_user_can_view_favorite_list_detail(self): # favorite list user self.client.force_login(self.user_favorite_list) url = reverse("dashboard_favorites:list_detail", args=[self.favorite_list_1.slug]) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) + + # check number of queries + with self.assertNumQueries(6): + response = self.client.get(url) + self.assertEqual(response.status_code, 200) diff --git a/lemarche/www/dashboard_favorites/views.py b/lemarche/www/dashboard_favorites/views.py index d2c843ba0..294571be8 100644 --- a/lemarche/www/dashboard_favorites/views.py +++ b/lemarche/www/dashboard_favorites/views.py @@ -64,7 +64,7 @@ def get_success_message(self, cleaned_data): class DashboardFavoriteListDetailView(FavoriteListOwnerRequiredMixin, DetailView): template_name = "favorites/dashboard_favorite_list_detail.html" - queryset = FavoriteList.objects.prefetch_related("siaes").all() + queryset = FavoriteList.objects.prefetch_related("siaes", "siaes__activities__sector_group").all() context_object_name = "favorite_list" def get_context_data(self, **kwargs): diff --git a/lemarche/www/siaes/forms.py b/lemarche/www/siaes/forms.py index ff12e31dc..a22f3161b 100644 --- a/lemarche/www/siaes/forms.py +++ b/lemarche/www/siaes/forms.py @@ -230,6 +230,8 @@ def filter_queryset(self, qs=None): # noqa C901 if not hasattr(self, "cleaned_data"): self.full_clean() + qs = qs.prefetch_related("activities__sector_group") + kinds = self.cleaned_data.get("kind", None) if kinds: qs = qs.filter(kind__in=kinds) diff --git a/lemarche/www/siaes/tests.py b/lemarche/www/siaes/tests.py index d279162c5..b3f57680a 100644 --- a/lemarche/www/siaes/tests.py +++ b/lemarche/www/siaes/tests.py @@ -64,7 +64,7 @@ def test_search_num_queries(self): # See https://docs.djangoproject.com/en/5.1/ref/contrib/sites/#caching-the-current-site-object Site.objects.get_current() - with self.assertNumQueries(12): + with self.assertNumQueries(13): response = self.client.get(url) siaes = list(response.context["siaes"]) self.assertEqual(len(siaes), 20) diff --git a/lemarche/www/siaes/views.py b/lemarche/www/siaes/views.py index 369623f77..9eeffd301 100644 --- a/lemarche/www/siaes/views.py +++ b/lemarche/www/siaes/views.py @@ -120,6 +120,7 @@ def get_context_data(self, **kwargs): current_sectors = siae_search_form.cleaned_data.get("sectors") if current_sectors: context["current_sectors"] = list(current_sectors.values("id", "slug", "name")) + context["current_sector_groups"] = list(set(sector.group for sector in current_sectors)) # store the current search query in the session current_search_query = self.request.GET.urlencode()