diff --git a/docs/changelog.rst b/docs/changelog.rst index 87ed164a3b..1b4979962c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ CHANGELOG 2.102.1+dev (XXXX-XX-XX) ------------------------ +- Add popup button to add organizer in touristic event form **New features** diff --git a/geotrek/tourism/forms.py b/geotrek/tourism/forms.py index a92670a311..cf5aef6169 100644 --- a/geotrek/tourism/forms.py +++ b/geotrek/tourism/forms.py @@ -4,9 +4,11 @@ from geotrek.tourism.widgets import AutoLocateMapWidget from crispy_forms.layout import Div, HTML, Fieldset +from mapentity.widgets import SelectMultipleWithPop + from .models import (TouristicContent, TouristicEvent, TouristicEventParticipantCount, - TouristicEventParticipantCategory) + TouristicEventParticipantCategory, TouristicEventOrganizer) from geotrek.common.forms import CommonForm @@ -142,6 +144,11 @@ def __init__(self, *args, **kwargs): self.fields['end_date'].widget.attrs['placeholder'] = _('dd/mm/yyyy') 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, + add_url=TouristicEventOrganizer.get_add_url() + ) # Since we use chosen() in trek_form.html, we don't need the default help text for f in ['themes', 'source']: self.fields[f].help_text = '' @@ -189,3 +196,9 @@ def _save_m2m(self): TouristicEventParticipantCount.objects.update_or_create(event=self.instance, category=category, defaults={'count': count}) else: TouristicEventParticipantCount.objects.filter(event=self.instance, category=category).delete() + + +class TouristicEventOrganizerFormPopup(CommonForm): + class Meta: + model = TouristicEventOrganizer + fields = ['label'] diff --git a/geotrek/tourism/models.py b/geotrek/tourism/models.py index 5b685ba716..d8a40b9009 100644 --- a/geotrek/tourism/models.py +++ b/geotrek/tourism/models.py @@ -11,6 +11,7 @@ from django.dispatch import receiver from django.utils.formats import date_format from django.utils.translation import gettext_lazy as _ +from django.urls import reverse from easy_thumbnails.alias import aliases from easy_thumbnails.exceptions import InvalidImageFormatError from easy_thumbnails.files import get_thumbnailer @@ -387,7 +388,7 @@ def __str__(self): class TouristicEventOrganizer(TimeStampedModelMixin): - label = models.CharField(verbose_name=_("Label"), max_length=256) + label = models.CharField(verbose_name=_("OrganizerLabel"), max_length=256) class Meta: verbose_name = _("Organizer") @@ -397,6 +398,10 @@ class Meta: def __str__(self): return self.label + @classmethod + def get_add_url(cls): + return reverse('tourism:organizer_add') + class TouristicEvent(ZoningPropertiesMixin, AddPropertyMixin, PublishableMixin, GeotrekMapEntityMixin, StructureRelated, PicturesMixin, TimeStampedModelMixin, NoDeleteMixin): diff --git a/geotrek/tourism/templates/tourism/touristiceventorganizer_form.html b/geotrek/tourism/templates/tourism/touristiceventorganizer_form.html new file mode 100644 index 0000000000..7ce60b145f --- /dev/null +++ b/geotrek/tourism/templates/tourism/touristiceventorganizer_form.html @@ -0,0 +1,11 @@ +{% extends "mapentity/base_site.html" %} +{% load static i18n crispy_forms_tags %} + +{% block title %}{% trans "Add Organizer" %}{% endblock title %} + +{% block navbar %}{% endblock navbar %} + +{% block content %} +

{% trans "Add Organizer"%}

