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