diff --git a/geotrek/common/views.py b/geotrek/common/views.py
index 76824e0b9b..cd47534c96 100644
--- a/geotrek/common/views.py
+++ b/geotrek/common/views.py
@@ -77,6 +77,11 @@
logger = logging.getLogger(__name__)
+ANNOTATION_FORBIDDEN_CHARS = re.compile(r"['`\"\]\[;\s]|--|/\*|\*/")
+REPLACEMENT_CHAR = "_"
+
+def normalize_annotation_column_name(col_name):
+ return ANNOTATION_FORBIDDEN_CHARS.sub(repl=REPLACEMENT_CHAR, string=col_name)
def handler404(request, exception, template_name="404.html"):
if "api/v2" in request.get_full_path():
diff --git a/geotrek/maintenance/views.py b/geotrek/maintenance/views.py
index 37128df704..659711ab48 100755
--- a/geotrek/maintenance/views.py
+++ b/geotrek/maintenance/views.py
@@ -12,6 +12,7 @@
from geotrek.authent.decorators import same_structure_required
from geotrek.common.mixins.forms import FormsetMixin
from geotrek.common.mixins.views import CustomColumnsMixin
+from geotrek.common.views import normalize_annotation_column_name
from geotrek.common.viewsets import GeotrekMapentityViewSet
from geotrek.feedback.models import Report
from .filters import InterventionFilterSet, ProjectFilterSet
@@ -24,14 +25,6 @@
logger = logging.getLogger(__name__)
-ANNOTATION_FORBIDDEN_CHARS = re.compile(r"['`\"\]\[;\s]|--|/\*|\*/")
-REPLACEMENT_CHAR = "_"
-
-
-def _normalize_annotation_column_name(col_name):
- return ANNOTATION_FORBIDDEN_CHARS.sub(repl=REPLACEMENT_CHAR, string=col_name)
-
-
class InterventionList(CustomColumnsMixin, MapEntityList):
queryset = Intervention.objects.existing()
mandatory_columns = ['id', 'name']
@@ -50,7 +43,7 @@ class InterventionFormatList(MapEntityFormat, InterventionList):
@classmethod
def build_cost_column_name(cls, job_name):
- return _normalize_annotation_column_name(f"{_('Cost')} {job_name}")
+ return normalize_annotation_column_name(f"{_('Cost')} {job_name}")
def get_queryset(self):
"""Returns all interventions joined with a new column for each job, to record the total cost of each job in each intervention"""
diff --git a/geotrek/settings/base.py b/geotrek/settings/base.py
index e83b63d08e..924ead3b94 100644
--- a/geotrek/settings/base.py
+++ b/geotrek/settings/base.py
@@ -843,6 +843,7 @@ def api_bbox(bbox, buffer):
HIDDEN_FORM_FIELDS = {'report': ['assigned_user']}
COLUMNS_LISTS = {}
ENABLE_JOBS_COSTS_DETAILED_EXPORT = False
+ENABLE_EVENTS_PARTICIPANTS_DETAILED_EXPORT = False
ACCESSIBILITY_ATTACHMENTS_ENABLED = True
diff --git a/geotrek/tourism/templates/tourism/touristicevent_detail_attributes.html b/geotrek/tourism/templates/tourism/touristicevent_detail_attributes.html
index 9ac7920c8d..380fda97da 100644
--- a/geotrek/tourism/templates/tourism/touristicevent_detail_attributes.html
+++ b/geotrek/tourism/templates/tourism/touristicevent_detail_attributes.html
@@ -47,7 +47,8 @@
{{ object|verbose:"price" }} |
- {{ object.price }} |
+ {% if object.price %}{{ object.price }}
+ {% else %}{% trans "None" %}{% endif %} |
{{ object|verbose:"meeting_point" }} |
@@ -105,7 +106,8 @@
{{ object|verbose:"capacity" }} |
- {{ object.capacity }} |
+ {% if object.capacity %}{{ object.capacity }}
+ {% else %}{% trans "None" %}{% endif %} |
{{ object|verbose:"cancelled" }} |
@@ -113,7 +115,8 @@
{{ object|verbose:"cancellation_reason" }} |
- {{ object.cancellation_reason|safe }} |
+ {% if object.cancellation_reason %}{{ object.cancellation_reason|safe }}
+ {% else %}{% trans "None" %}{% endif %} |
{% trans "Source" %} |
@@ -174,11 +177,13 @@
{{ object|verbose:"preparation_duration" }} |
- {{ object.preparation_duration }} |
+ {% if object.preparation_duration %}{{ object.preparation_duration|safe }}
+ {% else %}{% trans "None" %}{% endif %} |
{{ object|verbose:"intervention_duration" }} |
- {{ object.intervention_duration }} |
+ {% if object.intervention_duration %}{{ object.intervention_duration|safe }}
+ {% else %}{% trans "None" %}{% endif %} |
{% include "common/publication_info_fragment.html" %}
diff --git a/geotrek/tourism/views.py b/geotrek/tourism/views.py
index cf349fdc96..8aa8968565 100644
--- a/geotrek/tourism/views.py
+++ b/geotrek/tourism/views.py
@@ -4,10 +4,11 @@
from django.contrib.auth.decorators import login_required
from django.contrib.gis.db.models.functions import Transform
from django.core.exceptions import PermissionDenied
-from django.db.models import Sum
+from django.db.models import Sum, OuterRef, F
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
+from django.utils.translation import gettext_lazy as _
from django.utils.html import escape
from django.views.generic import CreateView
from mapentity.views import (MapEntityCreate, MapEntityUpdate, MapEntityList, MapEntityDetail, MapEntityFilter,
@@ -17,13 +18,13 @@
from geotrek.authent.decorators import same_structure_required
from geotrek.common.mixins.views import CompletenessMixin, CustomColumnsMixin
from geotrek.common.models import RecordSource, TargetPortal
-from geotrek.common.views import DocumentPublic, DocumentBookletPublic, MarkupPublic
+from geotrek.common.views import DocumentPublic, DocumentBookletPublic, MarkupPublic, normalize_annotation_column_name
from geotrek.common.viewsets import GeotrekMapentityViewSet
from geotrek.trekking.models import Trek
from .filters import TouristicContentFilterSet, TouristicEventFilterSet
from .forms import TouristicContentForm, TouristicEventForm, TouristicEventOrganizerFormPopup
from .models import (TouristicContent, TouristicEvent, TouristicContentCategory, TouristicEventOrganizer,
- InformationDesk)
+ InformationDesk, TouristicEventParticipantCategory, TouristicEventParticipantCount)
from .serializers import (TouristicContentSerializer, TouristicEventSerializer,
TrekInformationDeskGeojsonSerializer,
TouristicContentGeojsonSerializer, TouristicEventGeojsonSerializer)
@@ -184,13 +185,56 @@ class TouristicEventFormatList(MapEntityFormat, TouristicEventList):
'date_insert', 'date_update', 'source', 'portal',
'review', 'published', 'publication_date',
'cities', 'districts', 'areas', 'approved', 'uuid',
- 'cancelled', 'cancellation_reason', 'total_participants', 'place',
+ 'cancelled', 'cancellation_reason', 'place',
'preparation_duration', 'intervention_duration', 'price'
]
+ @classmethod
+ def build_participants_column_name(cls, category_label):
+ return normalize_annotation_column_name(f"{_('Participants')} {category_label}")
+
+ @classmethod
+ def get_mandatory_columns(cls):
+ mandatory_columns = ['id']
+ if settings.ENABLE_EVENTS_PARTICIPANTS_DETAILED_EXPORT:
+ categories_names = list(TouristicEventParticipantCategory.objects.order_by('order').values_list('label', flat=True))
+ # Create column names for each unique category
+ categories_columns_names = list(map(cls.build_participants_column_name, categories_names))
+ # Add these column names to export
+ mandatory_columns = mandatory_columns + categories_columns_names + ['total_participants']
+ else:
+ mandatory_columns = mandatory_columns + ['total_participants']
+ return mandatory_columns
+
def get_queryset(self):
- qs = super().get_queryset().select_related('place', 'cancellation_reason').prefetch_related('participants')
- return qs.annotate(total_participants=Sum('participants__count'))
+ """Returns all events joined with a new column for each participant count"""
+
+ queryset = super().get_queryset().select_related('place', 'cancellation_reason').prefetch_related('participants')
+
+ if settings.ENABLE_EVENTS_PARTICIPANTS_DETAILED_EXPORT:
+ # Get all participants categories, as unique ids and names
+ categories = TouristicEventParticipantCategory.objects.order_by('order').values_list('id', 'label')
+
+ # Iter over unique categories
+ for category_id, label in categories:
+
+ # Create column name for current category
+ column_name = self.build_participants_column_name(label)
+
+ # Create subquery to retrieve category count (renamed because of ambiguity with 'count' method)
+ subquery = (TouristicEventParticipantCount.objects.filter(
+ event=OuterRef('pk'),
+ category=category_id
+ ).annotate(
+ category_count=F('count')
+ ).values('category_count'))
+
+ # Annotate queryset with this cost query
+ params = {column_name: subquery}
+ queryset = queryset.annotate(**params).annotate(total_participants=Sum('participants__count'))
+ else:
+ return queryset.annotate(total_participants=Sum('participants__count'))
+ return queryset
class TouristicEventDetail(CompletenessMixin, MapEntityDetail):