diff --git a/README.md b/README.md index fbbdf7ddc..176e2901a 100644 --- a/README.md +++ b/README.md @@ -263,12 +263,14 @@ docker-compose exec web pip install -r requirements.txt ### Permissions The user belonging to backoffice group can be granted specific permissions: + - `view_private_form`, "Voir les demandes restreintes": allows the user to make requests that are not visible by standard user. Typically during the setup stage of a new form configuration -- `amend_submission`,"Traiter les demandes": allow the user the process (amend) the requests (fill the backoffice fields), require validation for other departments and print the documents -- `validate_submission`,"Valider les demandes": allow the user to fill the validation form -- `classify_submission`,"Classer les demandes" allow the user to accept/reject the requests if validations services have all accepted it -- `edit_submission`, "Éditer les demandes": allow the user to edit de requests filled by another person +- `amend_submission`, "Traiter les demandes": allow the user the process (amend) the requests (fill the backoffice fields), require validation for other departments and print the documents +- `validate_submission`, "Valider les demandes": allow the user to fill the validation form +- `classify_submission`, "Classer les demandes" allow the user to accept/reject the requests if validations services have all accepted it +- `edit_submission`, "Modifier les demandes": allow the user to edit the requests filled by another person - `read_submission`, "Consulter les demandes": allow a trusted user to only read submissions without any other permission +- `edit_submission_validations`, "Modifier les validations": allow the pilot to edit the validation to correct spelling mistakes ### Two factor authentication diff --git a/geocity/apps/accounts/permissions_groups.py b/geocity/apps/accounts/permissions_groups.py index 81477d859..735f29dec 100644 --- a/geocity/apps/accounts/permissions_groups.py +++ b/geocity/apps/accounts/permissions_groups.py @@ -53,6 +53,7 @@ AVAILABLE_FOR_INTEGRATOR_PERMISSION_CODENAMES = [ "read_submission", "amend_submission", + "edit_submission_validations", "validate_submission", "classify_submission", "edit_submission", diff --git a/geocity/apps/api/serializers.py b/geocity/apps/api/serializers.py index 70509f8e0..dbd830e00 100644 --- a/geocity/apps/api/serializers.py +++ b/geocity/apps/api/serializers.py @@ -422,11 +422,10 @@ def to_representation(self, value): values = {} for field in validation._meta.fields: values["validation_status"] = validation.get_validation_status_display() - if field.name in [ - "comment_before", - "comment_during", - "comment_after", - ]: + if ( + field.name == "comment" + or field.name == "comment_is_visible_by_author" + ): values[field.name] = getattr(validation, field.name) rep[validation.department.description] = values diff --git a/geocity/apps/core/static/js/submission_validations_edit.js b/geocity/apps/core/static/js/submission_validations_edit.js new file mode 100644 index 000000000..0d8cbcd0f --- /dev/null +++ b/geocity/apps/core/static/js/submission_validations_edit.js @@ -0,0 +1,13 @@ +// Create a label to replace "readonly hidden select" readonly +window.addEventListener('load', function () { + var selects = document.querySelectorAll("select[readonly][hidden]"); + for (select of selects) { + let elem = document.createElement('label'); + let text = select.querySelector("option[selected]").text + let div = select.closest('.col-md-9'); + + elem.innerHTML = text; + elem.classList.add('col-form-label', 'bold'); + div.appendChild(elem); + } +}); diff --git a/geocity/apps/reports/migrations/0023_sectionhorizontalrule_padding_top.py b/geocity/apps/reports/migrations/0023_sectionhorizontalrule_padding_top.py new file mode 100644 index 000000000..0ac72f194 --- /dev/null +++ b/geocity/apps/reports/migrations/0023_sectionhorizontalrule_padding_top.py @@ -0,0 +1,22 @@ +# Generated by Django 4.1.7 on 2023-03-23 12:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("reports", "0022_alter_sectionparagraph_content"), + ] + + operations = [ + migrations.AddField( + model_name="sectionhorizontalrule", + name="padding_top", + field=models.PositiveIntegerField( + default=0, + help_text="Espace vide au dessus afin de placer le texte au bon endroit (en pixels). Augmenter la valeur fait descendre le texte", + verbose_name="Espace vide au dessus", + ), + ), + ] diff --git a/geocity/apps/reports/models.py b/geocity/apps/reports/models.py index af6765446..d1c08221b 100644 --- a/geocity/apps/reports/models.py +++ b/geocity/apps/reports/models.py @@ -541,6 +541,8 @@ class Meta: class SectionHorizontalRule(Section): + padding_top = padding_top_field() + class Meta: verbose_name = _("Ligne horizontale") diff --git a/geocity/apps/reports/templates/reports/sections/sectionvalidation.html b/geocity/apps/reports/templates/reports/sections/sectionvalidation.html index b6b44a566..ff8db6128 100644 --- a/geocity/apps/reports/templates/reports/sections/sectionvalidation.html +++ b/geocity/apps/reports/templates/reports/sections/sectionvalidation.html @@ -9,11 +9,11 @@
{% for service, validation in request_data.properties.validations.items %} -

