diff --git a/docs/changelog.rst b/docs/changelog.rst index cfee2c0cd5..44e4e865a3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -17,6 +17,10 @@ CHANGELOG - Display link to attachment in admin site for attachments - Add license field on attachments (#3089) [thanks to Paul Florence] +- If `COMPLETENESS_FIELDS` is set for a model an object is published, + display completeness fields if missing on page detail +- Avoid publication or review if `COMPLETENESS_FIELDS` is set for a model, + and `COMPLETENESS_LEVEL` is one of 'error_on_publication' and 'error_on_review' **Bug fixes** diff --git a/docs/install/advanced-configuration.rst b/docs/install/advanced-configuration.rst index 38bffb0713..3b20357225 100644 --- a/docs/install/advanced-configuration.rst +++ b/docs/install/advanced-configuration.rst @@ -855,6 +855,26 @@ For each module, use the following syntax to configure fields to hide in the cre Please refer to the "settings detail" section for a complete list of modules and hideable fields. +Configure form fields required or needed for review or publication +------------------------------------------------------------------- + +Set 'error_on_publication' to avoid publication without completeness fields +and 'error_on_review' if you want this fields to be required before sending to review. + +:: + + COMPLETENESS_LEVEL = 'warning' + +For each module, configure fields to be needed or required on review or publication + +:: + + COMPLETENESS_FIELDS = { + 'trek': ['practice', 'departure', 'duration', 'difficulty', 'description_teaser'], + 'dive': ['practice', 'difficulty', 'description_teaser'], + } + + ================ Settings details ================ diff --git a/geotrek/common/forms.py b/geotrek/common/forms.py index 6ef6a69898..e83a59222c 100644 --- a/geotrek/common/forms.py +++ b/geotrek/common/forms.py @@ -7,7 +7,7 @@ from django.db.models import Q from django.db.models.query import QuerySet from django.db.models.fields.related import ForeignKey, ManyToManyField -from django.core.exceptions import FieldDoesNotExist +from django.core.exceptions import FieldDoesNotExist, ValidationError from django.forms.widgets import HiddenInput from django.urls import reverse from django.utils.text import format_lazy @@ -17,6 +17,8 @@ from geotrek.authent.models import default_structure, StructureRelated, StructureOrNoneRelated from geotrek.common.models import AccessibilityAttachment +from geotrek.common.mixins.models import PublishableMixin +from geotrek.common.utils.translation import get_translated_fields from .mixins.models import NoDeleteMixin @@ -130,28 +132,50 @@ def __init__(self, *args, **kwargs): self.fields[field_to_hide].widget = HiddenInput() def clean(self): + """Check field data with structure and completeness fields if relevant""" structure = self.cleaned_data.get('structure') - if not structure: - return self.cleaned_data - - # Copy cleaned_data because self.add_error may remove an item - for name, field in self.cleaned_data.copy().items(): - try: - modelfield = self.instance._meta.get_field(name) - except FieldDoesNotExist: - continue - if not isinstance(modelfield, (ForeignKey, ManyToManyField)): - continue - model = modelfield.remote_field.model - if not issubclass(model, (StructureRelated, StructureOrNoneRelated)): - continue - if not model.check_structure_in_forms: - continue - if isinstance(field, QuerySet): - for value in field: - self.check_structure(value, structure, name) - else: - self.check_structure(field, structure, name) + + # if structure in form, check each field same structure + if structure: + # Copy cleaned_data because self.add_error may remove an item + for name, field in self.cleaned_data.copy().items(): + try: + modelfield = self.instance._meta.get_field(name) + except FieldDoesNotExist: + continue + if not isinstance(modelfield, (ForeignKey, ManyToManyField)): + continue + model = modelfield.remote_field.model + if not issubclass(model, (StructureRelated, StructureOrNoneRelated)): + continue + if not model.check_structure_in_forms: + continue + if isinstance(field, QuerySet): + for value in field: + self.check_structure(value, structure, name) + else: + self.check_structure(field, structure, name) + + # If model is publishable or reviewable, + # check if completeness fields are required, and raise error if some fields are missing + if self.completeness_fields_are_required(): + missing_fields = [] + completeness_fields = settings.COMPLETENESS_FIELDS.get(self._meta.model._meta.model_name, []) + if settings.COMPLETENESS_LEVEL == 'error_on_publication': + missing_fields = self._get_missing_completeness_fields(completeness_fields, + _('This field is required to publish object.')) + elif settings.COMPLETENESS_LEVEL == 'error_on_review': + missing_fields = self._get_missing_completeness_fields(completeness_fields, + _('This field is required to review object.')) + + if missing_fields: + raise ValidationError( + _('Fields are missing to publish or review object: %(fields)s'), + params={ + 'fields': ', '.join(missing_fields) + }, + ) + return self.cleaned_data def check_structure(self, obj, structure, name): @@ -160,6 +184,65 @@ def check_structure(self, obj, structure, name): self.add_error(name, format_lazy(_("Please select a choice related to all structures (without brackets) " "or related to the structure {struc} (in brackets)"), struc=structure)) + @property + def any_published(self): + """Check if form has published in at least one of the language""" + return any([self.cleaned_data.get(f'published_{language[0]}', False) + for language in settings.MAPENTITY_CONFIG['TRANSLATED_LANGUAGES']]) + + @property + def published_languages(self): + """Returns languages in which the form has published data. + """ + languages = [language[0] for language in settings.MAPENTITY_CONFIG['TRANSLATED_LANGUAGES']] + if settings.PUBLISHED_BY_LANG: + return [language for language in languages if self.cleaned_data.get(f'published_{language}', None)] + else: + if self.any_published: + return languages + + def completeness_fields_are_required(self): + """Return True if the completeness fields are required""" + if not issubclass(self._meta.model, PublishableMixin): + return False + + if not self.instance.is_complete(): + if settings.COMPLETENESS_LEVEL == 'error_on_publication': + if self.any_published: + return True + elif settings.COMPLETENESS_LEVEL == 'error_on_review': + # Error on review implies error on publication + if self.cleaned_data['review'] or self.any_published: + return True + + return False + + def _get_missing_completeness_fields(self, completeness_fields, msg): + """Check fields completeness and add error message if field is empty""" + + missing_fields = [] + translated_fields = get_translated_fields(self._meta.model) + + # Add error on each field if it is empty + for field_required in completeness_fields: + if field_required in translated_fields: + if self.cleaned_data.get('review') and settings.COMPLETENESS_LEVEL == 'error_on_review': + # get field for first language only + field_required_lang = f"{field_required}_{settings.MAPENTITY_CONFIG['TRANSLATED_LANGUAGES'][0][0]}" + missing_fields.append(field_required_lang) + self.add_error(field_required_lang, msg) + else: + for language in self.published_languages: + field_required_lang = f'{field_required}_{language}' + if not self.cleaned_data.get(field_required_lang): + missing_fields.append(field_required_lang) + self.add_error(field_required_lang, msg) + else: + if not self.cleaned_data.get(field_required): + missing_fields.append(field_required) + self.add_error(field_required, msg) + return missing_fields + def save(self, commit=True): """Set structure field before saving if need be""" if self.update: # Structure is already set on object. diff --git a/geotrek/common/locale/de/LC_MESSAGES/django.po b/geotrek/common/locale/de/LC_MESSAGES/django.po index 0cf2b33cc8..34da6067cb 100644 --- a/geotrek/common/locale/de/LC_MESSAGES/django.po +++ b/geotrek/common/locale/de/LC_MESSAGES/django.po @@ -32,6 +32,13 @@ msgstr "" msgid "max %s" msgstr "" +msgid "This field is required to publish object." +msgstr "" + +#, python-format +msgid "Fields are missing to publish object: %(fields)s" +msgstr "" + #, python-brace-format msgid "" "Please select a choice related to all structures (without brackets) or " @@ -501,6 +508,9 @@ msgstr "" msgid "looks incomplete;" msgstr "" +msgid "these fields should be completed:" +msgstr "" + msgid "has invalid geometry;" msgstr "" diff --git a/geotrek/common/locale/en/LC_MESSAGES/django.po b/geotrek/common/locale/en/LC_MESSAGES/django.po index 0cf2b33cc8..34da6067cb 100644 --- a/geotrek/common/locale/en/LC_MESSAGES/django.po +++ b/geotrek/common/locale/en/LC_MESSAGES/django.po @@ -32,6 +32,13 @@ msgstr "" msgid "max %s" msgstr "" +msgid "This field is required to publish object." +msgstr "" + +#, python-format +msgid "Fields are missing to publish object: %(fields)s" +msgstr "" + #, python-brace-format msgid "" "Please select a choice related to all structures (without brackets) or " @@ -501,6 +508,9 @@ msgstr "" msgid "looks incomplete;" msgstr "" +msgid "these fields should be completed:" +msgstr "" + msgid "has invalid geometry;" msgstr "" diff --git a/geotrek/common/locale/es/LC_MESSAGES/django.po b/geotrek/common/locale/es/LC_MESSAGES/django.po index d1ba4b2d3f..730bd7f696 100644 --- a/geotrek/common/locale/es/LC_MESSAGES/django.po +++ b/geotrek/common/locale/es/LC_MESSAGES/django.po @@ -32,6 +32,13 @@ msgstr "min %s" msgid "max %s" msgstr "max %s" +msgid "This field is required to publish object." +msgstr "" + +#, python-format +msgid "Fields are missing to publish object: %(fields)s" +msgstr "" + #, python-brace-format msgid "" "Please select a choice related to all structures (without brackets) or " @@ -501,6 +508,9 @@ msgstr "" msgid "looks incomplete;" msgstr "parece incompleto;" +msgid "these fields should be completed:" +msgstr "" + msgid "has invalid geometry;" msgstr "tiene una geometría invalida;" diff --git a/geotrek/common/locale/fr/LC_MESSAGES/django.po b/geotrek/common/locale/fr/LC_MESSAGES/django.po index 8a9d409af0..4f0ef8fd27 100644 --- a/geotrek/common/locale/fr/LC_MESSAGES/django.po +++ b/geotrek/common/locale/fr/LC_MESSAGES/django.po @@ -33,6 +33,13 @@ msgstr "min %s" msgid "max %s" msgstr "max %s" +msgid "This field is required to publish object." +msgstr "Ce champs est obligatoire pour publier l'objet." + +#, python-format +msgid "Fields are missing to publish object: %(fields)s" +msgstr "Ces champs doivent être renseignés pour publier l'objet : %(fields)s" + #, python-brace-format msgid "" "Please select a choice related to all structures (without brackets) or " @@ -412,7 +419,9 @@ msgid "Init sync ..." msgstr "Début de synchro" msgid "Photos to illustrate information related to the accessibility of treks." -msgstr "Photos pour illustrer les informations liées à l'accessibilité des randonnées." +msgstr "" +"Photos pour illustrer les informations liées à l'accessibilité des " +"randonnées." msgid "Photos accessibility" msgstr "Photos accessibilité" @@ -519,6 +528,9 @@ msgstr "L'objet est publié mais" msgid "looks incomplete;" msgstr "semble incomplet ;" +msgid "these fields should be completed:" +msgstr "ces champs doivent être renseignés :" + msgid "has invalid geometry;" msgstr "a une géométrie invalide ;" diff --git a/geotrek/common/locale/it/LC_MESSAGES/django.po b/geotrek/common/locale/it/LC_MESSAGES/django.po index a1d411f30d..adc206925f 100644 --- a/geotrek/common/locale/it/LC_MESSAGES/django.po +++ b/geotrek/common/locale/it/LC_MESSAGES/django.po @@ -32,6 +32,13 @@ msgstr "min %s" msgid "max %s" msgstr "max %s" +msgid "This field is required to publish object." +msgstr "" + +#, python-format +msgid "Fields are missing to publish object: %(fields)s" +msgstr "" + #, python-brace-format msgid "" "Please select a choice related to all structures (without brackets) or " @@ -501,6 +508,9 @@ msgstr "" msgid "looks incomplete;" msgstr "" +msgid "these fields should be completed:" +msgstr "" + msgid "has invalid geometry;" msgstr "" diff --git a/geotrek/common/locale/nl/LC_MESSAGES/django.po b/geotrek/common/locale/nl/LC_MESSAGES/django.po index 0cf2b33cc8..b801bc371a 100644 --- a/geotrek/common/locale/nl/LC_MESSAGES/django.po +++ b/geotrek/common/locale/nl/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-04-25 09:07+0000\n" +"POT-Creation-Date: 2022-03-29 10:10+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -32,6 +32,13 @@ msgstr "" msgid "max %s" msgstr "" +msgid "This field is required to publish object." +msgstr "" + +#, python-format +msgid "Fields are missing to publish object: %(fields)s" +msgstr "" + #, python-brace-format msgid "" "Please select a choice related to all structures (without brackets) or " @@ -501,6 +508,9 @@ msgstr "" msgid "looks incomplete;" msgstr "" +msgid "these fields should be completed:" +msgstr "" + msgid "has invalid geometry;" msgstr "" diff --git a/geotrek/common/mixins/views.py b/geotrek/common/mixins/views.py index 3d5ff66ecc..8cbf69346e 100644 --- a/geotrek/common/mixins/views.py +++ b/geotrek/common/mixins/views.py @@ -228,3 +228,16 @@ def get_context_data(self, **kwargs): modelname = self.get_model()._meta.object_name.lower() context['mapimage_ratio'] = settings.EXPORT_MAP_IMAGE_SIZE[modelname] return context + + +class CompletenessMixin: + """Mixin for completeness fields""" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + modelname = self.get_model()._meta.object_name.lower() + if modelname in settings.COMPLETENESS_FIELDS: + obj = context['object'] + completeness_fields = settings.COMPLETENESS_FIELDS[modelname] + context['completeness_fields'] = [obj._meta.get_field(field).verbose_name for field in completeness_fields] + return context diff --git a/geotrek/common/parsers.py b/geotrek/common/parsers.py index 66eb8d76e3..94a18c4676 100644 --- a/geotrek/common/parsers.py +++ b/geotrek/common/parsers.py @@ -29,10 +29,10 @@ from geotrek.authent.models import default_structure from geotrek.common.models import FileType, Attachment +from geotrek.common.utils.translation import get_translated_fields if 'modeltranslation' in settings.INSTALLED_APPS: from modeltranslation.fields import TranslationField - from modeltranslation.translator import translator, NotRegistered logger = logging.getLogger(__name__) @@ -90,13 +90,7 @@ def __init__(self, progress_cb=None, user=None, encoding='utf8'): self.user = user self.structure = user and user.profile.structure or default_structure() self.encoding = encoding - - try: - mto = translator.get_options_for_model(self.model) - except NotRegistered: - self.translated_fields = [] - else: - self.translated_fields = mto.fields.keys() + self.translated_fields = get_translated_fields(self.model) if self.fields is None: self.fields = { diff --git a/geotrek/common/templates/common/publishable_completeness_fragment.html b/geotrek/common/templates/common/publishable_completeness_fragment.html index 3567658d0e..17fd36134b 100644 --- a/geotrek/common/templates/common/publishable_completeness_fragment.html +++ b/geotrek/common/templates/common/publishable_completeness_fragment.html @@ -9,7 +9,11 @@ {% else %} {% if not object.is_publishable %}
{% trans "Object is published but" %} - {% if not object.is_complete %}{% trans "looks incomplete;" %}{% endif %} + {% if not object.is_complete %} + {% trans "looks incomplete;" %} + {% trans "these fields should be completed:" %}
+ {{ completeness_fields|join:', ' }} + {% endif %} {% if not object.has_geom_valid %}{% trans "has invalid geometry;" %}{% endif %}
{% endif %} diff --git a/geotrek/common/utils/translation.py b/geotrek/common/utils/translation.py new file mode 100644 index 0000000000..099d22378a --- /dev/null +++ b/geotrek/common/utils/translation.py @@ -0,0 +1,15 @@ +from django.conf import settings + +if 'modeltranslation' in settings.INSTALLED_APPS: + from modeltranslation.translator import translator, NotRegistered + + +def get_translated_fields(model): + """Get translated fields from a model""" + try: + mto = translator.get_options_for_model(model) + except NotRegistered: + translated_fields = [] + else: + translated_fields = mto.fields.keys() + return translated_fields diff --git a/geotrek/diving/views.py b/geotrek/diving/views.py index 29320a68d9..b44b4773b9 100644 --- a/geotrek/diving/views.py +++ b/geotrek/diving/views.py @@ -11,7 +11,7 @@ from geotrek.authent.decorators import same_structure_required from geotrek.common.mixins.api import APIViewSet -from geotrek.common.mixins.views import CustomColumnsMixin, MetaMixin +from geotrek.common.mixins.views import CompletenessMixin, CustomColumnsMixin, MetaMixin from geotrek.common.models import RecordSource, TargetPortal from geotrek.common.views import DocumentPublic, DocumentBookletPublic, MarkupPublic from geotrek.common.viewsets import GeotrekMapentityViewSet @@ -50,7 +50,7 @@ class DiveFormatList(MapEntityFormat, DiveList): ] -class DiveDetail(MapEntityDetail): +class DiveDetail(CompletenessMixin, MapEntityDetail): queryset = Dive.objects.existing() def dispatch(self, *args, **kwargs): diff --git a/geotrek/outdoor/views.py b/geotrek/outdoor/views.py index fed20d6f79..1bb35786ab 100644 --- a/geotrek/outdoor/views.py +++ b/geotrek/outdoor/views.py @@ -7,7 +7,7 @@ from geotrek.authent.decorators import same_structure_required from geotrek.common.mixins.api import APIViewSet -from geotrek.common.mixins.views import CustomColumnsMixin +from geotrek.common.mixins.views import CompletenessMixin, CustomColumnsMixin from geotrek.common.views import DocumentBookletPublic, DocumentPublic, MarkupPublic from geotrek.common.viewsets import GeotrekMapentityViewSet from geotrek.outdoor.filters import SiteFilterSet, CourseFilterSet @@ -31,7 +31,7 @@ class SiteList(CustomColumnsMixin, MapEntityList): searchable_columns = ['id', 'name'] -class SiteDetail(MapEntityDetail): +class SiteDetail(CompletenessMixin, MapEntityDetail): queryset = Site.objects.all() def get_context_data(self, *args, **kwargs): @@ -153,7 +153,7 @@ class CourseList(CustomColumnsMixin, MapEntityList): searchable_columns = ['id', 'name'] -class CourseDetail(MapEntityDetail): +class CourseDetail(CompletenessMixin, MapEntityDetail): queryset = Course.objects.prefetch_related('type').all() def get_context_data(self, *args, **kwargs): diff --git a/geotrek/settings/base.py b/geotrek/settings/base.py index 6f07bfc640..3367c4b2e6 100644 --- a/geotrek/settings/base.py +++ b/geotrek/settings/base.py @@ -505,6 +505,11 @@ def api_bbox(bbox, buffer): 'course': (10.7, 5.35), # Keep ratio of THUMBNAIL_ALIASES['print'] } +# Set 'error_on_publication' to avoid publication without completeness fields +# and 'error_on_review' if you want this fields to be required before sending to review. +COMPLETENESS_LEVEL = 'warning' + +# Set fields required or needed for review or publication, for each model COMPLETENESS_FIELDS = { 'trek': ['practice', 'departure', 'duration', 'difficulty', 'description_teaser'], 'dive': ['practice', 'difficulty', 'description_teaser'], diff --git a/geotrek/tourism/views.py b/geotrek/tourism/views.py index 68c689d9d9..8176d7e911 100644 --- a/geotrek/tourism/views.py +++ b/geotrek/tourism/views.py @@ -19,7 +19,7 @@ from geotrek.authent.decorators import same_structure_required from geotrek.common.mixins.api import APIViewSet -from geotrek.common.mixins.views import CustomColumnsMixin, MetaMixin +from geotrek.common.mixins.views import CompletenessMixin, CustomColumnsMixin, MetaMixin from geotrek.common.models import RecordSource, TargetPortal from geotrek.common.views import DocumentPublic, DocumentBookletPublic, MarkupPublic from geotrek.common.viewsets import GeotrekMapentityViewSet @@ -66,7 +66,7 @@ class TouristicContentFormatList(MapEntityFormat, TouristicContentList): ] -class TouristicContentDetail(MapEntityDetail): +class TouristicContentDetail(CompletenessMixin, MapEntityDetail): queryset = TouristicContent.objects.existing() def get_context_data(self, *args, **kwargs): @@ -215,7 +215,7 @@ class TouristicEventFormatList(MapEntityFormat, TouristicEventList): ] -class TouristicEventDetail(MapEntityDetail): +class TouristicEventDetail(CompletenessMixin, MapEntityDetail): queryset = TouristicEvent.objects.existing() def get_context_data(self, *args, **kwargs): diff --git a/geotrek/trekking/forms.py b/geotrek/trekking/forms.py index 5e93d4be80..96b6c32c6c 100644 --- a/geotrek/trekking/forms.py +++ b/geotrek/trekking/forms.py @@ -214,6 +214,7 @@ def __init__(self, *args, **kwargs): # init hidden field with children order self.fields['hidden_ordered_children'].initial = ",".join(str(x) for x in queryset_children.values_list('child__id', flat=True)) + for scale in RatingScale.objects.all(): ratings = None if self.instance.pk: @@ -227,6 +228,7 @@ def __init__(self, *args, **kwargs): ) right_after_type_index = self.fieldslayout[0][1][0].fields.index('practice') + 1 self.fieldslayout[0][1][0].insert(right_after_type_index, fieldname) + if self.instance.pk: self.fields['pois_excluded'].queryset = self.instance.all_pois.all() else: diff --git a/geotrek/trekking/tests/test_trek_forms.py b/geotrek/trekking/tests/test_trek_forms.py index e731472634..5d89f838da 100644 --- a/geotrek/trekking/tests/test_trek_forms.py +++ b/geotrek/trekking/tests/test_trek_forms.py @@ -1,7 +1,10 @@ import json from django.conf import settings +from django.contrib.auth.models import Permission from django.core.exceptions import ValidationError +from django.core.management import call_command from django.test import TestCase +from django.test.utils import override_settings from geotrek.authent.tests.factories import UserFactory from geotrek.core.tests.factories import PathFactory @@ -10,7 +13,7 @@ from ..forms import TrekForm -class TrekFormTest(TestCase): +class TrekRatingFormTest(TestCase): @classmethod def setUpTestData(cls): cls.user = UserFactory.create() @@ -72,6 +75,119 @@ def test_ratings_clean(self): form.clean() +@override_settings(COMPLETENESS_FIELDS={'trek': ['practice', 'departure', 'duration', 'description_teaser']}) +class TrekCompletenessTest(TestCase): + """Test completeness fields on error if empty, according to COMPLETENESS_LEVEL setting""" + + @classmethod + def setUpTestData(cls): + call_command('update_geotrek_permissions', verbosity=0) + cls.user = UserFactory.create() + cls.user.user_permissions.add(Permission.objects.get(codename='publish_trek')) + path = PathFactory.create() + cls.data = { + 'name_en': 'My trek', + 'name_fr': 'Ma rando', + } + + if settings.TREKKING_TOPOLOGY_ENABLED: + cls.data['topology'] = json.dumps({"paths": [path.pk]}) + else: + cls.data['geom'] = 'SRID=4326;LINESTRING (0.0 0.0, 1.0 1.0)' + + def test_completeness_warning(self): + """Test form is valid if completeness level is only warning""" + data = self.data + data['published_en'] = True + + form = TrekForm(user=self.user, data=data) + self.assertTrue(form.is_valid()) + + @override_settings(COMPLETENESS_LEVEL='error_on_publication') + def test_completeness_error_on_publish_not_published(self): + """Test form is valid if completeness level is error on publication but published in no language""" + data = self.data + data['published_en'] = False + + form = TrekForm(user=self.user, data=data) + self.assertTrue(form.is_valid()) + + @override_settings(COMPLETENESS_LEVEL='error_on_publication') + def test_completeness_error_on_publish_en(self): + """Test completeness fields on error if empty""" + data = self.data + data['published_en'] = True + + form = TrekForm(user=self.user, data=data) + self.assertFalse(form.is_valid()) + with self.assertRaisesRegex( + ValidationError, + 'Fields are missing to publish or review object: ' + 'practice, departure_en, duration, description_teaser_en'): + form.clean() + + @override_settings(COMPLETENESS_LEVEL='error_on_publication') + def test_completeness_error_on_publish_fr(self): + """Test completeness fields on error if empty""" + data = self.data + data['published_en'] = False + data['published_fr'] = True + + form = TrekForm(user=self.user, data=data) + self.assertFalse(form.is_valid()) + with self.assertRaisesRegex( + ValidationError, + 'Fields are missing to publish or review object: ' + 'practice, departure_fr, duration, description_teaser_fr'): + form.clean() + + @override_settings(PUBLISHED_BY_LANG=False) + @override_settings(COMPLETENESS_LEVEL='error_on_publication') + def test_completeness_error_on_publish_nolang(self): + """Test completeness fields on error if empty (when PUBLISHED_BY_LANG=False)""" + data = self.data + data['published_en'] = True + data['published_fr'] = False + + form = TrekForm(user=self.user, data=data) + self.assertFalse(form.is_valid()) + with self.assertRaisesRegex( + ValidationError, + 'Fields are missing to publish or review object: ' + 'practice, departure_en, departure_es, departure_fr, departure_it, duration, ' + 'description_teaser_en, description_teaser_es, ' + 'description_teaser_fr, description_teaser_it'): + form.clean() + + @override_settings(COMPLETENESS_LEVEL='error_on_review') + def test_completeness_error_on_review(self): + """Test completeness fields on error if empty and is review, with 'error_on_review'""" + data = self.data + data['published_en'] = False + data['review'] = True + form = TrekForm(user=self.user, data=data) + + self.assertFalse(form.is_valid()) + with self.assertRaisesRegex( + ValidationError, + 'Fields are missing to publish or review object: ' + 'practice, departure_en, duration, description_teaser_en'): + form.clean() + + # Exception should raise also if object is to be published + data['published_en'] = False + data['published_fr'] = True + data['review'] = False + form = TrekForm(user=self.user, data=data) + + self.assertFalse(form.is_valid()) + with self.assertRaisesRegex( + ValidationError, + 'Fields are missing to publish or review object: ' + 'practice, departure_fr, duration, description_teaser_fr'): + form.clean() + + class TrekItinerancyTestCase(TestCase): @classmethod def setUpTestData(cls): diff --git a/geotrek/trekking/views.py b/geotrek/trekking/views.py index bdce9ac904..c0ecebb5c0 100755 --- a/geotrek/trekking/views.py +++ b/geotrek/trekking/views.py @@ -20,7 +20,7 @@ from geotrek.common.functions import Length from geotrek.common.mixins.api import APIViewSet from geotrek.common.mixins.forms import FormsetMixin -from geotrek.common.mixins.views import CustomColumnsMixin, MetaMixin +from geotrek.common.mixins.views import CompletenessMixin, CustomColumnsMixin, MetaMixin from geotrek.common.models import Attachment, RecordSource, TargetPortal, Label from geotrek.common.permissions import PublicOrReadPermMixin from geotrek.common.views import DocumentPublic, DocumentBookletPublic, MarkupPublic @@ -32,6 +32,7 @@ from geotrek.signage.models import Signage from geotrek.signage.serializers import SignageAPIGeojsonSerializer from geotrek.zoning.models import District, City, RestrictedArea + from .filters import TrekFilterSet, POIFilterSet, ServiceFilterSet from .forms import TrekForm, TrekRelationshipFormSet, POIForm, WebLinkCreateFormPopup, ServiceForm from .models import Trek, POI, WebLink, Service, TrekRelationship, OrderedTrekChild @@ -104,7 +105,7 @@ def render_to_response(self, context): return response -class TrekDetail(MapEntityDetail): +class TrekDetail(CompletenessMixin, MapEntityDetail): queryset = Trek.objects.existing() @property @@ -335,7 +336,7 @@ def get_queryset(self): yield poi -class POIDetail(MapEntityDetail): +class POIDetail(CompletenessMixin, MapEntityDetail): queryset = POI.objects.existing() def get_context_data(self, *args, **kwargs):