diff --git a/.env.example b/.env.example index 3c093d69e..855246ae4 100644 --- a/.env.example +++ b/.env.example @@ -104,3 +104,5 @@ USE_THUMBOR=false # For dockerized thumbor service not exposed over the Internet, attache Geocity to its network with this override on top of this file: # COMPOSE_FILE=docker-compose.yml:docker-compose.thumbor.yml THUMBOR_SERVICE_URL="http://nginx-proxy" +# https://docs.djangoproject.com/en/5.0/ref/settings/#csrf-trusted-origins +CSRF_TRUSTED_ORIGINS=https://yoursite.geocity diff --git a/Dockerfile b/Dockerfile index ce6a3a9b4..77c6b63d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,31 @@ -FROM sitdocker/geocity-base:v2.1.5 +FROM ghcr.io/osgeo/gdal:ubuntu-small-3.8.3 + +RUN apt-get -y update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --fix-missing \ + --no-install-recommends \ + build-essential \ + gettext \ + python3-pip \ + libcairo2-dev \ + poppler-utils \ + python3-dev \ + python3-setuptools \ + python3-wheel \ + python3-cffi \ + libcairo2 \ + libpango-1.0-0 \ + libpangocairo-1.0-0 \ + libgdk-pixbuf2.0-0 \ + libffi-dev \ + shared-mime-info \ + tzdata \ + && ln -fs /usr/share/zoneinfo/Europe/Zurich /etc/localtime \ + && dpkg-reconfigure -f noninteractive tzdata + +# Update C env vars so compiler can find gdal +ENV CPLUS_INCLUDE_PATH=/usr/include/gdal +ENV C_INCLUDE_PATH=/usr/include/gdal +ENV PYTHONUNBUFFERED 1 ARG ENV diff --git a/README.md b/README.md index b7aa28882..a052bcfbe 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Geocity - build your (geo)-forms easily! ![Geocity CI](https://github.com/yverdon/geocity/workflows/Geocity%20CI/badge.svg?branch=main) -**[What is Geocity ?](https://geocity.ch/about)** +**[What is Geocity ?](https://geocity-asso.ch)** **[Features and user guide](https://github.com/yverdon/geocity/wiki)** diff --git a/docker-compose.yml b/docker-compose.yml index 7c0cc4ddd..9c096651d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ volumes: services: web: # Name of this container should not be changed - image: gms_web + image: geocity restart: unless-stopped build: context: ./ @@ -79,6 +79,7 @@ services: SITE_DOMAIN: USE_THUMBOR: THUMBOR_SERVICE_URL: + CSRF_TRUSTED_ORIGINS: ports: - "${DJANGO_DOCKER_PORT}:9000" networks: diff --git a/geocity/apps/accounts/admin.py b/geocity/apps/accounts/admin.py index e466f849b..22300ba77 100644 --- a/geocity/apps/accounts/admin.py +++ b/geocity/apps/accounts/admin.py @@ -781,6 +781,7 @@ class AdministrativeEntityAdmin(IntegratorFilterMixin, admin.ModelAdmin): { "fields": ( "name", + "agenda_domain", "agenda_name", "tags", "ofs_id", @@ -1011,13 +1012,11 @@ def sortable_str(self, obj): @admin.display(boolean=True) def has_background_image(self, obj): - try: - return obj.background_image.url is not None - except ValueError: - return False + return True if obj.background_image.name else False - has_background_image.admin_order_field = "background_image" - has_background_image.short_description = "Image de fond" + if has_background_image: + has_background_image.admin_order_field = "background_image" + has_background_image.short_description = "Image de fond" # Inline for base Django Site diff --git a/geocity/apps/accounts/migrations/0019_administrativeentity_agenda_domain.py b/geocity/apps/accounts/migrations/0019_administrativeentity_agenda_domain.py new file mode 100644 index 000000000..1b366bcb1 --- /dev/null +++ b/geocity/apps/accounts/migrations/0019_administrativeentity_agenda_domain.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.11 on 2024-05-03 13:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0018_administrativeentity_agenda_name_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="administrativeentity", + name="agenda_domain", + field=models.CharField( + blank=True, + help_text="Utilisé afin de sélectionner les agendas visible dans agenda-embed", + max_length=128, + verbose_name="Domaine de l'agenda", + ), + ), + ] diff --git a/geocity/apps/accounts/models.py b/geocity/apps/accounts/models.py index 0f18c7370..bc208a22a 100644 --- a/geocity/apps/accounts/models.py +++ b/geocity/apps/accounts/models.py @@ -257,6 +257,14 @@ def associated_to_user(self, user): class AdministrativeEntity(models.Model): name = models.CharField(_("name"), max_length=128) + agenda_domain = models.CharField( + _("Domaine de l'agenda"), + help_text=_( + "Utilisé afin de sélectionner les agendas visible dans agenda-embed" + ), + max_length=128, + blank=True, + ) agenda_name = models.CharField( _("Nom dans l'api agenda"), help_text=_("Nom visible dans le filtre de l'agenda"), @@ -442,6 +450,18 @@ def clean(self): } ) + # Unique constraint for agenda_domain + # Cannot be used on model, because None is also subject to the constraint (blank=True) + if self.agenda_domain: + if ( + AdministrativeEntity.objects.exclude(pk=self.pk) + .filter(agenda_domain=self.agenda_domain) + .exists() + ): + raise ValidationError( + {"agenda_domain": _("Le domaine de l'agenda doit être unique.")} + ) + if ( not self.is_single_form_submissions and Form.objects.filter( diff --git a/geocity/apps/accounts/templates/account/lockout.html b/geocity/apps/accounts/templates/account/lockout.html index 1a614c9d4..f19db393b 100644 --- a/geocity/apps/accounts/templates/account/lockout.html +++ b/geocity/apps/accounts/templates/account/lockout.html @@ -25,7 +25,7 @@

