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