From 8f52d34bb26eb24e0f95156f373922b67f2ee2a5 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 31 May 2023 17:05:57 +0200 Subject: [PATCH] WIP: final bit to make sending edit link working --- umap/settings/base.py | 2 ++ umap/static/umap/base.css | 4 ++++ umap/static/umap/js/umap.js | 19 ++++++++++++------ umap/static/umap/js/umap.ui.js | 20 +++++++++++-------- umap/tests/settings.py | 2 ++ umap/tests/test_map_views.py | 32 +++++++++++++++++++++++++++++- umap/views.py | 36 ++++++++++++++++++++++------------ 7 files changed, 87 insertions(+), 28 deletions(-) diff --git a/umap/settings/base.py b/umap/settings/base.py index 10b91139..196c2a18 100644 --- a/umap/settings/base.py +++ b/umap/settings/base.py @@ -114,6 +114,8 @@ INSTALLED_APPS = ( ) DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + # ============================================================================= # Calculation of directories relative to the project module location # ============================================================================= diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index af18a94a..759504a9 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -748,6 +748,10 @@ input[type=hidden].blur + .button { .umap-alert .error .umap-action:hover { color: #fff; } +.umap-alert input { + padding: 5px; + border-radius: 4px; +} /* *********** */ /* Tooltip */ diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 06366ae9..9fcc106c 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -1345,10 +1345,9 @@ L.U.Map.include({ if (data.permissions && data.permissions.anonymous_edit_url) { alert.actions = [ { - label: L._('Send me edit link by email'), - callback: function () { - this.sendEditLink() - }, + label: L._('Send me the link'), + input: L._('Email'), + callback: this.sendEditLink, callbackContext: this, }, ] @@ -1374,8 +1373,16 @@ L.U.Map.include({ }, sendEditLink: function () { - var url = L.Util.template(this.options.urls.map_send_edit_link, { - map_id: this.options.umap_id, + const url = L.Util.template(this.options.urls.map_send_edit_link, { + map_id: this.options.umap_id, + }), + input = this.ui._alert.querySelector('input'), + email = input.value + + const formData = new FormData() + formData.append('email', email) + this.post(url, { + data: formData, }) }, diff --git a/umap/static/umap/js/umap.ui.js b/umap/static/umap/js/umap.ui.js index 30deed2f..3fd945be 100644 --- a/umap/static/umap/js/umap.ui.js +++ b/umap/static/umap/js/umap.ui.js @@ -75,7 +75,6 @@ L.U.UI = L.Evented.extend({ }, popAlert: function (e) { - const self = this if (!e) { if (this.ALERTS.length) e = this.ALERTS.pop() else return @@ -108,20 +107,25 @@ L.U.UI = L.Evented.extend({ ) L.DomUtil.add('div', '', this._alert, e.content) if (e.actions) { - let action, el + let action, el, input for (let i = 0; i < e.actions.length; i++) { action = e.actions[i] + if (action.input) { + input = L.DomUtil.element( + 'input', + { className: 'umap-alert-input', placeholder: action.input }, + this._alert + ) + } el = L.DomUtil.element('a', { className: 'umap-action' }, this._alert) el.href = '#' el.textContent = action.label L.DomEvent.on(el, 'click', L.DomEvent.stop).on(el, 'click', close, this) if (action.callback) - L.DomEvent.on( - el, - 'click', - action.callback, - action.callbackContext || this.map - ) + L.DomEvent.on(el, 'click', () => { + action.callback.bind(action.callbackContext || this.map)() + close.bind(this)() + }) } } if (e.duration !== Infinity) { diff --git a/umap/tests/settings.py b/umap/tests/settings.py index 286c6860..d8706f5d 100644 --- a/umap/tests/settings.py +++ b/umap/tests/settings.py @@ -4,6 +4,8 @@ from umap.settings.base import * # pylint: disable=W0614,W0401 SECRET_KEY = "justfortests" COMPRESS_ENABLED = False +FROM_EMAIL = "test@test.org" +EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' if "TRAVIS" in os.environ: DATABASES = { diff --git a/umap/tests/test_map_views.py b/umap/tests/test_map_views.py index 93d44a0c..00741bdb 100644 --- a/umap/tests/test_map_views.py +++ b/umap/tests/test_map_views.py @@ -2,9 +2,10 @@ import json import pytest from django.contrib.auth import get_user_model +from django.core import mail from django.urls import reverse - from django.core.signing import Signer + from umap.models import DataLayer, Map, Star from .base import login_required @@ -568,3 +569,32 @@ def test_user_can_see_their_star(client, map, user): response = client.get(url) assert response.status_code == 200 assert map.name in response.content.decode() + + +@pytest.mark.usefixtures("allow_anonymous") +def test_cannot_send_link_on_owned_map(client, map): + assert len(mail.outbox) == 0 + url = reverse("map_send_edit_link", args=(map.pk,)) + resp = client.post(url, {"email": "foo@bar.org"}) + assert resp.status_code == 200 + assert json.loads(resp.content.decode()) == {"login_required": "/en/login/"} + assert len(mail.outbox) == 0 + + +@pytest.mark.usefixtures("allow_anonymous") +def test_cannot_send_link_on_anonymous_map_without_cookie(client, anonymap): + assert len(mail.outbox) == 0 + url = reverse("map_send_edit_link", args=(anonymap.pk,)) + resp = client.post(url, {"email": "foo@bar.org"}) + assert resp.status_code == 403 + assert len(mail.outbox) == 0 + + +@pytest.mark.usefixtures("allow_anonymous") +def test_can_send_link_on_anonymous_map_with_cookie(cookieclient, anonymap): + assert len(mail.outbox) == 0 + url = reverse("map_send_edit_link", args=(anonymap.pk,)) + resp = cookieclient.post(url, {"email": "foo@bar.org"}) + assert resp.status_code == 200 + assert len(mail.outbox) == 1 + assert mail.outbox[0].subject == "Your secret edit link" diff --git a/umap/views.py b/umap/views.py index c1ef32bd..b50e698a 100644 --- a/umap/views.py +++ b/umap/views.py @@ -12,6 +12,7 @@ from django.contrib.auth import logout as do_logout from django.contrib.auth import get_user_model from django.contrib.gis.measure import D from django.contrib.postgres.search import SearchQuery, SearchVector +from django.core.mail import send_mail from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.core.signing import BadSignature, Signer from django.core.validators import URLValidator, ValidationError @@ -33,7 +34,7 @@ from django.utils.translation import to_locale from django.views.generic import DetailView, TemplateView, View from django.views.generic.base import RedirectView from django.views.generic.detail import BaseDetailView -from django.views.generic.edit import CreateView, DeleteView, UpdateView +from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView from django.views.generic.list import ListView from .forms import ( @@ -44,6 +45,7 @@ from .forms import ( DataLayerForm, FlatErrorList, MapSettingsForm, + SendLinkForm, UpdateMapPermissionsForm, ) from .models import DataLayer, Licence, Map, Pictogram, Star, TileLayer @@ -618,26 +620,34 @@ class AttachAnonymousMap(View): return simple_json_response() -class SendEditLink(FormLessEditMixin, PermissionsMixin, UpdateView): - model = Map - pk_url_kwarg = 'map_id' +class SendEditLink(FormLessEditMixin, FormView): + form_class = SendLinkForm - def form_valid(self, form): - if (self.object.owner - or not self.object.is_anonymous_owner(self.request) - or not self.object.can_edit(self.request.user, self.request)): + def post(self, form, **kwargs): + self.object = kwargs["map_inst"] + if ( + self.object.owner + or not self.object.is_anonymous_owner(self.request) + or not self.object.can_edit(self.request.user, self.request) + ): return HttpResponseForbidden() - email = form.cleaned_data["email"] - from django.core.mail import send_mail + form = self.get_form() + if form.is_valid(): + email = form.cleaned_data["email"] + else: + return HttpResponseBadRequest("Invalid") + link = self.object.get_anonymous_edit_url() send_mail( - _('Your secret edit link'), - _('Here is your secret edit link: %(link)s' % {"link": link}), + _("Your secret edit link"), + _("Here is your secret edit link: %(link)s" % {"link": link}), settings.FROM_EMAIL, [email], fail_silently=False, ) - return simple_json_response(info=_("Email sent to %(email)s" % {"email": email})) + return simple_json_response( + info=_("Email sent to %(email)s" % {"email": email}) + ) class MapDelete(DeleteView):