{% translate "Votre compte a été verrouillé par mesure de sécurité. Mer © {% now "Y" %} Geocity  |  - {% translate "A propos" %} + {% translate "A propos" %} {% if config.CONTACT_URL %}  |  {% translate "Contact" %} diff --git a/geocity/apps/accounts/views.py b/geocity/apps/accounts/views.py index 8f14ec81d..a844b5e63 100644 --- a/geocity/apps/accounts/views.py +++ b/geocity/apps/accounts/views.py @@ -26,7 +26,8 @@ check_mandatory_2FA, permanent_user_required, ) -from geocity.fields import PrivateFileSystemStorage, PublicFileSystemStorage +from geocity.apps.submissions import services +from geocity.fields import PublicFileSystemStorage from . import forms, models from .users import is_2FA_mandatory @@ -396,11 +397,8 @@ def administrative_entity_file_download(request, path): Only allows logged user to download administrative entity files """ - mime_type, encoding = mimetypes.guess_type(path) - storage = PrivateFileSystemStorage() - try: - return StreamingHttpResponse(storage.open(path), content_type=mime_type) + return services.download_file(path) except IOError: raise Http404 diff --git a/geocity/apps/api/serializers.py b/geocity/apps/api/serializers.py index 3a1bf4f2f..9a165860f 100644 --- a/geocity/apps/api/serializers.py +++ b/geocity/apps/api/serializers.py @@ -964,7 +964,7 @@ def get_available_filters_for_agenda_as_qs(domains): entity = ( AdministrativeEntity.objects.filter( - tags__name=domain, + agenda_domain=domain, forms__agenda_visible=True, ) .distinct() diff --git a/geocity/apps/api/views.py b/geocity/apps/api/views.py index 3019dfaa8..e255249a6 100644 --- a/geocity/apps/api/views.py +++ b/geocity/apps/api/views.py @@ -625,15 +625,19 @@ def get_queryset(self): # Filter domain (administrative_entity) to permit sites to filter on their own domain (e.g.: sports, culture) domains = None + # Dont filter with other conditions, when trying to access to event details + # Still possible to put after a domain_filter later, if we return it in agenda-embed + if "pk" in self.kwargs: + return submissions + if "domain_filter" in query_params: domain_filter = query_params.getlist("domain_filter") entities = AdministrativeEntity.objects.filter(id__in=domain_filter) submissions = get_agenda_submissions(entities, submissions) - elif "domain" in query_params: domains = query_params["domain"] domains = domains.split(",") if domains else None - entities = AdministrativeEntity.objects.filter(tags__name__in=domains) + entities = AdministrativeEntity.objects.filter(agenda_domain__in=domains) submissions = get_agenda_submissions(entities, submissions) if "starts_at" in query_params: diff --git a/geocity/apps/core/static/css/admin/admin.css b/geocity/apps/core/static/css/admin/admin.css index 165f5984b..1565d3ed8 100644 --- a/geocity/apps/core/static/css/admin/admin.css +++ b/geocity/apps/core/static/css/admin/admin.css @@ -11,7 +11,37 @@ overflow-x: auto; } -/*Reduce select2 field width so that CRUD button remain visible*/ -tr .select2 { - width: 70% !important; +/* tweak admin-sortable2 for jazzmin compatibility with tabular inlines */ +/* Used in "1.4 Formulaires", tab "Champs" */ + +fieldset.module.sortable tbody tr.form-row { + padding: 20px 0px 10px 0px; /* top right bottom left*/ + border-bottom: 1px dashed #c7c7c7; +} + +fieldset.module.sortable tbody .original { + width: 50px; + text-align: center; +} + +fieldset.module.sortable td.original p { + width: 45px !important; +} + +fieldset.module.sortable tbody .field-field { + width: 650px; +} + +fieldset.module.sortable tbody .delete { + width: 50px; /* width of div. (thead .original - tbody.original - tbody .field-field)*/ + position: relative; /* used to move 50 px left */ + left: 75px; +} + +fieldset.module.sortable thead .original { + width: 750px; +} + +fieldset.module.sortable thead .column-field { + display: none; } diff --git a/geocity/apps/core/templates/base_generic.html b/geocity/apps/core/templates/base_generic.html index b9802b7ac..9c7e841c1 100644 --- a/geocity/apps/core/templates/base_generic.html +++ b/geocity/apps/core/templates/base_generic.html @@ -206,7 +206,7 @@ © {% now "Y" %} Geocity  |  - {% translate "A propos" %} + {% translate "A propos" %} {% if config.CONTACT_URL %}  |  {% translate "Contact" %} diff --git a/geocity/apps/forms/admin.py b/geocity/apps/forms/admin.py index fbd9e4bca..00becb393 100644 --- a/geocity/apps/forms/admin.py +++ b/geocity/apps/forms/admin.py @@ -1,7 +1,7 @@ import string import django.db.models -from adminsortable2.admin import SortableAdminMixin, SortableInlineAdminMixin +from adminsortable2.admin import SortableAdminMixin, SortableTabularInline from constance import config from django import forms from django.contrib import admin @@ -178,8 +178,7 @@ def save(self, *args, **kwargs): return super().save(*args, **kwargs) -# TODO: enable drag and drop for inline reorder -class FormFieldInline(admin.TabularInline, SortableInlineAdminMixin): +class FormFieldInline(SortableTabularInline): model = models.FormField extra = 2 verbose_name = _("Champ") @@ -198,7 +197,7 @@ class Media: css = {"all": ("css/admin/admin.css",)} -class FormPricesInline(admin.TabularInline, SortableInlineAdminMixin): +class FormPricesInline(admin.TabularInline): model = models.Form.prices.through extra = 1 verbose_name = _("Tarif") @@ -556,6 +555,22 @@ def clean_maximum_date(self): return maximum_date + def clean_map_widget_configuration(self): + selected_input_type = self.cleaned_data.get("input_type") + map_widget_configuration = self.cleaned_data.get("map_widget_configuration") + + if ( + selected_input_type == models.Field.INPUT_TYPE_GEOM + and not map_widget_configuration + ): + raise forms.ValidationError( + _( + "Vous devez obligatoirement sélectionner une configuration de carte avancée." + ) + ) + + return map_widget_configuration + class Media: js = ("js/admin/form_field.js",) diff --git a/geocity/apps/forms/models.py b/geocity/apps/forms/models.py index c2603f591..3d58c8a44 100644 --- a/geocity/apps/forms/models.py +++ b/geocity/apps/forms/models.py @@ -820,6 +820,9 @@ class Meta: verbose_name_plural = _("Champs du formulaire") ordering = ("order",) + def __str__(self): + return str(self.order) + # Input types INPUT_TYPE_ADDRESS = "address" diff --git a/geocity/apps/reports/management/commands/add_payment_reports.py b/geocity/apps/reports/management/commands/add_payment_reports.py index cad4b88bb..99d5dd711 100644 --- a/geocity/apps/reports/management/commands/add_payment_reports.py +++ b/geocity/apps/reports/management/commands/add_payment_reports.py @@ -65,33 +65,35 @@ def _create_payment_report(self, group, layout): order=4, report=report, title="", - content=""" - - - - - - - - - - - - - - - - - -
Libellé Prix CHF TTC
-

 

- -

{{ transaction_data.line_text }} : {{request_data.properties.submission_price.text}}

- -

 

-
 {{request_data.properties.submission_price.amount}}
Montant payé {{request_data.properties.submission_price.amount}}
- -

 

""", + content="""
+ + + + + + + + + + + + + + + + + + +
Libellé Prix CHF TTC
+

 

+ +

{{ transaction_data.line_text }} : {{request_data.properties.submission_price.text}}

+ +

 

+
 {{request_data.properties.submission_price.amount}}
Montant payé {{request_data.properties.submission_price.amount}}
+ +

 

+
""", ) section_paragraph_4.save() @@ -149,33 +151,35 @@ def _create_refund_report(self, group, layout): order=4, report=report, title="", - content=""" - - - - - - - - - - - - - - - - - -
Libellé Prix CHF TTC
-

 

- -

{{ transaction_data.line_text }} : {{request_data.properties.submission_price.text}}

- -

 

-
 -{{request_data.properties.submission_price.amount}}
Montant remboursé -{{request_data.properties.submission_price.amount}}
- -

 

""", + content="""
+ + + + + + + + + + + + + + + + + + +
Libellé Prix CHF TTC
+

 

+ +

{{ transaction_data.line_text }} : {{request_data.properties.submission_price.text}}

+ +

 

+
 -{{request_data.properties.submission_price.amount}}
Montant remboursé -{{request_data.properties.submission_price.amount}}
+ +

 

+
""", ) section_paragraph_4.save() diff --git a/geocity/apps/reports/templates/reports/sections/sectionamendproperty.html b/geocity/apps/reports/templates/reports/sections/sectionamendproperty.html index 7e7fda38c..91210b76b 100644 --- a/geocity/apps/reports/templates/reports/sections/sectionamendproperty.html +++ b/geocity/apps/reports/templates/reports/sections/sectionamendproperty.html @@ -17,7 +17,7 @@

{{forms.title.form_category}}

{% endif %} {% for comment_key, comment in forms.fields.items %} - {% if not comment.name in section.undesired_properties %} + {% if not comment.name in section.list_undesired_properties %}
{{comment.name}} : {{comment.value}}

{% endif %} {% endfor %} diff --git a/geocity/apps/reports/templates/reports/sections/sectiondetail.html b/geocity/apps/reports/templates/reports/sections/sectiondetail.html index 0c87b5cf6..eba960b2e 100644 --- a/geocity/apps/reports/templates/reports/sections/sectiondetail.html +++ b/geocity/apps/reports/templates/reports/sections/sectiondetail.html @@ -16,12 +16,12 @@

{{forms.title.form_category}}

{% endif %} {% for field_key, field in forms.fields.items %} - {% if not field.name in section.undesired_properties %} + {% if not field.name in section.list_undesired_properties and not field_key in section.list_undesired_properties %} {% if section.style == 0 %} -
{{field.name}} : {{field.value}}
+
{{field.name}} : {{field.value_formatted}}
{% elif section.style == 1 %}
{{field.name}}
-
{{field.value}}
+
{{field.value_formatted}}
{% endif %} {% endif %} diff --git a/geocity/apps/reports/views.py b/geocity/apps/reports/views.py index b633c3878..80ab70059 100644 --- a/geocity/apps/reports/views.py +++ b/geocity/apps/reports/views.py @@ -1,6 +1,7 @@ from django.contrib.auth.decorators import login_required from django.shortcuts import get_object_or_404, render from django.template.loader import render_to_string +from django.utils.safestring import mark_safe from rest_framework.decorators import api_view from geocity.apps.accounts.decorators import permanent_user_required @@ -14,6 +15,31 @@ from .services import generate_report_pdf_as_response +def preprocess_field_format(value): + """ + Uses of this function : + - Interpret line breaks + - Translate bool values in french + - Transform list in formatted strings + """ + + if isinstance(value, list): + print(len(value)) + if len(value) > 1: + result = "- " + "
- ".join(str(item) for item in value) + else: + result = value[0] + return mark_safe(result) + + if isinstance(value, bool): + return "Vrai" if value else "Faux" + + if value: + return mark_safe(value.replace("\r\n", "
").replace("\n", "
")) + + return value + + # TODO: instead of taking Submission and Form arguments, we should take # in SelectedForm, which already joins both, so they are consistent. @api_view(["GET"]) # pretend it's a DRF view, so we get token auth @@ -44,6 +70,29 @@ def report_content(request, submission_id, form_id, report_id, **kwargs): }, } + # Add line breaks for validation + for group, validation in ( + request_json_data["properties"].get("validations", {}).items() + ): + if "comment" in validation: + validation["comment"] = preprocess_field_format(validation["comment"]) + + # Add line breaks for amend_fields + for form_key, forms in ( + request_json_data["properties"].get("amend_fields", {}).items() + ): + for comment_key, comment in forms.get("fields", {}).items(): + if "value" in comment: + comment["value"] = preprocess_field_format(comment["value"]) + + # Reformat fields to remove lists and add line breaks + for form_key, forms in ( + request_json_data["properties"].get("submission_fields", {}).items() + ): + for field_key, field in forms.get("fields", {}).items(): + if "value" in field: + field["value_formatted"] = preprocess_field_format(field["value"]) + transaction = None if kwargs.get("transaction_id"): transaction = ( diff --git a/geocity/apps/submissions/admin.py b/geocity/apps/submissions/admin.py index a513674a9..d70b63fc1 100644 --- a/geocity/apps/submissions/admin.py +++ b/geocity/apps/submissions/admin.py @@ -239,7 +239,11 @@ class ComplementaryDocumentTypeAdmin(IntegratorFilterMixin, admin.ModelAdmin): ComplementaryDocumentTypeInline, ] form = ComplementaryDocumentTypeAdminForm - fields = ["name", "form", "integrator"] + fields = [ + "name", + "form", + "integrator", + ] def get_list_display(self, request): if request.user.is_superuser: @@ -258,29 +262,39 @@ def get_list_display(self, request): return list_display # Fields used in search_fields and list_filter - integrator_fields = [ + superuser_search_fields = [ + "name", + "form__name", + "integrator__name", + ] + integrator_search_fields = [ + "name", + "form__name", + ] + + superuser_list_search_fields = [ "name", "form", "integrator", - "form__administrative_entities", ] - user_fields = [ + + integrator_list_search_fields = [ "name", "form", ] def get_search_fields(self, request): if request.user.is_superuser: - search_fields = self.integrator_fields + search_fields = self.superuser_search_fields else: - search_fields = self.user_fields + search_fields = self.integrator_search_fields return search_fields def get_list_filter(self, request): if request.user.is_superuser: - list_filter = self.integrator_fields + list_filter = self.superuser_list_search_fields else: - list_filter = self.user_fields + list_filter = self.integrator_list_search_fields return list_filter # List types of documents diff --git a/geocity/apps/submissions/models.py b/geocity/apps/submissions/models.py index c1ec15181..209c54941 100644 --- a/geocity/apps/submissions/models.py +++ b/geocity/apps/submissions/models.py @@ -719,13 +719,12 @@ def set_field_value(self, form, field, value): ) is_file = field.input_type == Field.INPUT_TYPE_FILE is_date = field.input_type == Field.INPUT_TYPE_DATE - # TODO this doesn’t seem to be used? Remove? - is_address = field.input_type == Field.INPUT_TYPE_ADDRESS if value == "" or value is None: existing_value_obj.delete() else: if is_file: + # Use private storage to prevent uploaded files exposition to the outside world private_storage = fields.PrivateFileSystemStorage() # If the given File has a `url` attribute, it means the value comes from the `initial` form data, so the @@ -758,7 +757,13 @@ def set_field_value(self, form, field, value): directory, "{}_{}_{}{}".format(form.pk, field.pk, file_uuid, ext) ) + # Check file size and extension + from . import services + + services.validate_file(value) + private_storage.save(path, value) + # Postprocess images: remove all exif metadata from for better security and user privacy if upper_ext != "PDF": upper_ext = ext[1:].upper() diff --git a/geocity/apps/submissions/services.py b/geocity/apps/submissions/services.py index 11fb96bd7..80da9fe03 100644 --- a/geocity/apps/submissions/services.py +++ b/geocity/apps/submissions/services.py @@ -1,6 +1,7 @@ import tempfile import zipfile from datetime import datetime +from email.header import Header import filetype from constance import config @@ -168,7 +169,7 @@ def send_validation_reminder(submission, absolute_uri_func): def send_email_notification(data, attachments=None): from_email_name = ( - f'{data["submission"].administrative_entity.expeditor_name} ' + f'{Header(data["submission"].administrative_entity.expeditor_name, "utf-8").encode()} ' if data["submission"].administrative_entity.expeditor_name else "" ) @@ -314,10 +315,10 @@ def login_for_anonymous_request(request, entity): def download_file(path): storage = fields.PrivateFileSystemStorage() - # for some strange reason, firefox refuses to download the file. - # so we need to set the `Content-Type` to `application/octet-stream` so - # firefox will download it. For the time being, this "dirty" hack works - return FileResponse(storage.open(path), content_type="application/octet-stream") + # Force all files to be downloaded and never opened in browser on same domain + return FileResponse( + storage.open(path), content_type="application/octet-stream", as_attachment=True + ) def download_archives(archive_ids, user): diff --git a/geocity/apps/submissions/tables.py b/geocity/apps/submissions/tables.py index 2aacf14b1..c897f3e28 100644 --- a/geocity/apps/submissions/tables.py +++ b/geocity/apps/submissions/tables.py @@ -1,5 +1,6 @@ import collections import json +from collections import OrderedDict from datetime import datetime, timedelta from io import BytesIO as IO @@ -510,7 +511,7 @@ def create_export(self, export_format): # Handle null selected_forms (due to old bug YC-1093) if list_selected_forms: sheet_name = "_".join(map(str, list_selected_forms)) - ordered_dict = SubmissionPrintSerializer(submission).data + ordered_dict = OrderedDict(SubmissionPrintSerializer(submission).data) ordered_dict.move_to_end("geometry") data_dict = dict(ordered_dict) data_str = json.dumps(data_dict) diff --git a/geocity/apps/submissions/templates/submissions/emails/submission_changed.txt b/geocity/apps/submissions/templates/submissions/emails/submission_changed.txt index 9f5ccd8ab..c17d15f2d 100644 --- a/geocity/apps/submissions/templates/submissions/emails/submission_changed.txt +++ b/geocity/apps/submissions/templates/submissions/emails/submission_changed.txt @@ -11,7 +11,6 @@ {% else %} {% translate "Vous pouvez la consulter sur le lien suivant" %}: {{ submission_url }} {% endif %} - {% translate "Avec nos meilleures salutations," %} {% if administrative_entity.custom_signature %} {{ administrative_entity.custom_signature }} diff --git a/geocity/apps/submissions/views.py b/geocity/apps/submissions/views.py index 233b56dd9..586a1d662 100644 --- a/geocity/apps/submissions/views.py +++ b/geocity/apps/submissions/views.py @@ -1255,6 +1255,15 @@ def anonymous_submission(request): raise Http404 +def display_warning_message_for_awaiting_supplement_submission(request): + messages.warning( + request, + _( + "N'oubliez pas de renvoyer le formulaire une fois que vous aurez ajouté les compléments demandés." + ), + ) + + @redirect_bad_status_to_detail @login_required @user_passes_test(has_profile) @@ -1450,6 +1459,9 @@ def submission_fields(request, submission_id): if form_payment is not None: requires_online_payment = form_payment.requires_online_payment + if submission.status == models.Submission.STATUS_AWAITING_SUPPLEMENT: + display_warning_message_for_awaiting_supplement_submission(request) + if request.method == "POST": # Disable `required` fields validation to allow partial save form = forms.FieldsForm( @@ -1650,6 +1662,9 @@ def submission_appendices(request, submission_id): current_step_type=StepType.APPENDICES, ) + if submission.status == models.Submission.STATUS_AWAITING_SUPPLEMENT: + display_warning_message_for_awaiting_supplement_submission(request) + if request.method == "POST": form = forms.AppendicesForm( instance=submission, @@ -1703,6 +1718,9 @@ def submission_contacts(request, submission_id): request.POST or None, instance=submission ) + if submission.status == models.Submission.STATUS_AWAITING_SUPPLEMENT: + display_warning_message_for_awaiting_supplement_submission(request) + if request.method == "POST": formset = forms.get_submission_contacts_formset_initiated( submission, data=request.POST @@ -1777,6 +1795,9 @@ def submission_geo_time(request, submission_id): ).all(), ) + if submission.status == models.Submission.STATUS_AWAITING_SUPPLEMENT: + display_warning_message_for_awaiting_supplement_submission(request) + if request.method == "POST": if formset.is_valid(): with transaction.atomic(): @@ -1973,6 +1994,9 @@ def submission_submit(request, submission_id): if step.errors_count and step.url ] + if submission.status == models.Submission.STATUS_AWAITING_SUPPLEMENT: + display_warning_message_for_awaiting_supplement_submission(request) + if request.method == "POST": if incomplete_steps: raise SuspiciousOperation @@ -2621,8 +2645,6 @@ def get(self, request, pk, *args, **kwargs): transaction = get_transaction_from_id(pk) submission = transaction.submission_price.submission - submission.generate_and_save_pdf("confirmation", transaction) - if ( not request.user == submission.author or not transaction.status == transaction.STATUS_UNPAID @@ -2631,6 +2653,7 @@ def get(self, request, pk, *args, **kwargs): processor = get_payment_processor(submission.get_form_for_payment()) if processor.is_transaction_authorized(transaction): + submission.generate_and_save_pdf("confirmation", transaction) transaction.set_paid() submission_submit_confirmed(request, submission.pk) @@ -2766,8 +2789,6 @@ def get(self, request, pk, prolongation_date, *args, **kwargs): transaction = get_transaction_from_id(pk) submission = transaction.submission_price.submission - submission.generate_and_save_pdf("confirmation", transaction) - if ( not request.user == submission.author or not transaction.status == transaction.STATUS_UNPAID @@ -2776,9 +2797,11 @@ def get(self, request, pk, prolongation_date, *args, **kwargs): processor = get_payment_processor(submission.get_form_for_payment()) if processor.is_transaction_authorized(transaction): + submission.generate_and_save_pdf("confirmation", transaction) transaction.set_paid() submission.prolongation_date = datetime.fromtimestamp(prolongation_date) _set_prolongation_requested_and_notify(submission, request) + return render( request, "submissions/submission_payment_callback_confirm.html", diff --git a/geocity/settings.py b/geocity/settings.py index 335b0d585..b7fa91b57 100644 --- a/geocity/settings.py +++ b/geocity/settings.py @@ -42,6 +42,12 @@ SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True +CSRF_TRUSTED_ORIGINS = ( + os.getenv("CSRF_TRUSTED_ORIGINS").split(",") + if os.getenv("CSRF_TRUSTED_ORIGINS") + else [] +) + # SESSION TIMEOUT # default session time is one hour diff --git a/geocity/tests/api/test_agenda_api.py b/geocity/tests/api/test_agenda_api.py index 4fdcaf4a6..5fa689a92 100644 --- a/geocity/tests/api/test_agenda_api.py +++ b/geocity/tests/api/test_agenda_api.py @@ -51,7 +51,7 @@ def setUp(self): self.sit_integrator_group = factories.IntegratorGroupFactory(department=None) self.sit_pilot_group = factories.SecretariatGroupFactory(department=None) self.sit_administrative_entity = factories.AdministrativeEntityFactory( - tags=["sit"], integrator=self.sit_integrator_group + agenda_domain="sit", integrator=self.sit_integrator_group ) factories.IntegratorPermitDepartmentFactory( @@ -435,7 +435,7 @@ def setUp(self): self.fin_integrator_group = factories.IntegratorGroupFactory(department=None) self.fin_group = factories.SecretariatGroupFactory(department=None) self.fin_administrative_entity = factories.AdministrativeEntityFactory( - tags=["fin"], integrator=self.fin_integrator_group + agenda_domain="fin", integrator=self.fin_integrator_group ) factories.IntegratorPermitDepartmentFactory( diff --git a/geocity/tests/submissions/test_a_permit_request.py b/geocity/tests/submissions/test_a_permit_request.py index 84b284720..a6bde5740 100644 --- a/geocity/tests/submissions/test_a_permit_request.py +++ b/geocity/tests/submissions/test_a_permit_request.py @@ -2,6 +2,7 @@ import datetime import re from datetime import date +from email.header import Header from django.conf import settings from django.contrib.auth import get_user_model @@ -2952,11 +2953,13 @@ def test_secretary_email_and_name_are_set_for_the_administrative_entity(self): follow=True, ) + from_email = ( + f'{Header("Geocity Rocks", "utf-8").encode()} ' + ) + self.assertEqual(response.status_code, 200) self.assertEqual(len(mail.outbox), 1) - self.assertEqual( - mail.outbox[0].from_email, "Geocity Rocks " - ) + self.assertEqual(mail.outbox[0].from_email, from_email) self.assertEqual( mail.outbox[0].subject, "{} ({})".format( diff --git a/requirements.in b/requirements.in index 950a8cd1f..d88363c92 100644 --- a/requirements.in +++ b/requirements.in @@ -7,7 +7,7 @@ django-tables2 django-tables2-column-shifter # Base docker image must be update when GDAL is updated # https://github.com/yverdon/docker-geocity/ -gdal==3.6.3 +gdal==3.8.3 gunicorn html5lib jdcal diff --git a/requirements.txt b/requirements.txt index 51ecfc357..c8d12d1ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -87,7 +87,7 @@ django-formtools==2.5.1 # via django-two-factor-auth django-ipware==6.0.5 # via django-axes -django-jazzmin==2.6.1 +django-jazzmin==3.0.0 # via -r requirements.in django-jsoneditor==0.2.4 # via -r requirements.in @@ -136,7 +136,7 @@ et-xmlfile==1.1.0 # via openpyxl filetype==1.2.0 # via -r requirements.in -gdal==3.6.3 +gdal==3.8.3 # via -r requirements.in gunicorn==22.0.0 # via -r requirements.in diff --git a/requirements_dev.txt b/requirements_dev.txt index 3a50f0e0a..89a60bf8d 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -126,7 +126,7 @@ django-ipware==6.0.5 # via # -r requirements.txt # django-axes -django-jazzmin==2.6.1 +django-jazzmin==3.0.0 # via -r requirements.txt django-jsoneditor==0.2.4 # via -r requirements.txt @@ -199,7 +199,7 @@ filetype==1.2.0 # via -r requirements.txt freezegun==1.4.0 # via -r requirements_dev.in -gdal==3.6.3 +gdal==3.8.3 # via -r requirements.txt gunicorn==22.0.0 # via -r requirements.txt