Validation: {{service}}

- Statut : {{validation.validation_status}}
- Commentaire (avant) : {{validation.comment_before}}
- Commentaire (pendant) : {{validation.comment_during}}
- Commentaire (après) : {{validation.comment_after}}
+

Validation: {{service}}

+ Statut : {{validation.validation_status}}
+ {% if validation.comment_is_visible_by_author %} + Commentaire : {{validation.comment}}
+ {% endif %} {% endfor %}
diff --git a/geocity/apps/submissions/forms.py b/geocity/apps/submissions/forms.py index c13425254..d8bb81ba5 100644 --- a/geocity/apps/submissions/forms.py +++ b/geocity/apps/submissions/forms.py @@ -1406,15 +1406,13 @@ class Meta: model = models.SubmissionValidation fields = [ "validation_status", - "comment_before", - "comment_during", - "comment_after", + "comment", + "comment_is_visible_by_author", ] widgets = { "validation_status": forms.RadioSelect(), - "comment_before": forms.Textarea(attrs={"rows": 3}), - "comment_during": forms.Textarea(attrs={"rows": 3}), - "comment_after": forms.Textarea(attrs={"rows": 3}), + "comment": forms.Textarea(attrs={"rows": 3}), + "comment_is_visible_by_author": forms.CheckboxInput(), } def __init__(self, *args, **kwargs): @@ -1896,3 +1894,15 @@ def get_submission_forms(submission): ] return forms_infos + + +class SubmissionValidationsForm(forms.ModelForm): + class Meta: + model = models.SubmissionValidation + fields = ["department", "comment", "comment_is_visible_by_author"] + + def __init__(self, *args, **kwargs): + super(SubmissionValidationsForm, self).__init__(*args, **kwargs) + if self.instance.id: + self.fields["department"].widget.attrs["readonly"] = True + self.fields["department"].widget.attrs["hidden"] = True diff --git a/geocity/apps/submissions/management/commands/fixturize.py b/geocity/apps/submissions/management/commands/fixturize.py index ee406a46a..f0c7f5c0b 100644 --- a/geocity/apps/submissions/management/commands/fixturize.py +++ b/geocity/apps/submissions/management/commands/fixturize.py @@ -447,11 +447,9 @@ def setup_submission(self, entity, user_iterations, administrative_entity, text) another_department = PermitDepartment.objects.get( group__name=f"{entity}-validator" ) - comment_before_default = ( - "Ce projet n'est pas admissible, veuillez l'améliorer.", - ) - comment_during_default = ("Les améliorations ont été prise en compte.",) - comment_after_default = ("Excellent projet qui bénéficiera à la communauté.",) + comment = """Avant : Ce projet n'est pas admissible, veuillez l'améliorer. +Pendant : Les améliorations ont été prise en compte. +Après : Excellent projet qui bénéficiera à la communauté.""" for user_iteration in range(user_iterations): username = f"{entity}-user-{user_iteration}" @@ -473,9 +471,7 @@ def setup_submission(self, entity, user_iterations, administrative_entity, text) submission, department, validation_status, - comment_before_default, - comment_during_default, - comment_after_default, + comment, ) # Submission to Classify with mixed objects requiring and not requiring validation document @@ -490,9 +486,7 @@ def setup_submission(self, entity, user_iterations, administrative_entity, text) submission, department, validation_status, - comment_before_default, - comment_during_default, - comment_after_default, + comment, ) # Submission to Classify with validation document required @@ -506,9 +500,7 @@ def setup_submission(self, entity, user_iterations, administrative_entity, text) submission, department, validation_status, - comment_before_default, - comment_during_default, - comment_after_default, + comment, ) # Submission to Classify with validation document required (with another Form) @@ -522,9 +514,7 @@ def setup_submission(self, entity, user_iterations, administrative_entity, text) submission, department, validation_status, - comment_before_default, - comment_during_default, - comment_after_default, + comment, ) # Submission with pending validations @@ -550,16 +540,12 @@ def setup_submission(self, entity, user_iterations, administrative_entity, text) department, validation_status, text, - text, - text, ) self.create_submission_validation( submission, another_department, validation_status, text, - text, - text, ) # Amend properties @@ -831,6 +817,7 @@ def get_pilot_permissions(self): secretariat_permissions = Permission.objects.filter( codename__in=[ "amend_submission", + "edit_submission_validations", "classify_submission", ], content_type=self.submission_ct, @@ -1018,17 +1005,13 @@ def create_submission_validation( submission, department, validation_status=SubmissionValidation.STATUS_REQUESTED, - comment_before="", - comment_during="", - comment_after="", + comment="", ): SubmissionValidation.objects.get_or_create( submission=submission, department=department, validation_status=validation_status, - comment_before=comment_before, - comment_during=comment_during, - comment_after=comment_after, + comment=comment, ) def create_submission_amend_field( diff --git a/geocity/apps/submissions/migrations/0016_historicalsubmissionvalidation_comment_and_more.py b/geocity/apps/submissions/migrations/0016_historicalsubmissionvalidation_comment_and_more.py new file mode 100644 index 000000000..be5971d60 --- /dev/null +++ b/geocity/apps/submissions/migrations/0016_historicalsubmissionvalidation_comment_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 4.1.7 on 2023-03-22 10:49 + +from django.db import migrations, models + + +def migrate_comments(apps, schema_editor): + """ + Put comment_before, comment_during and comment_after in 1 comment + """ + SubmissionValidation = apps.get_model("submissions", "SubmissionValidation") + + validations = SubmissionValidation.objects.all().exclude( + comment_before__exact="", comment_during__exact="", comment_after__exact="" + ) + + # For each SubmissionValidation, squash comment_before comment_during comment_after in 1 comment + for validation in validations: + comment = f"""{validation.comment_before} + {validation.comment_during} + {validation.comment_after}""" + + # save it in SubmissionValidation.comment + validation.comment = comment + validation.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("submissions", "0015_alter_submission_options"), + ] + + operations = [ + migrations.AddField( + model_name="historicalsubmissionvalidation", + name="comment", + field=models.TextField( + blank=True, + help_text="Information supplémentaire facultative transmise au requérant", + verbose_name="Commentaire", + ), + ), + migrations.AddField( + model_name="submissionvalidation", + name="comment", + field=models.TextField( + blank=True, + help_text="Information supplémentaire facultative transmise au requérant", + verbose_name="Commentaire", + ), + ), + migrations.RunPython(migrate_comments), + ] diff --git a/geocity/apps/submissions/migrations/0017_alter_submission_options_and_more.py b/geocity/apps/submissions/migrations/0017_alter_submission_options_and_more.py new file mode 100644 index 000000000..7e75b359f --- /dev/null +++ b/geocity/apps/submissions/migrations/0017_alter_submission_options_and_more.py @@ -0,0 +1,131 @@ +# Generated by Django 4.1.7 on 2023-04-03 11:24 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("accounts", "0011_add_signature_sheet_to_administrative_entity"), + ("submissions", "0016_historicalsubmissionvalidation_comment_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="submission", + options={ + "permissions": [ + ("read_submission", "Consulter les demandes"), + ("amend_submission", "Traiter les demandes"), + ("edit_submission_validations", "Modifier les validations"), + ("validate_submission", "Valider les demandes"), + ("classify_submission", "Classer les demandes"), + ("edit_submission", "Modifier les demandes"), + ("view_private_form", "Voir les demandes restreintes"), + ("can_refund_transactions", "Rembourser une transaction"), + ("can_revert_refund_transactions", "Revenir sur un remboursement"), + ], + "verbose_name": "2.2 Consultation de la demande", + "verbose_name_plural": "2.2 Consultation des demandes", + }, + ), + migrations.RemoveField( + model_name="historicalsubmissionvalidation", + name="comment_after", + ), + migrations.RemoveField( + model_name="historicalsubmissionvalidation", + name="comment_before", + ), + migrations.RemoveField( + model_name="historicalsubmissionvalidation", + name="comment_during", + ), + migrations.RemoveField( + model_name="submissionvalidation", + name="comment_after", + ), + migrations.RemoveField( + model_name="submissionvalidation", + name="comment_before", + ), + migrations.RemoveField( + model_name="submissionvalidation", + name="comment_during", + ), + migrations.AddField( + model_name="historicalsubmissionvalidation", + name="comment_is_visible_by_author", + field=models.BooleanField( + default=True, + verbose_name="Commentaire visible par l'auteur de la demande", + ), + ), + migrations.AddField( + model_name="submissionvalidation", + name="comment_is_visible_by_author", + field=models.BooleanField( + default=True, + verbose_name="Commentaire visible par l'auteur de la demande", + ), + ), + migrations.AlterField( + model_name="historicalsubmissionvalidation", + name="comment", + field=models.TextField(blank=True, verbose_name="Commentaire"), + ), + migrations.AlterField( + model_name="historicalsubmissionvalidation", + name="department", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="accounts.permitdepartment", + verbose_name="Département", + ), + ), + migrations.AlterField( + model_name="historicalsubmissionvalidation", + name="validated_by", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + verbose_name="Validé par", + ), + ), + migrations.AlterField( + model_name="submissionvalidation", + name="comment", + field=models.TextField(blank=True, verbose_name="Commentaire"), + ), + migrations.AlterField( + model_name="submissionvalidation", + name="department", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="submission_validations", + to="accounts.permitdepartment", + verbose_name="Département", + ), + ), + migrations.AlterField( + model_name="submissionvalidation", + name="validated_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + verbose_name="Validé par", + ), + ), + ] diff --git a/geocity/apps/submissions/models.py b/geocity/apps/submissions/models.py index e483097ca..e86eb91ef 100644 --- a/geocity/apps/submissions/models.py +++ b/geocity/apps/submissions/models.py @@ -81,14 +81,15 @@ def filter_for_user(self, user, form_filter=None, ignore_archived=True): starts_at_min=Min("geo_time__starts_at"), ends_at_max=Max("geo_time__ends_at"), permit_duration_max=Max("forms__permit_duration"), - remaining_validations=Count("validations") + remaining_validations=Count("validations__department", distinct=True) - Count( - "validations", + "validations__department", + distinct=True, filter=~Q( validations__validation_status=SubmissionValidation.STATUS_REQUESTED ), ), - required_validations=Count("validations"), + required_validations=Count("validations__department", distinct=True), author_fullname=Concat( F("author__first_name"), Value(" "), @@ -271,9 +272,10 @@ class Meta: permissions = [ ("read_submission", _("Consulter les demandes")), ("amend_submission", _("Traiter les demandes")), + ("edit_submission_validations", _("Modifier les validations")), ("validate_submission", _("Valider les demandes")), ("classify_submission", _("Classer les demandes")), - ("edit_submission", _("Éditer les demandes")), + ("edit_submission", _("Modifier les demandes")), ("view_private_form", _("Voir les demandes restreintes")), ("can_refund_transactions", _("Rembourser une transaction")), ("can_revert_refund_transactions", _("Revenir sur un remboursement")), @@ -1782,28 +1784,23 @@ class SubmissionValidation(models.Model): ) department = models.ForeignKey( PermitDepartment, + verbose_name=_("Département"), on_delete=models.CASCADE, related_name="submission_validations", ) validation_status = models.IntegerField( _("Statut de validation"), choices=STATUS_CHOICES, default=STATUS_REQUESTED ) - comment_before = models.TextField( - _("Commentaire (avant)"), + comment = models.TextField( + _("Commentaire"), blank=True, - help_text=_("Information supplémentaire facultative transmise au requérant"), ) - comment_during = models.TextField( - _("Commentaire (pendant)"), - blank=True, - help_text=_("Information supplémentaire facultative transmise au requérant"), + comment_is_visible_by_author = models.BooleanField( + _("Commentaire visible par l'auteur de la demande"), default=True ) - comment_after = models.TextField( - _("Commentaire (après)"), - blank=True, - help_text=_("Information supplémentaire facultative transmise au requérant"), + validated_by = models.ForeignKey( + User, verbose_name=_("Validé par"), null=True, on_delete=models.SET_NULL ) - validated_by = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) validated_at = models.DateTimeField(_("Validé le"), null=True) history = HistoricalRecords() diff --git a/geocity/apps/submissions/permissions.py b/geocity/apps/submissions/permissions.py index 2c8f8d77b..757a96d57 100644 --- a/geocity/apps/submissions/permissions.py +++ b/geocity/apps/submissions/permissions.py @@ -16,6 +16,14 @@ def has_permission_to_amend_submission(user, submission): ) +def has_permission_to_edit_submission_validations(user, submission): + return user.has_perm( + "submissions.edit_submission_validations" + ) and submission.administrative_entity in AdministrativeEntity.objects.associated_to_user( + user + ) + + def can_amend_submission(user, submission): return submission.can_be_amended() and has_permission_to_amend_submission( user, submission diff --git a/geocity/apps/submissions/templates/submissions/_submission_actions.html b/geocity/apps/submissions/templates/submissions/_submission_actions.html index eeaa872ba..08553ba5a 100644 --- a/geocity/apps/submissions/templates/submissions/_submission_actions.html +++ b/geocity/apps/submissions/templates/submissions/_submission_actions.html @@ -91,7 +91,6 @@

