diff --git a/docs/changelog.rst b/docs/changelog.rst
index 8440447e..a9a5202d 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -11,7 +11,8 @@ CHANGELOG
* Add contributions type / category filters
* Add contributions manager
* Add contribution status
-
+* Send mail to managers when contribution is created
+* Send mail to contributor when contribution is created
1.1.0 (2023-13-06)
-------------------------
diff --git a/docs/usage/configuration.rst b/docs/usage/configuration.rst
index 819a338f..a40e6f8c 100644
--- a/docs/usage/configuration.rst
+++ b/docs/usage/configuration.rst
@@ -1,3 +1,4 @@
+=============
Configuration
=============
@@ -63,4 +64,31 @@ To customize lists for each module, go to django administration page.
* Cities
* Districts
* Restricted area types
- * Restricted areas
\ No newline at end of file
+ * Restricted areas
+
+
+Email settings
+--------------
+
+Georiviere-admin will send emails:
+
+* to administrators when internal errors occur
+* to managers when a contribution is created
+* to contributors when a contribution is created
+
+Email configuration takes place in ``var/conf/custom.py``, where you control
+recipients emails (``ADMINS``, ``MANAGERS``) and email server configuration.
+
+You can test your configuration with the following command. A fake email will
+be sent to the managers:
+
+::
+
+ docker-compose run --rm web ./manage.py sendtestemail --managers
+
+If you don't want to send an email to contributors when they create a contribution on portal website,
+change this setting in ``var/conf/custom.py``:
+
+::
+
+ SEND_REPORT_ACK = False
diff --git a/georiviere/contribution/models.py b/georiviere/contribution/models.py
index 58768973..b370274a 100644
--- a/georiviere/contribution/models.py
+++ b/georiviere/contribution/models.py
@@ -1,6 +1,10 @@
+import logging
+
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.gis.db import models
+from django.core.mail import mail_managers
+from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
from mapentity.models import MapEntityMixin
@@ -19,6 +23,8 @@
from georiviere.studies.models import Study
from georiviere.watershed.mixins import WatershedPropertiesMixin
+logger = logging.getLogger(__name__)
+
class SeverityType(models.Model):
label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True)
@@ -144,6 +150,22 @@ def category_display(self):
s = '☆ ' % _("Published") + s
return s
+ def send_report_to_managers(self, template_name="contribution/report_email.txt"):
+ subject = _("Feedback from {email}").format(email=self.email_author)
+ message = render_to_string(template_name, {"contribution": self})
+ mail_managers(subject, message, fail_silently=False)
+
+ def try_send_report_to_managers(self):
+ try:
+ self.send_report_to_managers()
+ except Exception as e:
+ logger.error("Email could not be sent to managers.")
+ logger.exception(e) # This sends an email to admins :)
+
+ def save(self, *args, **kwargs):
+ super().save(*args, **kwargs) # Contribution updates should do nothing more
+ self.try_send_report_to_managers()
+
class LandingType(models.Model):
label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True)
diff --git a/georiviere/contribution/templates/contribution/report_email.txt b/georiviere/contribution/templates/contribution/report_email.txt
new file mode 100644
index 00000000..e99ca736
--- /dev/null
+++ b/georiviere/contribution/templates/contribution/report_email.txt
@@ -0,0 +1,16 @@
+{% load i18n l10n %}
+{% autoescape off %}
+{% blocktrans with email=contribution.email_author %}{{email}} has sent a contribution.{% endblocktrans %}
+
+{% if contribution.severity %}{% blocktrans with severity=report.severity %}Severity : {{severity}}{% endblocktrans %}{% endif %}
+{% trans "Category" %} : {{contribution.category}}
+{% trans "Description" %} : {{contribution.description}}
+
+{% trans "Portal" %} : {{contribution.portal}}
+
+{% if report.geom %}{% blocktrans with lat=report.geom_wgs84.y|stringformat:".6f" lng=report.geom_wgs84.x|stringformat:".6f" %}
+Lat : {{lat}} / Lon : {{lng}}
+http://www.openstreetmap.org/?mlat={{lat}}&mlon={{lng}}
+{% endblocktrans %}{% endif %}
+
+{% endautoescape %}
diff --git a/georiviere/contribution/tests/test_models.py b/georiviere/contribution/tests/test_models.py
index 848fe089..ae75692b 100644
--- a/georiviere/contribution/tests/test_models.py
+++ b/georiviere/contribution/tests/test_models.py
@@ -1,4 +1,5 @@
-from django.test import TestCase
+from django.core import mail
+from django.test import override_settings, TestCase
from .factories import (ContributionFactory, ContributionPotentialDamageFactory, ContributionQualityFactory,
ContributionQuantityFactory, ContributionFaunaFloraFactory,
@@ -8,16 +9,27 @@
TypePollutionFactory, ContributionStatusFactory)
-class ContributionCategoriesTest(TestCase):
- """Test for Category Contribution model"""
+@override_settings(MANAGERS=[("Fake", "fake@fake.fake"), ])
+class ContributionMetaTest(TestCase):
+ """Test for Contribution model"""
+
+ @override_settings(MANAGERS=["fake@fake.fake"])
+ def test_contribution_try_send_report_fail(self):
+ self.assertEqual(len(mail.outbox), 0)
+ contribution = ContributionFactory(email_author='mail.mail@mail')
+ self.assertEqual(str(contribution), "mail.mail@mail")
+ self.assertEqual(len(mail.outbox), 0)
def test_contribution_str(self):
ContributionStatusFactory(label="Informé")
+ self.assertEqual(len(mail.outbox), 0)
contribution = ContributionFactory(email_author='mail.mail@mail')
self.assertEqual(str(contribution), "mail.mail@mail")
self.assertEqual(contribution.category, "No category")
+ self.assertEqual(len(mail.outbox), 1)
def test_potentialdamage_str(self):
+ self.assertEqual(len(mail.outbox), 0)
potential_damage = ContributionPotentialDamageFactory(type=2)
self.assertEqual(str(potential_damage), "Contribution Potential Damage Excessive cutting of riparian forest")
contribution = potential_damage.contribution
@@ -25,8 +37,10 @@ def test_potentialdamage_str(self):
f"{contribution.email_author} "
f"Contribution Potential Damage Excessive cutting of riparian forest")
self.assertEqual(contribution.category, potential_damage)
+ self.assertEqual(len(mail.outbox), 1)
def test_quality_str(self):
+ self.assertEqual(len(mail.outbox), 0)
quality = ContributionQualityFactory(type=2)
self.assertEqual(str(quality), "Contribution Quality Pollution")
contribution = quality.contribution
@@ -34,8 +48,10 @@ def test_quality_str(self):
f"{contribution.email_author} "
f"Contribution Quality Pollution")
self.assertEqual(contribution.category, quality)
+ self.assertEqual(len(mail.outbox), 1)
def test_quantity_str(self):
+ self.assertEqual(len(mail.outbox), 0)
quantity = ContributionQuantityFactory(type=2)
self.assertEqual(str(quantity), "Contribution Quantity In the process of drying out")
contribution = quantity.contribution
@@ -43,8 +59,10 @@ def test_quantity_str(self):
f"{contribution.email_author} "
f"Contribution Quantity In the process of drying out")
self.assertEqual(contribution.category, quantity)
+ self.assertEqual(len(mail.outbox), 1)
def test_fauna_flora_str(self):
+ self.assertEqual(len(mail.outbox), 0)
fauna_flora = ContributionFaunaFloraFactory(type=2)
self.assertEqual(str(fauna_flora), "Contribution Fauna-Flora Heritage species")
contribution = fauna_flora.contribution
@@ -52,8 +70,10 @@ def test_fauna_flora_str(self):
f"{contribution.email_author} "
f"Contribution Fauna-Flora Heritage species")
self.assertEqual(contribution.category, fauna_flora)
+ self.assertEqual(len(mail.outbox), 1)
def test_landscape_elements_str(self):
+ self.assertEqual(len(mail.outbox), 0)
landscape_elements = ContributionLandscapeElementsFactory(type=2)
self.assertEqual(str(landscape_elements), "Contribution Landscape Element Fountain")
contribution = landscape_elements.contribution
@@ -61,6 +81,7 @@ def test_landscape_elements_str(self):
f"{contribution.email_author} "
f"Contribution Landscape Element Fountain")
self.assertEqual(contribution.category, landscape_elements)
+ self.assertEqual(len(mail.outbox), 1)
def test_severitytype_str(self):
severity_type = SeverityTypeTypeFactory(label="Severity type 1")
diff --git a/georiviere/portal/locale/en/LC_MESSAGES/django.po b/georiviere/portal/locale/en/LC_MESSAGES/django.po
index 76a1093e..102b97bf 100644
--- a/georiviere/portal/locale/en/LC_MESSAGES/django.po
+++ b/georiviere/portal/locale/en/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-05-15 20:17+0000\n"
+"POT-Creation-Date: 2023-07-24 14:23+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -27,7 +27,7 @@ msgstr ""
msgid "Map group layer"
msgstr ""
-msgid "Map group layers"
+msgid "Map groups layer"
msgstr ""
msgid "Map base layer"
@@ -86,3 +86,21 @@ msgstr ""
msgid "Category is not valid"
msgstr ""
+
+msgid "An error occured"
+msgstr ""
+
+msgid "Georiviere : Contribution"
+msgstr ""
+
+msgid ""
+"Hello,\n"
+"\n"
+" We acknowledge receipt of your contribution, thank you for "
+"your interest in Georiviere.\n"
+"\n"
+" Best regards,\n"
+"\n"
+" The Georiviere Team\n"
+" http://georiviere.fr"
+msgstr ""
diff --git a/georiviere/portal/locale/fr/LC_MESSAGES/django.po b/georiviere/portal/locale/fr/LC_MESSAGES/django.po
index 01530da8..7153c11b 100644
--- a/georiviere/portal/locale/fr/LC_MESSAGES/django.po
+++ b/georiviere/portal/locale/fr/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-05-15 20:17+0000\n"
+"POT-Creation-Date: 2023-07-24 14:23+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -27,8 +27,8 @@ msgstr "Couches de carte"
msgid "Map group layer"
msgstr "Groupe de couche de carte"
-msgid "Map group layers"
-msgstr "Groupes de couche de carte"
+msgid "Map groups layer"
+msgstr "Groupe de couche de carte"
msgid "Map base layer"
msgstr "Fond de carte"
@@ -86,3 +86,30 @@ msgstr "Portails"
msgid "Category is not valid"
msgstr "La catégorie n'est pas valide"
+
+msgid "An error occured"
+msgstr "Une erreur s'es produite"
+
+msgid "Georiviere : Contribution"
+msgstr "Georiviere : Contribution"
+
+msgid ""
+"Hello,\n"
+"\n"
+" We acknowledge receipt of your contribution, thank you for "
+"your interest in Georiviere.\n"
+"\n"
+" Best regards,\n"
+"\n"
+" The Georiviere Team\n"
+" http://georiviere.fr"
+msgstr ""
+"Bonjour,\n"
+"\n"
+"Nous accusons réception de votre contribution, merci de votre intérêt pour "
+"Georiviere.\n"
+"\n"
+"Cordialement,\n"
+"\n"
+"L'équipe Georiviere\n"
+"http://georiviere.fr"
diff --git a/georiviere/portal/tests/test_views/test_contribution.py b/georiviere/portal/tests/test_views/test_contribution.py
index 4d9f0db2..42fea79c 100644
--- a/georiviere/portal/tests/test_views/test_contribution.py
+++ b/georiviere/portal/tests/test_views/test_contribution.py
@@ -1,6 +1,7 @@
+from django.core import mail
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
-from django.test import TestCase
+from django.test import TestCase, override_settings
from django.urls import reverse
from unittest import mock
from geotrek.common.utils.testdata import get_dummy_uploaded_image, get_dummy_uploaded_file
@@ -32,6 +33,7 @@ def test_contribution_structure(self):
self.assertSetEqual(set(response.json().keys()), {'type', 'required', 'properties', 'allOf'})
def test_contribution_landscape_element(self):
+ self.assertEqual(len(mail.outbox), 0)
url = reverse('api_portal:contributions-list',
kwargs={'portal_pk': self.portal.pk, 'lang': 'fr'})
response = self.client.post(url, data={"geom": "POINT(0 0)",
@@ -44,8 +46,21 @@ def test_contribution_landscape_element(self):
landscape_element = contribution.landscape_element
self.assertEqual(contribution.email_author, 'x@x.x')
self.assertEqual(landscape_element.get_type_display(), 'Doline')
+ self.assertEqual(len(mail.outbox), 1)
+
+ @override_settings(MANAGERS=[("Fake", "fake@fake.fake"), ])
+ def test_contribution_create_send_manager_contributor(self):
+ self.assertEqual(len(mail.outbox), 0)
+ url = reverse('api_portal:contributions-list',
+ kwargs={'portal_pk': self.portal.pk, 'lang': 'fr'})
+ self.client.post(url, data={"geom": "POINT(0 0)",
+ "properties": '{"email_author": "x@x.x", "date_observation": "2022-08-16", '
+ '"category": "Contribution Élément Paysagers",'
+ '"type": "Doline"}'})
+ self.assertEqual(len(mail.outbox), 2)
def test_contribution_quality(self):
+ self.assertEqual(len(mail.outbox), 0)
url = reverse('api_portal:contributions-list',
kwargs={'portal_pk': self.portal.pk, 'lang': 'fr'})
response = self.client.post(url, data={"geom": "POINT(0 0)",
@@ -60,8 +75,10 @@ def test_contribution_quality(self):
self.assertEqual(quality.nature_pollution.label, 'Baz')
self.assertEqual(contribution.email_author, 'x@x.x')
self.assertEqual(quality.get_type_display(), 'Pollution')
+ self.assertEqual(len(mail.outbox), 1)
def test_contribution_quantity(self):
+ self.assertEqual(len(mail.outbox), 0)
url = reverse('api_portal:contributions-list',
kwargs={'portal_pk': self.portal.pk, 'lang': 'fr', 'format': 'json'})
response = self.client.post(url, data={"geom": "POINT(0 0)",
@@ -74,8 +91,10 @@ def test_contribution_quantity(self):
quantity = contribution.quantity
self.assertEqual(contribution.email_author, 'x@x.x')
self.assertEqual(quantity.get_type_display(), 'A sec')
+ self.assertEqual(len(mail.outbox), 1)
def test_contribution_faunaflora(self):
+ self.assertEqual(len(mail.outbox), 0)
url = reverse('api_portal:contributions-list',
kwargs={'portal_pk': self.portal.pk, 'lang': 'fr', 'format': 'json'})
response = self.client.post(url, data={"geom": "POINT(4 43.5)",
@@ -90,8 +109,10 @@ def test_contribution_faunaflora(self):
fauna_flora = contribution.fauna_flora
self.assertEqual(contribution.email_author, 'x@x.x')
self.assertEqual(fauna_flora.get_type_display(), 'Espèce invasive')
+ self.assertEqual(len(mail.outbox), 1)
def test_contribution_potential_damages(self):
+ self.assertEqual(len(mail.outbox), 0)
url = reverse('api_portal:contributions-list',
kwargs={'portal_pk': self.portal.pk, 'lang': 'fr', 'format': 'json'})
response = self.client.post(url, data={"geom": "POINT(4 42.5)",
@@ -104,6 +125,7 @@ def test_contribution_potential_damages(self):
potential_damage = contribution.potential_damage
self.assertEqual(contribution.email_author, 'x@x.x')
self.assertEqual(potential_damage.get_type_display(), 'Éboulements')
+ self.assertEqual(len(mail.outbox), 1)
def test_contribution_category_does_not_exist(self):
url = reverse('api_portal:contributions-list',
diff --git a/georiviere/portal/views/contribution.py b/georiviere/portal/views/contribution.py
index 3682d5cd..dce3d642 100644
--- a/georiviere/portal/views/contribution.py
+++ b/georiviere/portal/views/contribution.py
@@ -1,3 +1,4 @@
+import json
import os
from PIL import Image
@@ -7,9 +8,11 @@
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.files import File
+from django.core.mail import send_mail
from django.db.models import F, Q
from django.contrib.gis.db.models.functions import Transform
from django.utils import translation
+from django.utils.translation import gettext_lazy as _
from djangorestframework_camel_case.render import CamelCaseJSONRenderer
@@ -115,4 +118,20 @@ def create(self, request, *args, **kwargs):
logger.error(
f"Failed to convert attachment {name}{extension} for report {response.data.get('id')}: " + str(
e))
+ if settings.SEND_REPORT_ACK and response.status_code == 201:
+ send_mail(
+ _("Georiviere : Contribution"),
+ _(
+ """Hello,
+
+ We acknowledge receipt of your contribution, thank you for your interest in Georiviere.
+
+ Best regards,
+
+ The Georiviere Team
+ http://georiviere.fr"""
+ ),
+ settings.DEFAULT_FROM_EMAIL,
+ [json.loads(request.data.get("properties")).get('email_author'), ],
+ )
return response
diff --git a/georiviere/settings/__init__.py b/georiviere/settings/__init__.py
index d7fe93c0..cce24361 100644
--- a/georiviere/settings/__init__.py
+++ b/georiviere/settings/__init__.py
@@ -419,6 +419,7 @@ def construct_relative_path_mock(current_template_name, relative_name):
PHONE_NUMBER_DOCUMENT_REPORT = ''
WEBSITE_DOCUMENT_REPORT = ''
URL_DOCUMENT_REPORT = ''
+SEND_REPORT_ACK = True
# sensitivity