+{% crispy form %} +{% endblock content %} diff --git a/geotrek/tourism/tests/test_forms.py b/geotrek/tourism/tests/test_forms.py index 16c3ed594a..be23bf4024 100644 --- a/geotrek/tourism/tests/test_forms.py +++ b/geotrek/tourism/tests/test_forms.py @@ -1,5 +1,10 @@ + +from django.forms.widgets import Select from django.test import TestCase +from django.contrib.auth.models import Permission + +from mapentity.widgets import SelectMultipleWithPop from geotrek.authent.tests.factories import UserFactory from geotrek.tourism.forms import TouristicEventForm @@ -85,3 +90,23 @@ def test_begin_end_date(self): ) self.assertFalse(form1.is_valid()) self.assertIn("Start date is after end date", str(form1.errors)) + + def test_organizers_widget_select(self): + # if user has 'add_touristiceventorganizer' permission the widget must be a SelectMultipleWithPop + # otherwith a Select + form = TouristicEventForm( + user=UserFactory(), + data={} + ) + assert type(form.fields['organizer'].widget) is Select + + def test_organizers_widget_select_multiple_with_pop(self): + # if user has 'add_touristiceventorganizer' permission the widget must be a SelectMultipleWithPop + # otherwith 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 diff --git a/geotrek/tourism/tests/test_models.py b/geotrek/tourism/tests/test_models.py index 34ca55bfab..4cad65ba39 100644 --- a/geotrek/tourism/tests/test_models.py +++ b/geotrek/tourism/tests/test_models.py @@ -9,7 +9,7 @@ from mapentity.middleware import clear_internal_user_cache from geotrek.core.tests import factories as core_factories -from geotrek.tourism.models import TouristicContentType +from geotrek.tourism.models import TouristicContentType, TouristicEventOrganizer from geotrek.tourism.tests import factories as tourism_factories from geotrek.tourism.tests.factories import (InformationDeskFactory, InformationDeskTypeFactory, @@ -149,6 +149,10 @@ def test_str(self): organizer = tourism_factories.TouristicEventOrganizerFactory(label="foo bar") self.assertEqual('foo bar', str(organizer)) + def test_get_add_url(self): + url = TouristicEventOrganizer.get_add_url() + self.assertEqual(url, "/popup/add/organizer/") + class TouristicEventModelTest(TestCase): def test_dates_display_no_end_date(self): diff --git a/geotrek/tourism/tests/test_views.py b/geotrek/tourism/tests/test_views.py index 86559b244e..5085aaed60 100644 --- a/geotrek/tourism/tests/test_views.py +++ b/geotrek/tourism/tests/test_views.py @@ -7,7 +7,8 @@ from unittest import mock from django.conf import settings -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group, Permission + from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase from django.test.utils import override_settings @@ -486,6 +487,21 @@ def test_not_published_document_pdf(self): self.assertEqual(response.status_code, 403) +class TouristicEventOrganizerCreatePopupTest(TestCase): + def test_cannot_create_organizer(self): + url = '/popup/add/organizer/' + response = self.client.get(url) + self.assertRedirects(response, "/login/?next=/popup/add/organizer/") + + def test_can_create_organizer(self): + user = UserFactory() + user.user_permissions.add(Permission.objects.get(codename='add_touristiceventorganizer')) + self.client.force_login(user=user) + url = '/popup/add/organizer/' + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + class TouristicEventCustomViewTests(TrekkingManagerTest): @mock.patch('mapentity.helpers.requests.get') def test_public_document_pdf(self, mocked): diff --git a/geotrek/tourism/urls.py b/geotrek/tourism/urls.py index e0bc694d0d..6226e8eaf2 100644 --- a/geotrek/tourism/urls.py +++ b/geotrek/tourism/urls.py @@ -22,6 +22,8 @@ path('api//touristiccategories.json', tourism_views.TouristicCategoryView.as_view(), name="touristic_categories_json"), path('api//touristiccontents//meta.html', tourism_views.TouristicContentMeta.as_view(), name="touristiccontent_meta"), path('api//touristicevents//meta.html', tourism_views.TouristicEventMeta.as_view(), name="touristicevent_meta"), + path('popup/add/organizer/', tourism_views.TouristicEventOrganizerCreatePopup.as_view(), name='organizer_add'), + ] diff --git a/geotrek/tourism/views.py b/geotrek/tourism/views.py index 9119e031df..f9fb011292 100644 --- a/geotrek/tourism/views.py +++ b/geotrek/tourism/views.py @@ -2,12 +2,16 @@ import os from django.conf import settings +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 Q, Sum -from django.http import Http404 +from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404 +from django.utils.decorators import method_decorator from django.utils.translation import gettext as _ -from django.views.generic import DetailView +from django.utils.html import escape +from django.views.generic import DetailView, CreateView from django_filters.rest_framework import DjangoFilterBackend from mapentity.views import (MapEntityCreate, MapEntityUpdate, MapEntityList, MapEntityDetail, MapEntityDelete, MapEntityFormat, MapEntityDocument) @@ -25,8 +29,8 @@ from geotrek.common.viewsets import GeotrekMapentityViewSet from geotrek.trekking.models import Trek from .filters import TouristicContentFilterSet, TouristicEventFilterSet, TouristicEventApiFilterSet -from .forms import TouristicContentForm, TouristicEventForm -from .models import (TouristicContent, TouristicEvent, TouristicContentCategory, InformationDesk) +from .forms import TouristicContentForm, TouristicEventForm, TouristicEventOrganizerFormPopup +from .models import (TouristicContent, TouristicEvent, TouristicContentCategory, TouristicEventOrganizer, InformationDesk) from .serializers import (TouristicContentSerializer, TouristicEventSerializer, TouristicContentAPIGeojsonSerializer, TouristicEventAPIGeojsonSerializer, InformationDeskGeojsonSerializer, TouristicContentAPISerializer, TouristicEventAPISerializer, @@ -275,6 +279,23 @@ def get_context_data(self, **kwargs): return context +class TouristicEventOrganizerCreatePopup(CreateView): + model = TouristicEventOrganizer + form_class = TouristicEventOrganizerFormPopup + + @method_decorator(login_required) + def dispatch(self, request, *args, **kwargs): + if not request.user.has_perm("tourism.add_touristiceventorganizer"): + raise PermissionDenied + return super().dispatch(request, *args, **kwargs) + + def form_valid(self, form): + self.object = form.save() + return HttpResponse(""" + + """ % (escape(form.instance._get_pk_val()), escape(form.instance))) + + class TouristicEventDocumentPublic(TouristicEventDocumentPublicMixin, DocumentPublic): pass