From a900197b4eec15b59a7913fb2c97d899f7eed7a5 Mon Sep 17 00:00:00 2001 From: TheoLechemia Date: Wed, 20 Dec 2023 14:42:22 +0100 Subject: [PATCH 01/10] :dizzy: Improvements - touristicevent: transform organizer to a many to many field --- docs/install/advanced-configuration.rst | 6 ++-- geotrek/api/tests/test_v2.py | 2 +- geotrek/api/v2/filters.py | 1 + geotrek/api/v2/serializers.py | 27 +++++++++++---- geotrek/api/v2/views/tourism.py | 2 +- geotrek/tourism/forms.py | 4 +-- .../tourism/locale/en/LC_MESSAGES/django.po | 4 +-- .../migrations/0050_auto_20231220_0952.py | 34 +++++++++++++++++++ geotrek/tourism/models.py | 5 +-- geotrek/tourism/serializers.py | 6 ++++ .../templates/tourism/sql/post_20_views.sql | 10 ++++-- .../touristicevent_detail_attributes.html | 5 +-- geotrek/tourism/tests/test_functional.py | 2 +- geotrek/tourism/views.py | 2 +- 14 files changed, 86 insertions(+), 24 deletions(-) create mode 100644 geotrek/tourism/migrations/0050_auto_20231220_0952.py diff --git a/docs/install/advanced-configuration.rst b/docs/install/advanced-configuration.rst index 27fef76484..687f4ade97 100644 --- a/docs/install/advanced-configuration.rst +++ b/docs/install/advanced-configuration.rst @@ -1389,7 +1389,7 @@ A (nearly?) exhaustive list of attributes available for display and export as co "email", "website", "end_date", - "organizer", + "organizers", "speaker", "type", "accessibility", @@ -1929,7 +1929,7 @@ A (nearly?) exhaustive list of attributes available for display and export as co "contact", "email", "website", - "organizer", + "organizers", "speaker", "accessibility", "capacity", @@ -2277,7 +2277,7 @@ An exhaustive list of form fields hideable in each module. 'contact', 'email', 'website', - 'organizer', + 'organizers', 'speaker', 'type', 'accessibility', diff --git a/geotrek/api/tests/test_v2.py b/geotrek/api/tests/test_v2.py index bad2747d2b..1b4b5aa03d 100644 --- a/geotrek/api/tests/test_v2.py +++ b/geotrek/api/tests/test_v2.py @@ -3241,8 +3241,8 @@ def setUpTestData(cls): capacity=12, bookable=False, place=cls.place, - organizer=cls.organizer ) + cls.touristic_event5.organizers.set([cls.organizer]) cls.touristic_content = tourism_factory.TouristicContentFactory(geom=Point(0.77802, 43.047482, srid=4326)) def test_touristic_event_list(self): diff --git a/geotrek/api/v2/filters.py b/geotrek/api/v2/filters.py index c4509eb395..0e403a340b 100644 --- a/geotrek/api/v2/filters.py +++ b/geotrek/api/v2/filters.py @@ -507,6 +507,7 @@ class TouristicEventFilterSet(filters.FilterSet): widget=CSVWidget(), queryset=TouristicEventOrganizer.objects.all(), help_text=_("Filter by one or more organizer, comma-separated."), + field_name="organizers" ) help_texts = { diff --git a/geotrek/api/v2/serializers.py b/geotrek/api/v2/serializers.py index d7205d5222..51d23e036b 100644 --- a/geotrek/api/v2/serializers.py +++ b/geotrek/api/v2/serializers.py @@ -232,6 +232,14 @@ class Meta: ) +class TouristicOrganismSerializer(DynamicFieldsMixin, serializers.ModelSerializer): + class Meta: + model = tourism_models.TouristicEventOrganizer + fields = ( + 'id', + ) + + class OrganismSerializer(DynamicFieldsMixin, serializers.ModelSerializer): name = serializers.CharField(source='organism') @@ -503,13 +511,8 @@ def get_departure_city(self, obj): return city.code if city else None class TouristicEventSerializer(TouristicModelSerializer): - organizer = serializers.SlugRelatedField( - read_only=True, - slug_field='label' - ) - organizer_id = serializers.PrimaryKeyRelatedField( - read_only=True - ) + organizers = serializers.SerializerMethodField() + organizers_id = serializers.SerializerMethodField() attachments = AttachmentSerializer(many=True, source='sorted_attachments') url = HyperlinkedIdentityField(view_name='apiv2:touristicevent-detail') begin_date = serializers.DateField() @@ -545,6 +548,16 @@ def get_participant_number(self, obj): def get_end_date(self, obj): return obj.end_date or obj.begin_date + def get_organizers(self, obj): + return ", ".join( + map(lambda org: org.label, obj.organizers.all()) + ) + + def get_organizers_id(self, obj): + return ", ".join( + map(lambda org: org.label, obj.organizers.all()) + ) + class Meta(TimeStampedSerializer.Meta): model = tourism_models.TouristicEvent fields = TimeStampedSerializer.Meta.fields + ( diff --git a/geotrek/api/v2/views/tourism.py b/geotrek/api/v2/views/tourism.py index b70f93db22..0ac4a4bf10 100644 --- a/geotrek/api/v2/views/tourism.py +++ b/geotrek/api/v2/views/tourism.py @@ -95,7 +95,7 @@ def get_queryset(self): activate(self.request.GET.get('language')) return tourism_models.TouristicEvent.objects.existing()\ .select_related('type') \ - .prefetch_related('themes', 'source', 'portal', + .prefetch_related('themes', 'source', 'portal', 'organizers', Prefetch('attachments', queryset=Attachment.objects.select_related('license', 'filetype', 'filetype__structure')) ) \ diff --git a/geotrek/tourism/forms.py b/geotrek/tourism/forms.py index cf5aef6169..4697d65e7f 100644 --- a/geotrek/tourism/forms.py +++ b/geotrek/tourism/forms.py @@ -95,7 +95,7 @@ class TouristicEventForm(CommonForm): 'contact', 'email', 'website', - 'organizer', + 'organizers', 'speaker', 'accessibility', 'bookable', @@ -131,7 +131,7 @@ class TouristicEventForm(CommonForm): class Meta: fields = ['name', 'place', 'review', 'published', 'description_teaser', 'description', 'themes', 'begin_date', 'end_date', 'duration', 'meeting_point', - 'start_time', 'end_time', 'contact', 'email', 'website', 'organizer', 'speaker', + 'start_time', 'end_time', 'contact', 'email', 'website', 'organizers', 'speaker', 'type', 'accessibility', 'capacity', 'booking', 'target_audience', 'practical_info', 'approved', 'source', 'portal', 'geom', 'eid', 'structure', 'bookable', 'cancelled', 'cancellation_reason', 'preparation_duration', 'intervention_duration'] diff --git a/geotrek/tourism/locale/en/LC_MESSAGES/django.po b/geotrek/tourism/locale/en/LC_MESSAGES/django.po index 2d7fa78f4a..55ee69bd09 100644 --- a/geotrek/tourism/locale/en/LC_MESSAGES/django.po +++ b/geotrek/tourism/locale/en/LC_MESSAGES/django.po @@ -63,7 +63,7 @@ msgid "Start date is after end date" msgstr "" msgid "Label" -msgstr "Label" +msgstr "" msgid "Information desk type" msgstr "" @@ -280,7 +280,7 @@ msgid "Event places" msgstr "" msgid "Organizer" -msgstr "Organizer" +msgstr "" msgid "Organizers" msgstr "" diff --git a/geotrek/tourism/migrations/0050_auto_20231220_0952.py b/geotrek/tourism/migrations/0050_auto_20231220_0952.py new file mode 100644 index 0000000000..14b78a3432 --- /dev/null +++ b/geotrek/tourism/migrations/0050_auto_20231220_0952.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.23 on 2023-12-20 09:52 + +from django.db import migrations, models + + +def forward_func(apps, schema_editors): + ToursticEvent = apps.get_model("tourism", "TouristicEvent") + for event in ToursticEvent.objects.all(): + if event.organizer: + event.organizers.add(event.organizer) + + +def reverse_func(apps, schema_editors): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('tourism', '0049_alter_touristiccontentcategory_color'), + ] + + operations = [ + migrations.AddField( + model_name='touristicevent', + name='organizers', + field=models.ManyToManyField(blank=True, related_name='touristicevent', to='tourism.TouristicEventOrganizer', verbose_name='Organizers'), + ), + migrations.RunPython(forward_func, reverse_func), + migrations.RemoveField( + model_name='touristicevent', + name='organizer', + ), + ] diff --git a/geotrek/tourism/models.py b/geotrek/tourism/models.py index 5af120e49a..d8f429671b 100644 --- a/geotrek/tourism/models.py +++ b/geotrek/tourism/models.py @@ -430,8 +430,9 @@ class TouristicEvent(ZoningPropertiesMixin, AddPropertyMixin, PublishableMixin, blank=True, null=True) website = models.URLField(verbose_name=_("Website"), max_length=256, blank=True, null=True) - organizer = models.ForeignKey('tourism.TouristicEventOrganizer', verbose_name=_("Organizer"), blank=True, null=True, - on_delete=models.PROTECT, related_name="touristicevent") + + organizers = models.ManyToManyField('tourism.TouristicEventOrganizer', verbose_name=_("Organizers"), blank=True, + related_name="touristicevent") speaker = models.CharField(verbose_name=_("Speaker"), max_length=256, blank=True) type = models.ForeignKey(TouristicEventType, verbose_name=_("Type"), blank=True, null=True, on_delete=models.PROTECT) accessibility = models.TextField(verbose_name=_("Accessibility"), blank=True) diff --git a/geotrek/tourism/serializers.py b/geotrek/tourism/serializers.py index 15682805ab..90d0e55a13 100644 --- a/geotrek/tourism/serializers.py +++ b/geotrek/tourism/serializers.py @@ -182,6 +182,7 @@ class TouristicEventAPISerializer(PicturesSerializerMixin, PublishableSerializer source = RecordSourceSerializer(many=True) portal = TargetPortalSerializer(many=True) structure = StructureSerializer() + organizer = rest_serializers.SerializerMethodField(source="organizers") # Nearby touristic_contents = CloseTouristicContentSerializer(many=True, source='published_touristic_contents') @@ -193,6 +194,11 @@ class TouristicEventAPISerializer(PicturesSerializerMixin, PublishableSerializer type1 = TouristicEventTypeSerializer(many=True) category = rest_serializers.SerializerMethodField() + def get_organizers(self, obj): + return ", ".join( + map(lambda org: org.label, obj.organizers.all()) + ) + def __init__(self, instance=None, *args, **kwargs): super().__init__(instance, *args, **kwargs) if 'geotrek.diving' in settings.INSTALLED_APPS: diff --git a/geotrek/tourism/templates/tourism/sql/post_20_views.sql b/geotrek/tourism/templates/tourism/sql/post_20_views.sql index 2718db6fb1..6524aa1e83 100644 --- a/geotrek/tourism/templates/tourism/sql/post_20_views.sql +++ b/geotrek/tourism/templates/tourism/sql/post_20_views.sql @@ -196,7 +196,7 @@ SELECT a.id, {% for lang in MODELTRANSLATION_LANGUAGES %} a.meeting_point_{{ lang }} AS "Meeting point {{ lang }}", {% endfor %} - d.label AS "Organizer", + o.labels AS "Organizers", {% for lang in MODELTRANSLATION_LANGUAGES %} a.accessibility_{{ lang }} AS "Accessibility {{ lang }}", {% endfor %} @@ -218,7 +218,6 @@ SELECT a.id, FROM public.tourism_touristicevent a LEFT JOIN public.tourism_touristiceventtype b ON a.type_id = b.id LEFT JOIN public.authent_structure c ON a.structure_id = c.id -LEFT JOIN public.tourism_touristiceventorganizer d ON a.organizer_id = d.id LEFT JOIN public.tourism_cancellationreason cr ON a.cancellation_reason_id = cr.id LEFT JOIN public.tourism_touristiceventplace p ON a.place_id = p.id LEFT JOIN LATERAL ( @@ -240,6 +239,13 @@ LEFT JOIN JOIN tourism_touristicevent_themes b ON a.id = b.theme_id JOIN tourism_touristicevent c ON b.touristicevent_id = c.id GROUP BY c.id) h ON a.id = h.id +LEFT JOIN + (SELECT c.id, + array_to_string(ARRAY_AGG (a.label ORDER BY a.id), ', ', '_') labels + FROM tourism_touristiceventorganizer a + JOIN tourism_touristicevent_organizers b ON a.id = b.touristiceventorganizer_id + JOIN tourism_touristicevent c ON b.touristicevent_id = c.id + GROUP BY c.id) o ON a.id = o.id WHERE deleted IS FALSE ; diff --git a/geotrek/tourism/templates/tourism/touristicevent_detail_attributes.html b/geotrek/tourism/templates/tourism/touristicevent_detail_attributes.html index f23b02b5a7..a0bd44fb64 100644 --- a/geotrek/tourism/templates/tourism/touristicevent_detail_attributes.html +++ b/geotrek/tourism/templates/tourism/touristicevent_detail_attributes.html @@ -79,8 +79,9 @@

{% trans "Attributes" %}

{% if object.website %}{{ object.website }}{% endif %} - {{ object|verbose:"organizer" }} - {% if object.organizer %}{{ object.organizer }} + {{ object|verbose:"organizers" }} + {% if object.organizers %} + {% valuelist object.organizers.all %} {% else %}{% trans "None" %}{% endif %} diff --git a/geotrek/tourism/tests/test_functional.py b/geotrek/tourism/tests/test_functional.py index cdc517a28b..0fbcbb5c9c 100644 --- a/geotrek/tourism/tests/test_functional.py +++ b/geotrek/tourism/tests/test_functional.py @@ -368,7 +368,7 @@ def test_csv_participants_count(self): total_count = sum(map(attrgetter('count'), counts)) self.assertEqual(event.participants_total, total_count) self.assertEqual(event.participants_total_verbose_name, "Number of participants") - with self.assertNumQueries(15): + with self.assertNumQueries(16): response = self.client.get(event.get_format_list_url()) self.assertEqual(response.status_code, 200) self.assertEqual(response.get('Content-Type'), 'text/csv') diff --git a/geotrek/tourism/views.py b/geotrek/tourism/views.py index f9fb011292..416bc0b539 100644 --- a/geotrek/tourism/views.py +++ b/geotrek/tourism/views.py @@ -204,7 +204,7 @@ class TouristicEventFormatList(MapEntityFormat, TouristicEventList): default_extra_columns = [ 'structure', 'eid', 'name', 'type', 'description_teaser', 'description', 'themes', 'begin_date', 'end_date', 'duration', 'meeting_point', 'start_time', 'end_time', - 'contact', 'email', 'website', 'organizer', 'speaker', 'accessibility', 'bookable', + 'contact', 'email', 'website', 'organizers', 'speaker', 'accessibility', 'bookable', 'capacity', 'booking', 'target_audience', 'practical_info', 'date_insert', 'date_update', 'source', 'portal', 'review', 'published', 'publication_date', From badb37294bced059cac8f03f744156bcdcac3138 Mon Sep 17 00:00:00 2001 From: TheoLechemia Date: Wed, 24 Jan 2024 17:04:08 +0100 Subject: [PATCH 02/10] fix test --- geotrek/api/tests/test_v2.py | 2 +- geotrek/api/v2/serializers.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/geotrek/api/tests/test_v2.py b/geotrek/api/tests/test_v2.py index 1b4b5aa03d..11ede453fb 100644 --- a/geotrek/api/tests/test_v2.py +++ b/geotrek/api/tests/test_v2.py @@ -228,7 +228,7 @@ TOURISTIC_EVENT_DETAIL_JSON_STRUCTURE = sorted([ 'id', 'accessibility', 'approved', 'attachments', 'begin_date', 'bookable', 'booking', 'cities', 'contact', 'create_datetime', 'description', 'description_teaser', 'districts', 'duration', 'email', 'end_date', 'external_id', 'geometry', - 'meeting_point', 'start_time', 'meeting_time', 'end_time', 'name', 'organizer', 'organizer_id', 'capacity', 'pdf', 'place', 'portal', + 'meeting_point', 'start_time', 'meeting_time', 'end_time', 'name', 'organizers', 'organizers_id', 'capacity', 'pdf', 'place', 'portal', 'practical_info', 'provider', 'published', 'source', 'speaker', 'structure', 'target_audience', 'themes', 'type', 'update_datetime', 'url', 'uuid', 'website', 'cancelled', 'cancellation_reason', 'participant_number' ]) diff --git a/geotrek/api/v2/serializers.py b/geotrek/api/v2/serializers.py index 51d23e036b..fc13122ee5 100644 --- a/geotrek/api/v2/serializers.py +++ b/geotrek/api/v2/serializers.py @@ -552,7 +552,7 @@ def get_organizers(self, obj): return ", ".join( map(lambda org: org.label, obj.organizers.all()) ) - + def get_organizers_id(self, obj): return ", ".join( map(lambda org: org.label, obj.organizers.all()) @@ -565,7 +565,7 @@ class Meta(TimeStampedSerializer.Meta): 'booking', 'cancellation_reason', 'cancelled', 'capacity', 'cities', 'contact', 'description', 'description_teaser', 'districts', 'duration', 'email', 'end_date', 'end_time', 'external_id', 'geometry', 'meeting_point', - 'meeting_time', 'name', 'organizer', 'organizer_id', 'participant_number', 'pdf', 'place', + 'meeting_time', 'name', 'organizers', 'organizers_id', 'participant_number', 'pdf', 'place', 'portal', 'practical_info', 'provider', 'published', 'source', 'speaker', 'start_time', 'structure', 'target_audience', 'themes', 'type', 'url', 'uuid', 'website' From 4f2ff8deccedc23bd622d5f7ef55f7e1dd53bb51 Mon Sep 17 00:00:00 2001 From: TheoLechemia Date: Thu, 25 Jan 2024 13:58:17 +0100 Subject: [PATCH 03/10] fix test and retro compat --- docs/changelog.rst | 6 +++++- geotrek/api/v2/serializers.py | 6 +++++- geotrek/tourism/parsers.py | 6 +++--- geotrek/tourism/serializers.py | 4 ++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1b4979962c..0eed24a9f8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,12 +4,16 @@ CHANGELOG 2.102.1+dev (XXXX-XX-XX) ------------------------ -- Add popup button to add organizer in touristic event form **New features** - Add `include_externals` filter to Cirkwi trek exports, to allow excluding treks with an external id (eid) (#3947) +**Improvments** + +- Add popup button to add organizer in touristic event form +- Change the `organizer` field of `TouristicEvent` model to a many to many field named `organizers` (#3587) + 2.102.1 (2024-02-20) -------------------- diff --git a/geotrek/api/v2/serializers.py b/geotrek/api/v2/serializers.py index fc13122ee5..f3ffada373 100644 --- a/geotrek/api/v2/serializers.py +++ b/geotrek/api/v2/serializers.py @@ -512,6 +512,7 @@ def get_departure_city(self, obj): class TouristicEventSerializer(TouristicModelSerializer): organizers = serializers.SerializerMethodField() + organizer = serializers.SerializerMethodField() organizers_id = serializers.SerializerMethodField() attachments = AttachmentSerializer(many=True, source='sorted_attachments') url = HyperlinkedIdentityField(view_name='apiv2:touristicevent-detail') @@ -553,6 +554,9 @@ def get_organizers(self, obj): map(lambda org: org.label, obj.organizers.all()) ) + # for retrocompatibility of API + get_organizer = get_organizers + def get_organizers_id(self, obj): return ", ".join( map(lambda org: org.label, obj.organizers.all()) @@ -565,7 +569,7 @@ class Meta(TimeStampedSerializer.Meta): 'booking', 'cancellation_reason', 'cancelled', 'capacity', 'cities', 'contact', 'description', 'description_teaser', 'districts', 'duration', 'email', 'end_date', 'end_time', 'external_id', 'geometry', 'meeting_point', - 'meeting_time', 'name', 'organizers', 'organizers_id', 'participant_number', 'pdf', 'place', + 'meeting_time', 'name', 'organizers', 'organizer', 'organizers_id', 'participant_number', 'pdf', 'place', 'portal', 'practical_info', 'provider', 'published', 'source', 'speaker', 'start_time', 'structure', 'target_audience', 'themes', 'type', 'url', 'uuid', 'website' diff --git a/geotrek/tourism/parsers.py b/geotrek/tourism/parsers.py index c84e550b7e..4bdad007b8 100644 --- a/geotrek/tourism/parsers.py +++ b/geotrek/tourism/parsers.py @@ -246,7 +246,6 @@ class TouristicEventApidaeParser(AttachmentApidaeParserMixin, ApidaeParser): ), 'email': 'informations.moyensCommunication', 'website': 'informations.moyensCommunication', - 'organizer': 'informations.structureGestion.nom.libelleFr', 'type': 'informationsFeteEtManifestation.typesManifestation.0.libelleFr', 'capacity': 'informationsFeteEtManifestation.nbParticipantsAttendu', 'practical_info': ( @@ -284,12 +283,13 @@ class TouristicEventApidaeParser(AttachmentApidaeParserMixin, ApidaeParser): 'illustrations' ] m2m_fields = { - 'themes': 'informationsFeteEtManifestation.themes.*.libelleFr' + 'themes': 'informationsFeteEtManifestation.themes.*.libelleFr', + 'organizers': 'informations.structureGestion.nom.libelleFr', } natural_keys = { 'themes': 'label', 'type': 'type', - 'organizer': 'label', + 'organizers': 'label', 'source': 'name', 'portal': 'name', } diff --git a/geotrek/tourism/serializers.py b/geotrek/tourism/serializers.py index 90d0e55a13..6ff6b59977 100644 --- a/geotrek/tourism/serializers.py +++ b/geotrek/tourism/serializers.py @@ -182,7 +182,7 @@ class TouristicEventAPISerializer(PicturesSerializerMixin, PublishableSerializer source = RecordSourceSerializer(many=True) portal = TargetPortalSerializer(many=True) structure = StructureSerializer() - organizer = rest_serializers.SerializerMethodField(source="organizers") + organizers = rest_serializers.SerializerMethodField(source="organizers") # Nearby touristic_contents = CloseTouristicContentSerializer(many=True, source='published_touristic_contents') @@ -211,7 +211,7 @@ class Meta: fields = ( 'id', 'accessibility', 'approved', 'begin_date', 'booking', 'capacity', 'category', 'contact', 'description', 'description_teaser', - 'duration', 'email', 'end_date', 'end_time', 'meeting_point', 'organizer', + 'duration', 'email', 'end_date', 'end_time', 'meeting_point', 'organizers', 'pois', 'portal', 'practical_info', 'source', 'speaker', 'start_time', 'structure', 'target_audience', 'themes', 'touristic_contents', 'touristic_events', 'treks', 'type', 'type1', 'website' From bc875e6c8af41ff070f485f8dabd1f054cfc00cf Mon Sep 17 00:00:00 2001 From: TheoLechemia Date: Wed, 14 Feb 2024 14:06:40 +0100 Subject: [PATCH 04/10] try to fix some tests --- geotrek/api/mobile/serializers/tourism.py | 2 +- geotrek/tourism/parsers.py | 9 +++++---- geotrek/tourism/tests/test_functional.py | 2 +- geotrek/tourism/tests/test_parsers.py | 3 ++- geotrek/tourism/tests/test_views.py | 2 +- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/geotrek/api/mobile/serializers/tourism.py b/geotrek/api/mobile/serializers/tourism.py index ba4b6ee26f..151ad2a65a 100644 --- a/geotrek/api/mobile/serializers/tourism.py +++ b/geotrek/api/mobile/serializers/tourism.py @@ -57,7 +57,7 @@ class Meta: fields = ('id', 'pk', 'name', 'description_teaser', 'description', 'themes', 'pictures', 'begin_date', 'end_date', 'duration', 'meeting_point', 'start_time', 'contact', 'email', 'website', - 'organizer', 'speaker', 'type', 'accessibility', + 'organizers', 'speaker', 'type', 'accessibility', 'capacity', 'booking', 'target_audience', 'practical_info', 'approved', 'geometry') diff --git a/geotrek/tourism/parsers.py b/geotrek/tourism/parsers.py index 4bdad007b8..92979b464d 100644 --- a/geotrek/tourism/parsers.py +++ b/geotrek/tourism/parsers.py @@ -301,7 +301,7 @@ def __init__(self, *args, **kwargs): self.field_options = self.field_options.copy() self.field_options['themes'] = {'create': True} self.field_options['type'] = {'create': True} - self.field_options['organizer'] = {'create': True} + self.field_options['organizers'] = {'create': True} if self.type is not None: self.constant_fields['type'] = self.type if self.themes is not None: @@ -351,6 +351,7 @@ def filter_capacity(self, src, val): def filter_email(self, src, val): return self._filter_comm(val, 204, multiple=False) + def filter_website(self, src, val): return self._filter_comm(val, 205, multiple=False) @@ -939,7 +940,7 @@ class LEITouristicEventParser(LEIParser): 'ADRPROD_TEL', 'ADRPROD_TEL2', 'ADRPREST_TEL', 'ADRPREST_TEL2'), 'email': ('ADRPROD_EMAIL', 'ADRPREST_EMAIL', 'ADRPREST_EMAIL2'), 'website': ('ADRPROD_URL', 'ADRPREST_URL'), - 'organizer': ('RAISONSOC_PERSONNE_EN_CHARGE', 'RAISONSOC_RESPONSABLE'), + 'organizers': ('RAISONSOC_PERSONNE_EN_CHARGE', 'RAISONSOC_RESPONSABLE'), 'speaker': ('CIVILITE_RESPONSABLE', 'NOM_RESPONSABLE', 'PRENOM_RESPONSABLE'), 'type': 'TYPE_NOM', 'geom': ('LATITUDE', 'LONGITUDE'), @@ -966,9 +967,9 @@ def filter_description_teaser(self, src, val): val = val.replace('\n', '
') return val - def filter_organizer(self, src, val): + def filter_organizers(self, src, val): (first, second) = val - return self.apply_filter('organizer', src, [first if first else second]) + return self.apply_filter('organizers', src, [first if first else second]) def filter_speaker(self, src, val): (civilite, nom, prenom) = val diff --git a/geotrek/tourism/tests/test_functional.py b/geotrek/tourism/tests/test_functional.py index 0fbcbb5c9c..96e337714f 100644 --- a/geotrek/tourism/tests/test_functional.py +++ b/geotrek/tourism/tests/test_functional.py @@ -219,7 +219,7 @@ def get_expected_json_attrs(self): 'start_time': None, 'end_time': None, 'name': 'Touristic event', - 'organizer': None, + 'organizers': '', 'capacity': None, 'pictures': [], 'pois': [], diff --git a/geotrek/tourism/tests/test_parsers.py b/geotrek/tourism/tests/test_parsers.py index 5966c44a49..845f3b8d82 100644 --- a/geotrek/tourism/tests/test_parsers.py +++ b/geotrek/tourism/tests/test_parsers.py @@ -461,7 +461,8 @@ def mocked_json(): self.assertIn("Langues Parlées:
Français
", event.practical_info) self.assertIn("Accès:
TestFr
", event.practical_info) self.assertTrue(event.published) - self.assertEqual(str(event.organizer), 'Toto') + organizers = event.organizers.all() + self.assertEqual(organizers[0].label, 'Toto') self.assertEqual(str(event.start_time), '09:00:00') self.assertEqual(event.type.type, 'Sports') self.assertQuerysetEqual( diff --git a/geotrek/tourism/tests/test_views.py b/geotrek/tourism/tests/test_views.py index 2f74685c48..dc79694507 100644 --- a/geotrek/tourism/tests/test_views.py +++ b/geotrek/tourism/tests/test_views.py @@ -372,7 +372,7 @@ def test_expected_properties(self): 'cities', 'contact', 'description', 'description_teaser', 'districts', 'duration', 'email', 'end_date', 'filelist_url', 'files', 'id', 'map_image_url', 'meeting_point', 'start_time', 'end_time', 'name', - 'organizer', 'capacity', 'pictures', 'pois', 'portal', 'practical_info', + 'organizers', 'capacity', 'pictures', 'pois', 'portal', 'practical_info', 'printable', 'publication_date', 'published', 'published_status', 'slug', 'source', 'speaker', 'structure', 'target_audience', 'themes', 'thumbnail', 'touristic_contents', 'touristic_events', 'treks', 'type', From 49df53479aa4bda31ca74ee6f9adaace0aa5b23a Mon Sep 17 00:00:00 2001 From: TheoLechemia Date: Thu, 22 Feb 2024 16:09:10 +0100 Subject: [PATCH 05/10] fix mobile api response and test --- geotrek/api/mobile/serializers/tourism.py | 11 ++++++++++- geotrek/api/tests/test_mobile/test_api_mobile.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/geotrek/api/mobile/serializers/tourism.py b/geotrek/api/mobile/serializers/tourism.py index 151ad2a65a..33ade6c07e 100644 --- a/geotrek/api/mobile/serializers/tourism.py +++ b/geotrek/api/mobile/serializers/tourism.py @@ -37,6 +37,8 @@ class Meta: class TouristicEventListSerializer(geo_serializers.GeoFeatureModelSerializer): geometry = geo_serializers.GeometryField(read_only=True, precision=7, source='geom2d_transformed') pictures = rest_serializers.SerializerMethodField() + organizers = rest_serializers.SerializerMethodField() + organizer = rest_serializers.SerializerMethodField() def get_pictures(self, obj): if not obj.resized_pictures: @@ -49,6 +51,13 @@ def get_pictures(self, obj): 'legend': first_picture.legend, 'url': os.path.join('/', str(self.context['root_pk']), settings.MEDIA_URL[1:], thdetail_first.name), }] + + def get_organizers(self, obj): + return ", ".join( + map(lambda org: org.label, obj.organizers.all()) + ) + # for retrocompatibility of API + get_organizer = get_organizers class Meta: model = tourism_models.TouristicEvent @@ -57,7 +66,7 @@ class Meta: fields = ('id', 'pk', 'name', 'description_teaser', 'description', 'themes', 'pictures', 'begin_date', 'end_date', 'duration', 'meeting_point', 'start_time', 'contact', 'email', 'website', - 'organizers', 'speaker', 'type', 'accessibility', + 'organizers', 'organizer', 'speaker', 'type', 'accessibility', 'capacity', 'booking', 'target_audience', 'practical_info', 'approved', 'geometry') diff --git a/geotrek/api/tests/test_mobile/test_api_mobile.py b/geotrek/api/tests/test_mobile/test_api_mobile.py index aca8ea35b1..86cbb046b7 100644 --- a/geotrek/api/tests/test_mobile/test_api_mobile.py +++ b/geotrek/api/tests/test_mobile/test_api_mobile.py @@ -49,7 +49,7 @@ TOURISTIC_EVENT_LIST_PROPERTIES_GEOJSON_STRUCTURE = sorted([ 'id', 'name', 'description_teaser', 'description', 'themes', 'pictures', 'begin_date', 'end_date', 'duration', - 'start_time', 'contact', 'email', 'website', 'organizer', 'speaker', 'type', 'accessibility', 'meeting_point', + 'start_time', 'contact', 'email', 'website', 'organizer', 'organizers', 'speaker', 'type', 'accessibility', 'meeting_point', 'capacity', 'booking', 'target_audience', 'practical_info', 'approved', ]) From 8ceffbf13ea4f2f9d19880487b87a11ee9093f29 Mon Sep 17 00:00:00 2001 From: TheoLechemia Date: Thu, 22 Feb 2024 16:10:01 +0100 Subject: [PATCH 06/10] tests --- geotrek/api/tests/test_v2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geotrek/api/tests/test_v2.py b/geotrek/api/tests/test_v2.py index 11ede453fb..0845aaa68a 100644 --- a/geotrek/api/tests/test_v2.py +++ b/geotrek/api/tests/test_v2.py @@ -228,7 +228,7 @@ TOURISTIC_EVENT_DETAIL_JSON_STRUCTURE = sorted([ 'id', 'accessibility', 'approved', 'attachments', 'begin_date', 'bookable', 'booking', 'cities', 'contact', 'create_datetime', 'description', 'description_teaser', 'districts', 'duration', 'email', 'end_date', 'external_id', 'geometry', - 'meeting_point', 'start_time', 'meeting_time', 'end_time', 'name', 'organizers', 'organizers_id', 'capacity', 'pdf', 'place', 'portal', + 'meeting_point', 'start_time', 'meeting_time', 'end_time', 'name', 'organizer', 'organizers', 'organizers_id', 'capacity', 'pdf', 'place', 'portal', 'practical_info', 'provider', 'published', 'source', 'speaker', 'structure', 'target_audience', 'themes', 'type', 'update_datetime', 'url', 'uuid', 'website', 'cancelled', 'cancellation_reason', 'participant_number' ]) From c4807c0d727b8daba6dd8f3736bdcd3221417368 Mon Sep 17 00:00:00 2001 From: TheoLechemia Date: Thu, 22 Feb 2024 16:10:36 +0100 Subject: [PATCH 07/10] fix parser with str value on m2m --- geotrek/api/mobile/serializers/tourism.py | 2 +- geotrek/tourism/parsers.py | 12 +++++++----- geotrek/tourism/tests/test_parsers.py | 10 ++++++++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/geotrek/api/mobile/serializers/tourism.py b/geotrek/api/mobile/serializers/tourism.py index 33ade6c07e..7c7287d298 100644 --- a/geotrek/api/mobile/serializers/tourism.py +++ b/geotrek/api/mobile/serializers/tourism.py @@ -51,7 +51,7 @@ def get_pictures(self, obj): 'legend': first_picture.legend, 'url': os.path.join('/', str(self.context['root_pk']), settings.MEDIA_URL[1:], thdetail_first.name), }] - + def get_organizers(self, obj): return ", ".join( map(lambda org: org.label, obj.organizers.all()) diff --git a/geotrek/tourism/parsers.py b/geotrek/tourism/parsers.py index 92979b464d..857dab45cd 100644 --- a/geotrek/tourism/parsers.py +++ b/geotrek/tourism/parsers.py @@ -284,7 +284,7 @@ class TouristicEventApidaeParser(AttachmentApidaeParserMixin, ApidaeParser): ] m2m_fields = { 'themes': 'informationsFeteEtManifestation.themes.*.libelleFr', - 'organizers': 'informations.structureGestion.nom.libelleFr', + 'organizers': ('informations.structureGestion.nom.libelleFr',), } natural_keys = { 'themes': 'label', @@ -293,6 +293,7 @@ class TouristicEventApidaeParser(AttachmentApidaeParserMixin, ApidaeParser): 'source': 'name', 'portal': 'name', } + # separator = "," def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -351,7 +352,6 @@ def filter_capacity(self, src, val): def filter_email(self, src, val): return self._filter_comm(val, 204, multiple=False) - def filter_website(self, src, val): return self._filter_comm(val, 205, multiple=False) @@ -940,16 +940,17 @@ class LEITouristicEventParser(LEIParser): 'ADRPROD_TEL', 'ADRPROD_TEL2', 'ADRPREST_TEL', 'ADRPREST_TEL2'), 'email': ('ADRPROD_EMAIL', 'ADRPREST_EMAIL', 'ADRPREST_EMAIL2'), 'website': ('ADRPROD_URL', 'ADRPREST_URL'), - 'organizers': ('RAISONSOC_PERSONNE_EN_CHARGE', 'RAISONSOC_RESPONSABLE'), 'speaker': ('CIVILITE_RESPONSABLE', 'NOM_RESPONSABLE', 'PRENOM_RESPONSABLE'), 'type': 'TYPE_NOM', 'geom': ('LATITUDE', 'LONGITUDE'), } - m2m_fields = {} + m2m_fields = { + 'organizers': ('RAISONSOC_PERSONNE_EN_CHARGE', 'RAISONSOC_RESPONSABLE') + } type = None natural_keys = { 'category': 'label', - 'organizer': 'label', + 'organizers': 'label', 'geom': {'required': True}, 'type': 'type', } @@ -959,6 +960,7 @@ class LEITouristicEventParser(LEIParser): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.field_options['organizers'] = {'create': True} if self.type: self.constant_fields['type'] = self.type diff --git a/geotrek/tourism/tests/test_parsers.py b/geotrek/tourism/tests/test_parsers.py index 845f3b8d82..9be26d95a6 100644 --- a/geotrek/tourism/tests/test_parsers.py +++ b/geotrek/tourism/tests/test_parsers.py @@ -461,14 +461,16 @@ def mocked_json(): self.assertIn("Langues Parlées:
Français
", event.practical_info) self.assertIn("Accès:
TestFr
", event.practical_info) self.assertTrue(event.published) - organizers = event.organizers.all() - self.assertEqual(organizers[0].label, 'Toto') self.assertEqual(str(event.start_time), '09:00:00') self.assertEqual(event.type.type, 'Sports') self.assertQuerysetEqual( event.themes.all(), ['', ''] ) + self.assertQuerysetEqual( + event.organizers.all(), + [''] + ) self.assertEqual(Attachment.objects.count(), 3) self.assertEqual(TouristicEventApidaeParser().filter_capacity("capacity", "12"), 12) @@ -872,6 +874,10 @@ def mocked_requests_get(*args, **kwargs): self.assertIn("Description A", event.description) self.assertEqual(Attachment.objects.count(), 2) self.assertEqual(Attachment.objects.first().content_object, event) + self.assertQuerysetEqual( + event.organizers.all(), + [''] + ) class TestGeotrekTouristicContentParser(GeotrekTouristicContentParser): From ced721caa660f1fb6528cb5872012054a7c324a0 Mon Sep 17 00:00:00 2001 From: TheoLechemia Date: Fri, 23 Feb 2024 15:51:31 +0100 Subject: [PATCH 08/10] use PrimaryKeyRelatedField for organizers_id --- geotrek/api/v2/serializers.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/geotrek/api/v2/serializers.py b/geotrek/api/v2/serializers.py index f3ffada373..6003640fc4 100644 --- a/geotrek/api/v2/serializers.py +++ b/geotrek/api/v2/serializers.py @@ -513,7 +513,7 @@ def get_departure_city(self, obj): class TouristicEventSerializer(TouristicModelSerializer): organizers = serializers.SerializerMethodField() organizer = serializers.SerializerMethodField() - organizers_id = serializers.SerializerMethodField() + organizers_id = serializers.PrimaryKeyRelatedField(many=True, source='organizers', read_only=True) attachments = AttachmentSerializer(many=True, source='sorted_attachments') url = HyperlinkedIdentityField(view_name='apiv2:touristicevent-detail') begin_date = serializers.DateField() @@ -557,11 +557,6 @@ def get_organizers(self, obj): # for retrocompatibility of API get_organizer = get_organizers - def get_organizers_id(self, obj): - return ", ".join( - map(lambda org: org.label, obj.organizers.all()) - ) - class Meta(TimeStampedSerializer.Meta): model = tourism_models.TouristicEvent fields = TimeStampedSerializer.Meta.fields + ( From 3ee3e3aa45e40b07b8a2d7b27bd3b47175aa5a4c Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Thu, 29 Feb 2024 12:25:25 +0100 Subject: [PATCH 09/10] merge migrations --- .../tourism/migrations/0051_merge_20240229_1225.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 geotrek/tourism/migrations/0051_merge_20240229_1225.py diff --git a/geotrek/tourism/migrations/0051_merge_20240229_1225.py b/geotrek/tourism/migrations/0051_merge_20240229_1225.py new file mode 100644 index 0000000000..4427ced73e --- /dev/null +++ b/geotrek/tourism/migrations/0051_merge_20240229_1225.py @@ -0,0 +1,14 @@ +# Generated by Django 3.2.24 on 2024-02-29 11:25 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tourism', '0050_alter_touristiceventorganizer_label'), + ('tourism', '0050_auto_20231220_0952'), + ] + + operations = [ + ] From d133c9abed81c5f8a42b1b40e9031749a761392e Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Thu, 29 Feb 2024 14:44:35 +0100 Subject: [PATCH 10/10] fix tests --- geotrek/tourism/forms.py | 4 ++-- geotrek/tourism/tests/test_forms.py | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/geotrek/tourism/forms.py b/geotrek/tourism/forms.py index 4697d65e7f..9c2c70c8b7 100644 --- a/geotrek/tourism/forms.py +++ b/geotrek/tourism/forms.py @@ -145,8 +145,8 @@ def __init__(self, *args, **kwargs): self.fields['start_time'].widget.attrs['placeholder'] = _('HH:MM') self.fields['end_time'].widget.attrs['placeholder'] = _('HH:MM') if self.user.has_perm("tourism.add_touristiceventorganizer"): - self.fields['organizer'].widget = SelectMultipleWithPop( - choices=self.fields['organizer'].choices, + self.fields['organizers'].widget = SelectMultipleWithPop( + choices=self.fields['organizers'].choices, add_url=TouristicEventOrganizer.get_add_url() ) # Since we use chosen() in trek_form.html, we don't need the default help text diff --git a/geotrek/tourism/tests/test_forms.py b/geotrek/tourism/tests/test_forms.py index 8760f09f70..822b05d073 100644 --- a/geotrek/tourism/tests/test_forms.py +++ b/geotrek/tourism/tests/test_forms.py @@ -1,5 +1,4 @@ - -from django.forms.widgets import Select +from django.forms.widgets import SelectMultiple from django.test import TestCase from django.contrib.auth.models import Permission @@ -9,7 +8,7 @@ from geotrek.tourism.forms import TouristicEventForm -class PathFormTest(TestCase): +class TouristicEventFormTestCase(TestCase): def test_begin_end_time(self): data = { 'geom': '{"type": "Point", "coordinates":[0, 0]}', @@ -92,20 +91,20 @@ def test_begin_end_date(self): def test_organizers_widget_select(self): # if user has 'add_touristiceventorganizer' permission the widget must be a SelectMultipleWithPop - # otherwith a Select + # otherwise a Select form = TouristicEventForm( user=UserFactory(), data={} ) - assert type(form.fields['organizer'].widget) is Select + self.assertEqual(type(form.fields['organizers'].widget), SelectMultiple) def test_organizers_widget_select_multiple_with_pop(self): # if user has 'add_touristiceventorganizer' permission the widget must be a SelectMultipleWithPop - # otherwith a Select + # otherwise a Select user = UserFactory() user.user_permissions.add(Permission.objects.get(codename='add_touristiceventorganizer')) form = TouristicEventForm( user=user, data={} ) - assert type(form.fields['organizer'].widget) is SelectMultipleWithPop + self.assertEqual(type(form.fields['organizers'].widget), SelectMultipleWithPop)