{% trans "" %}

{% trans "Informations générales" %}

{% csrf_token %} - {% for field in forms.amend.get_base_fields %} {% bootstrap_field field layout='horizontal'%} {% endfor %} diff --git a/geocity/apps/submissions/templates/submissions/submission_detail.html b/geocity/apps/submissions/templates/submissions/submission_detail.html index 2018c2ad3..c980a71f0 100644 --- a/geocity/apps/submissions/templates/submissions/submission_detail.html +++ b/geocity/apps/submissions/templates/submissions/submission_detail.html @@ -151,7 +151,7 @@

{% trans "Historique" %}

{% if validations %} -
+

{% trans "Informations générales" %}

@@ -168,9 +168,23 @@

{% trans "Informations générales" %}

{% endif %} {% if validations %} -
+
-

{% trans "Validation" %}

+
+
+
+

{% trans "Validation" %}

+
+
+ {% if has_permission_to_edit_submission_validations %} + + {% endif %} +
+
+
+ {% for validation in validations %}
@@ -186,24 +200,31 @@

{% trans "Validation" %}

{% if has_permission_to_classify or can_validate_submission%} {{ validation.get_validation_status_display }} par {% if validation.validated_by %} - {{ validation.validated_by.userprofile.first_name }} - {{ validation.validated_by.userprofile.last_name }} + {{ validation.validated_by.first_name }} + {{ validation.validated_by.last_name }} le {{ validation.validated_at }} {% endif %} {% endif %} - {% if validation.comment_before %} -
{% trans "Commentaire (avant)" %}:
{{ validation.comment_before|linebreaksbr }}
- {% endif %} - {% if validation.comment_during %} -
{% trans "Commentaire (pendant)" %}:
{{ validation.comment_during|linebreaksbr }}
- {% endif %} - {% if validation.comment_after %} -
{% trans "Commentaire (après)" %}:
{{ validation.comment_after|linebreaksbr }}
+ {% if validation.comment %} + {% if validation.comment_is_visible_by_author or current_user != submission.author %} +
{% trans "Commentaire" %}:
{{ validation.comment|linebreaksbr }}
+ {% if current_user != submission.author %} +
{% trans "Commentaire visible par l'auteur de la demande" %}:
+
+ {% if validation.comment_is_visible_by_author %} + + {% else %} + + {% endif %} +
+ {% endif %} + {% endif %} {% endif %}
+ {% if not forloop.last %}
{% endif %} {% endfor %}
@@ -212,7 +233,7 @@

