Skip to content

Commit

Permalink
Merge pull request #3021 from GeotrekCE/2898_completeness_fields_form…
Browse files Browse the repository at this point in the history
…s_error

 Display completeness fields if object is not complete and raise an error if needed
  • Loading branch information
numahell authored Jun 7, 2022
2 parents 65cb892 + 87f961c commit d7ff666
Show file tree
Hide file tree
Showing 20 changed files with 364 additions and 45 deletions.
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down
20 changes: 20 additions & 0 deletions docs/install/advanced-configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
================
Expand Down
127 changes: 105 additions & 22 deletions geotrek/common/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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):
Expand All @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions geotrek/common/locale/de/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand Down Expand Up @@ -501,6 +508,9 @@ msgstr ""
msgid "looks incomplete;"
msgstr ""

msgid "these fields should be completed:"
msgstr ""

msgid "has invalid geometry;"
msgstr ""

Expand Down
10 changes: 10 additions & 0 deletions geotrek/common/locale/en/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand Down Expand Up @@ -501,6 +508,9 @@ msgstr ""
msgid "looks incomplete;"
msgstr ""

msgid "these fields should be completed:"
msgstr ""

msgid "has invalid geometry;"
msgstr ""

Expand Down
10 changes: 10 additions & 0 deletions geotrek/common/locale/es/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand Down Expand Up @@ -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;"

Expand Down
14 changes: 13 additions & 1 deletion geotrek/common/locale/fr/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand Down Expand Up @@ -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é"
Expand Down Expand Up @@ -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 ;"

Expand Down
10 changes: 10 additions & 0 deletions geotrek/common/locale/it/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand Down Expand Up @@ -501,6 +508,9 @@ msgstr ""
msgid "looks incomplete;"
msgstr ""

msgid "these fields should be completed:"
msgstr ""

msgid "has invalid geometry;"
msgstr ""

Expand Down
12 changes: 11 additions & 1 deletion geotrek/common/locale/nl/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
Expand All @@ -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 "
Expand Down Expand Up @@ -501,6 +508,9 @@ msgstr ""
msgid "looks incomplete;"
msgstr ""

msgid "these fields should be completed:"
msgstr ""

msgid "has invalid geometry;"
msgstr ""

Expand Down
13 changes: 13 additions & 0 deletions geotrek/common/mixins/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit d7ff666

Please sign in to comment.