{% trans "Validation" %}

{% if submission.can_be_prolonged and submission.prolongation_date %} -
+

{% trans "Prolongation" %}

@@ -232,7 +253,7 @@

{% trans "Prolongation" %}

{% endif %} -
+

{% trans "Résumé" %}

{% submission_summary submission %} @@ -240,7 +261,7 @@

{% trans "Résumé" %}

{% if submission.additional_decision_information %} -
+

{% trans "Informations complémentaires" %}

diff --git a/geocity/apps/submissions/templates/submissions/submission_validations_edit.html b/geocity/apps/submissions/templates/submissions/submission_validations_edit.html new file mode 100644 index 000000000..055d7f332 --- /dev/null +++ b/geocity/apps/submissions/templates/submissions/submission_validations_edit.html @@ -0,0 +1,27 @@ +{% extends "base_generic.html" %} +{% load static %} +{% load i18n %} +{% load bootstrap4 %} + +{% block content %} +

{{ title }}

+ + + {% csrf_token %} + {{ formset.management_form }} + + {% for form in formset %} + {{ form.media }} + {% bootstrap_form form layout='horizontal' %} + {% endfor %} +
+ + + +
+ +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/geocity/apps/submissions/urls.py b/geocity/apps/submissions/urls.py index 119a1dc14..808aa6db0 100644 --- a/geocity/apps/submissions/urls.py +++ b/geocity/apps/submissions/urls.py @@ -131,4 +131,9 @@ views.SubmissionPaymentRedirect.as_view(), name="submission_payment_redirect", ), + path( + "validations//edit", + views.submission_validations_edit, + name="submission_validations_edit", + ), ] diff --git a/geocity/apps/submissions/views.py b/geocity/apps/submissions/views.py index 036dd8462..4a4a2f8ae 100644 --- a/geocity/apps/submissions/views.py +++ b/geocity/apps/submissions/views.py @@ -282,6 +282,9 @@ def get_context_data(self, **kwargs): self.request.user, self.submission ), "can_validate_submission": can_validate_submission, + "has_permission_to_edit_submission_validations": permissions.has_permission_to_edit_submission_validations( + self.request.user, self.submission + ), "directives": self.submission.get_submission_directives(), "prolongation_enabled": prolongation_enabled, "document_enabled": self.submission.has_document_enabled(), @@ -293,6 +296,7 @@ def get_context_data(self, **kwargs): == self.submission.forms.count(), "inquiry_in_progress": self.submission.status == models.Submission.STATUS_INQUIRY_IN_PROGRESS, + "current_user": self.request.user, }, } @@ -2210,3 +2214,57 @@ def get(self, request, pk, *args, **kwargs): return redirect(payment_url) return redirect(reverse_lazy("submissions:submissions_list")) + + +# TODO: SET PERMISSIONS +@login_required +@check_mandatory_2FA +def submission_validations_edit(request, submission_id): + + # Check that user is authorize to see submission + submission = get_object_or_404( + models.Submission.objects.filter_for_user(request.user), pk=submission_id + ) + + # Check that user is authorized to edit submission validations + if not permissions.has_permission_to_edit_submission_validations( + request.user, submission + ): + # TODO: be nicer whith user + raise PermissionDenied + + submissionValidationFormset = modelformset_factory( + models.SubmissionValidation, + form=forms.SubmissionValidationsForm, + edit_only=True, + extra=0, + ) + if request.method == "POST": + if request.method == "POST": + formset = submissionValidationFormset(request.POST) + if formset.is_valid(): + formset.save() + if "save_continue" in request.POST: + return redirect( + "submissions:submission_validations_edit", + submission_id=submission_id, + ) + else: + return redirect( + "submissions:submission_detail", + submission_id=submission_id, + ) + + else: + formset = submissionValidationFormset( + queryset=models.SubmissionValidation.objects.filter( + submission=submission_id + ) + ) + return render( + request, + "submissions/submission_validations_edit.html", + { + "formset": formset, + }, + ) diff --git a/geocity/tests/submissions/functional/test_submission_validation.py b/geocity/tests/submissions/functional/test_submission_validation.py index 1199b871f..760815da8 100644 --- a/geocity/tests/submissions/functional/test_submission_validation.py +++ b/geocity/tests/submissions/functional/test_submission_validation.py @@ -1,13 +1,15 @@ +from django.contrib.auth.models import Permission from django.core import mail from django.test import TestCase from django.urls import reverse from geocity.apps.submissions import models as submissions_models +from geocity.apps.submissions.models import Submission, SubmissionValidation from geocity.tests import factories from geocity.tests.utils import LoggedInSecretariatMixin, get_parser -class SubmissionValidationRequestTestcase(LoggedInSecretariatMixin, TestCase): +class SubmissionValidationRequestTestCase(LoggedInSecretariatMixin, TestCase): def test_secretariat_can_request_validation(self): validator_groups = factories.ValidatorGroupFactory.create_batch( 2, department__administrative_entity=self.administrative_entity @@ -131,7 +133,7 @@ def test_validation_request_sends_mail_to_selected_validators(self): self.assertEqual(mail.outbox[0].to, [validator_user.email]) -class SubmissionValidationTestcase(TestCase): +class SubmissionValidationTestCase(TestCase): def test_validator_can_see_assigned_submissions(self): validation = factories.SubmissionValidationFactory() validator = factories.ValidatorUserFactory( @@ -321,3 +323,184 @@ def test_secretary_email_is_sent_when_submission_is_validated(self): "Les services chargés de la validation d'une demande ont donné leur préavis", mail.outbox[0].message().as_string(), ) + + +class SubmissionValidationOnDetailsTestCase(TestCase): + def setUp(self): + super().setUp() + """Generate validations and users + """ + + self.edit_submission_validations = Permission.objects.get( + codename="edit_submission_validations", + content_type__app_label="submissions", + ) + + # Create submission + self.submission = factories.SubmissionFactory( + status=Submission.STATUS_PROCESSING + ) + + # Create all cases of validations + self.validation_1 = factories.SubmissionValidationFactory( + comment_is_visible_by_author=True, + submission=self.submission, + comment="Requested and visible", + validation_status=SubmissionValidation.STATUS_REQUESTED, + ) + self.validation_2 = factories.SubmissionValidationFactory( + comment_is_visible_by_author=False, + submission=self.submission, + comment="Approved and hidden", + validation_status=SubmissionValidation.STATUS_APPROVED, + ) + self.validation_3 = factories.SubmissionValidationFactory( + comment_is_visible_by_author=True, + submission=self.submission, + comment="Approved and visible", + validation_status=SubmissionValidation.STATUS_APPROVED, + ) + self.validation_4 = factories.SubmissionValidationFactory( + comment_is_visible_by_author=True, + submission=self.submission, + comment="Rejected and visible", + validation_status=SubmissionValidation.STATUS_REJECTED, + ) + + # Create pilot group + self.pilot_group = factories.GroupFactory(name="pilot") + + # Create department + department = factories.PermitDepartmentFactory( + group=self.pilot_group, is_backoffice=True + ) + + # Create pilot user + self.pilot = factories.SecretariatUserFactory( + groups=[self.pilot_group], email="secretary@geocity.ch" + ) + + # Create validator + self.validator = factories.ValidatorUserFactory( + groups=[ + self.validation_1.department.group, + factories.ValidatorGroupFactory(), + ], + ) + + # Define author + self.author = self.submission.author + + # Set department for submission.administrative_entity + self.submission.administrative_entity.departments.set([department]) + + # Create used form + self.form = factories.FormFactory() + + # Assign form to submission + self.submission.forms.set([self.form]) + + def test_author_can_only_see_visible_validation_comment(self): + self.client.login(username=self.author.username, password="password") + + response = self.client.get( + reverse( + "submissions:submission_detail", + kwargs={"submission_id": self.submission.pk}, + ), + ) + + self.assertEqual(response.status_code, 200) + + content = response.content.decode() + + # Button should not appear + self.assertNotIn("Modifier les validations", content) + + # Check comment and status for comment_is_visible_by_author=True appears + # Check comment and status for comment_is_visible_by_author=False dont appears + self.assertIn(self.validation_1.comment, content) + self.assertIn(self.validation_3.comment, content) + self.assertIn(self.validation_4.comment, content) + self.assertNotIn(self.validation_2.comment, content) + + self.assertIn("fa fa-check-circle", content) + self.assertIn("fa fa-clock-o", content) + self.assertIn("fa fa-times-circle", content) + + # Check comment_is_visible_by_author never appears + self.assertNotIn("Commentaire visible par l'auteur de la demande", content) + + def test_pilot_can_edit_validation_only_with_perms(self): + self.client.login(username=self.pilot.username, password="password") + + response = self.client.get( + reverse( + "submissions:submission_detail", + kwargs={"submission_id": self.submission.pk}, + ), + ) + + self.assertEqual(response.status_code, 200) + + content = response.content.decode() + + # Button should not appear + self.assertNotIn("Modifier les validations", content) + + # Can see all comments + self.assertIn(self.validation_1.comment, content) + self.assertIn(self.validation_2.comment, content) + self.assertIn(self.validation_3.comment, content) + self.assertIn(self.validation_4.comment, content) + + # See all icons + self.assertIn("fa fa-check-circle", content) + self.assertIn("fa fa-clock-o", content) + self.assertIn("fa fa-times-circle", content) + + # Add permission + self.pilot_group.permissions.add(self.edit_submission_validations) + + # Check permission is assigned correctly + self.assertTrue(self.pilot.has_perm("submissions.edit_submission_validations")) + + # Refresh response + response = self.client.get( + reverse( + "submissions:submission_detail", + kwargs={"submission_id": self.submission.pk}, + ), + ) + + self.assertEqual(response.status_code, 200) + + content = response.content.decode() + + # Button should appear + self.assertIn("Modifier les validations", content) + + def test_validator_cant_edit_validation_even_with_perms(self): + # Add permission + self.validator.user_permissions.add(self.edit_submission_validations) + + # Check permission is assigned correctly + self.assertTrue( + self.validator.has_perm("submissions.edit_submission_validations") + ) + + self.client.login(username=self.pilot.username, password="password") + + response = self.client.get( + reverse( + "submissions:submission_detail", + kwargs={"submission_id": self.submission.pk}, + ), + ) + + self.assertEqual(response.status_code, 200) + + content = response.content.decode() + + # Button should not appear + self.assertNotIn("Modifier les validations", content)