From 69bf6593ac1f60fc63a0f1c1120b2a742943c6c0 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 29 Dec 2021 17:34:11 +0100 Subject: [PATCH 01/15] (WIP) Add a button to send edit link by email in anonymous mode --- umap/forms.py | 4 ++++ umap/static/umap/js/umap.js | 27 ++++++++++++++++++++++----- umap/urls.py | 5 +++++ umap/views.py | 22 ++++++++++++++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/umap/forms.py b/umap/forms.py index 09d0ab3a..5fb2ed78 100644 --- a/umap/forms.py +++ b/umap/forms.py @@ -25,6 +25,10 @@ class FlatErrorList(ErrorList): return u' — '.join([e for e in self]) +class SendLinkForm(forms.Form): + email = forms.EmailField() + + class UpdateMapPermissionsForm(forms.ModelForm): class Meta: diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index ce0a5ef3..c317ee94 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -1336,11 +1336,23 @@ L.U.Map.include({ data: formData, context: this, callback: function (data) { - let duration = 3000 + let duration = 3000, + alert = { content: L._('Map has been saved!'), level: 'info' } if (!this.options.umap_id) { - duration = 100000 // we want a longer message at map creation (TODO UGLY) + alert.duration = 100000 // we want a longer message at map creation (TODO UGLY) this.options.umap_id = data.id this.permissions.setOptions(data.permissions) + if (data.permissions && data.permissions.anonymous_edit_url) { + alert.actions = [ + { + label: L._('Send me edit link by email'), + callback: function () { + this.sendEditLink() + }, + callbackContext: this, + }, + ] + } } else if (!this.permissions.isDirty) { // Do not override local changes to permissions, // but update in case some other editors changed them in the meantime. @@ -1350,11 +1362,10 @@ L.U.Map.include({ if (history && history.pushState) history.pushState({}, this.options.name, data.url) else window.location = data.url - if (data.info) msg = data.info - else msg = L._('Map has been saved!') + alert.content = data.info || alert.content this.once('saved', function () { this.isDirty = false - this.ui.alert({ content: msg, level: 'info', duration: duration }) + this.ui.alert(alert) }) this.ui.closePanel() this.permissions.save() @@ -1362,6 +1373,12 @@ L.U.Map.include({ }) }, + sendEditLink: function () { + var url = L.Util.template(this.options.urls.map_send_edit_link, { + map_id: this.options.umap_id, + }) + }, + getEditUrl: function () { return L.Util.template(this.options.urls.map_update, { map_id: this.options.umap_id, diff --git a/umap/urls.py b/umap/urls.py index a6bcfab4..33ffccfb 100644 --- a/umap/urls.py +++ b/umap/urls.py @@ -111,6 +111,11 @@ i18n_urls += decorated_patterns( views.UpdateMapPermissions.as_view(), name="map_update_permissions", ), + re_path( + r"^map/(?P[\d]+)/send-edit-link/$", + views.SendEditLink.as_view(), + name="map_send_edit_link", + ), re_path( r"^map/(?P[\d]+)/update/owner/$", views.AttachAnonymousMap.as_view(), diff --git a/umap/views.py b/umap/views.py index ed242369..22ef4111 100644 --- a/umap/views.py +++ b/umap/views.py @@ -622,6 +622,28 @@ class AttachAnonymousMap(View): return simple_json_response() +class SendEditLink(FormLessEditMixin, PermissionsMixin, UpdateView): + model = Map + pk_url_kwarg = 'map_id' + + 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)): + return HttpResponseForbidden() + email = form.cleaned_data["email"] + from django.core.mail import send_mail + + send_mail( + _('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})) + + class MapDelete(DeleteView): model = Map pk_url_kwarg = "map_id" From 7f2545f09bf0594069a61fc951aec9ccc86cfd1e Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 31 May 2023 16:16:31 +0200 Subject: [PATCH 02/15] Allow to use Infinity for an alert duration --- umap/static/umap/js/umap.js | 2 +- umap/static/umap/js/umap.ui.js | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index c317ee94..06366ae9 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -1339,7 +1339,7 @@ L.U.Map.include({ let duration = 3000, alert = { content: L._('Map has been saved!'), level: 'info' } if (!this.options.umap_id) { - alert.duration = 100000 // we want a longer message at map creation (TODO UGLY) + alert.duration = Infinity this.options.umap_id = data.id this.permissions.setOptions(data.permissions) if (data.permissions && data.permissions.anonymous_edit_url) { diff --git a/umap/static/umap/js/umap.ui.js b/umap/static/umap/js/umap.ui.js index acdb0817..30deed2f 100644 --- a/umap/static/umap/js/umap.ui.js +++ b/umap/static/umap/js/umap.ui.js @@ -124,10 +124,12 @@ L.U.UI = L.Evented.extend({ ) } } - self.ALERT_ID = timeoutID = window.setTimeout( - L.bind(close, this), - e.duration || 3000 - ) + if (e.duration !== Infinity) { + this.ALERT_ID = timeoutID = window.setTimeout( + L.bind(close, this), + e.duration || 3000 + ) + } }, tooltip: function (e) { From eb32dcc9b6c741996538fe67fa82f2dad01634af Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 31 May 2023 16:30:07 +0200 Subject: [PATCH 03/15] Make that Map.get_anonymous_edit_url returns full URL --- umap/forms.py | 3 +-- umap/management/commands/anonymous_edit_url.py | 3 +-- umap/models.py | 3 ++- umap/tests/test_map_views.py | 3 +-- umap/views.py | 10 +++------- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/umap/forms.py b/umap/forms.py index 5fb2ed78..cff7150b 100644 --- a/umap/forms.py +++ b/umap/forms.py @@ -40,8 +40,7 @@ class AnonymousMapPermissionsForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(AnonymousMapPermissionsForm, self).__init__(*args, **kwargs) - full_secret_link = "%s%s" % (settings.SITE_URL, self.instance.get_anonymous_edit_url()) - help_text = _('Secret edit link is %s') % full_secret_link + help_text = _('Secret edit link is %s') % self.instance.get_anonymous_edit_url() self.fields['edit_status'].help_text = _(help_text) STATUS = ( diff --git a/umap/management/commands/anonymous_edit_url.py b/umap/management/commands/anonymous_edit_url.py index f23d0f56..41c4c0a4 100644 --- a/umap/management/commands/anonymous_edit_url.py +++ b/umap/management/commands/anonymous_edit_url.py @@ -1,7 +1,6 @@ import sys from django.core.management.base import BaseCommand -from django.conf import settings from umap.models import Map @@ -25,4 +24,4 @@ class Command(BaseCommand): self.abort('Map with pk {} not found'.format(pk)) if map_.owner: self.abort('Map is not anonymous (owner: {})'.format(map_.owner)) - print(settings.SITE_URL + map_.get_anonymous_edit_url()) + print(map_.get_anonymous_edit_url()) diff --git a/umap/models.py b/umap/models.py index 6613a25c..fc9b8c40 100644 --- a/umap/models.py +++ b/umap/models.py @@ -162,7 +162,8 @@ class Map(NamedModel): def get_anonymous_edit_url(self): signer = Signer() signature = signer.sign(self.pk) - return reverse("map_anonymous_edit_url", kwargs={"signature": signature}) + path = reverse("map_anonymous_edit_url", kwargs={"signature": signature}) + return settings.SITE_URL + path def is_anonymous_owner(self, request): if self.owner: diff --git a/umap/tests/test_map_views.py b/umap/tests/test_map_views.py index 7a8f2f49..93d44a0c 100644 --- a/umap/tests/test_map_views.py +++ b/umap/tests/test_map_views.py @@ -45,8 +45,7 @@ def test_create(client, user, post_data): 'url': '/en/user/Joe/' }, 'editors': [], - 'anonymous_edit_url': ('http://umap.org' - + created_map.get_anonymous_edit_url()) + 'anonymous_edit_url': created_map.get_anonymous_edit_url() } diff --git a/umap/views.py b/umap/views.py index 22ef4111..c1ef32bd 100644 --- a/umap/views.py +++ b/umap/views.py @@ -466,13 +466,9 @@ class PermissionsMixin: for editor in self.object.editors.all() ] if not self.object.owner and self.object.is_anonymous_owner(self.request): - permissions["anonymous_edit_url"] = self.get_anonymous_edit_url() + permissions["anonymous_edit_url"] = self.object.get_anonymous_edit_url() return permissions - def get_anonymous_edit_url(self): - anonymous_url = self.object.get_anonymous_edit_url() - return settings.SITE_URL + anonymous_url - class MapView(MapDetailMixin, PermissionsMixin, DetailView): def get(self, request, *args, **kwargs): @@ -541,7 +537,7 @@ class MapCreate(FormLessEditMixin, PermissionsMixin, CreateView): if self.request.user.is_authenticated: form.instance.owner = self.request.user self.object = form.save() - anonymous_url = self.get_anonymous_edit_url() + anonymous_url = self.object.get_anonymous_edit_url() if not self.request.user.is_authenticated: msg = _( "Your map has been created! If you want to edit this map from " @@ -676,7 +672,7 @@ class MapClone(PermissionsMixin, View): msg = _( "Your map has been cloned! If you want to edit this map from " "another computer, please use this link: %(anonymous_url)s" - % {"anonymous_url": self.get_anonymous_edit_url()} + % {"anonymous_url": self.object.get_anonymous_edit_url()} ) else: msg = _("Congratulations, your map has been cloned!") From 8f52d34bb26eb24e0f95156f373922b67f2ee2a5 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 31 May 2023 17:05:57 +0200 Subject: [PATCH 04/15] 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): From e0760ca403055c317fe1da4fdd922dcc475f3d63 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 31 May 2023 17:20:22 +0200 Subject: [PATCH 05/15] Only ask to send edit link if email has been set up --- umap/settings/base.py | 1 + umap/static/umap/js/umap.js | 6 +++++- umap/urls.py | 19 +++++++++++-------- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/umap/settings/base.py b/umap/settings/base.py index 196c2a18..4e051c40 100644 --- a/umap/settings/base.py +++ b/umap/settings/base.py @@ -115,6 +115,7 @@ INSTALLED_APPS = ( DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" +FROM_EMAIL = None # ============================================================================= # Calculation of directories relative to the project module location diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 9fcc106c..57299e87 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -1342,7 +1342,11 @@ L.U.Map.include({ alert.duration = Infinity this.options.umap_id = data.id this.permissions.setOptions(data.permissions) - if (data.permissions && data.permissions.anonymous_edit_url) { + if ( + data.permissions && + data.permissions.anonymous_edit_url && + this.options.urls.map_send_edit_link + ) { alert.actions = [ { label: L._('Send me the link'), diff --git a/umap/urls.py b/umap/urls.py index 33ffccfb..e4d71291 100644 --- a/umap/urls.py +++ b/umap/urls.py @@ -99,8 +99,7 @@ i18n_urls += decorated_patterns( name="map_star", ), ) -i18n_urls += decorated_patterns( - [map_permissions_check, never_cache], +map_urls = [ re_path( r"^map/(?P[\d]+)/update/settings/$", views.MapUpdate.as_view(), @@ -111,11 +110,6 @@ i18n_urls += decorated_patterns( views.UpdateMapPermissions.as_view(), name="map_update_permissions", ), - re_path( - r"^map/(?P[\d]+)/send-edit-link/$", - views.SendEditLink.as_view(), - name="map_send_edit_link", - ), re_path( r"^map/(?P[\d]+)/update/owner/$", views.AttachAnonymousMap.as_view(), @@ -146,7 +140,16 @@ i18n_urls += decorated_patterns( views.DataLayerDelete.as_view(), name="datalayer_delete", ), -) +] +if settings.FROM_EMAIL: + map_urls.append( + re_path( + r"^map/(?P[\d]+)/send-edit-link/$", + views.SendEditLink.as_view(), + name="map_send_edit_link", + ) + ) +i18n_urls += decorated_patterns([map_permissions_check, never_cache], *map_urls) urlpatterns += i18n_patterns( re_path(r"^$", views.home, name="home"), re_path( From 7c2ba9d3e82302a1d582fa0b02e82bf005496508 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 31 May 2023 17:26:48 +0200 Subject: [PATCH 06/15] Add doc for email sending --- docs/install.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/install.md b/docs/install.md index 1aa9c8e3..2254cb39 100644 --- a/docs/install.md +++ b/docs/install.md @@ -94,3 +94,24 @@ may want to add an index. For that, you should do so: CREATE INDEX IF NOT EXISTS search_idx ON umap_map USING GIN(to_tsvector('umapdict', name), share_status); And change `UMAP_SEARCH_CONFIGURATION = "umapdict"` in your settings. + + +## Emails + +UMap can send the anonymous edit link by email. For this to work, you need to +add email specific settings. See [Django](https://docs.djangoproject.com/en/4.2/topics/email/#smtp-backend) +documentation. + +In general, you'll need to add something like this in your local settings: + +``` +FROM_EMAIL = "youradmin@email.org" +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_HOST = "smtp.provider.org" +EMAIL_PORT = 456 +EMAIL_HOST_USER = "username" +EMAIL_HOST_PASSWORD = "xxxx" +EMAIL_USE_TLS = True +# or +EMAIL_USE_SSL = True +``` From 8f77b63b080d04d5979f999b583f1442ec9a18bc Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 31 May 2023 17:33:05 +0200 Subject: [PATCH 07/15] Changed working of edit link alert + added a carriage return --- umap/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/umap/views.py b/umap/views.py index b50e698a..d323c353 100644 --- a/umap/views.py +++ b/umap/views.py @@ -542,9 +542,9 @@ class MapCreate(FormLessEditMixin, PermissionsMixin, CreateView): anonymous_url = self.object.get_anonymous_edit_url() if not self.request.user.is_authenticated: msg = _( - "Your map has been created! If you want to edit this map from " - "another computer, please use this link: %(anonymous_url)s" - % {"anonymous_url": anonymous_url} + "Your map has been created! As you are not logged in, here is your " + "secret link to edit the map, please keep it safe:" + + f"
{anonymous_url}" ) else: msg = _("Congratulations, your map has been created!") From 5d6afdfb1bcab16b66455ecc410a70a0cd7e3e88 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 31 May 2023 18:32:51 +0200 Subject: [PATCH 08/15] Add a "Copy link" button near to the anonymous edit link --- umap/static/umap/js/umap.js | 16 +++++++++++++++- umap/static/umap/js/umap.ui.js | 4 ++-- umap/views.py | 9 --------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 57299e87..5b9d1176 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -1339,7 +1339,7 @@ L.U.Map.include({ let duration = 3000, alert = { content: L._('Map has been saved!'), level: 'info' } if (!this.options.umap_id) { - alert.duration = Infinity + alert.content = L._('Congratulations, your map has been created!') this.options.umap_id = data.id this.permissions.setOptions(data.permissions) if ( @@ -1347,6 +1347,12 @@ L.U.Map.include({ data.permissions.anonymous_edit_url && this.options.urls.map_send_edit_link ) { + alert.duration = Infinity + alert.content = + L._( + 'Your map has been created! As you are not logged in, here is your secret link to edit the map, please keep it safe:' + ) + `
${data.permissions.anonymous_edit_url}` + alert.actions = [ { label: L._('Send me the link'), @@ -1354,6 +1360,14 @@ L.U.Map.include({ callback: this.sendEditLink, callbackContext: this, }, + { + label: L._('Copy link'), + callback: () => { + navigator.clipboard.writeText(data.permissions.anonymous_edit_url) + this.ui.alert({content: L._('Copied!'), level: 'info'}) + }, + callbackContext: this, + }, ] } } else if (!this.permissions.isDirty) { diff --git a/umap/static/umap/js/umap.ui.js b/umap/static/umap/js/umap.ui.js index 3fd945be..ea6ee9d5 100644 --- a/umap/static/umap/js/umap.ui.js +++ b/umap/static/umap/js/umap.ui.js @@ -84,7 +84,7 @@ L.U.UI = L.Evented.extend({ this._alert.innerHTML = '' L.DomUtil.addClass(this.parent, 'umap-alert') L.DomUtil.addClass(this._alert, level_class) - function close() { + const close = () => { if (timeoutID !== this.ALERT_ID) { return } // Another alert has been forced @@ -124,7 +124,7 @@ L.U.UI = L.Evented.extend({ if (action.callback) L.DomEvent.on(el, 'click', () => { action.callback.bind(action.callbackContext || this.map)() - close.bind(this)() + close() }) } } diff --git a/umap/views.py b/umap/views.py index d323c353..8c6374c8 100644 --- a/umap/views.py +++ b/umap/views.py @@ -540,14 +540,6 @@ class MapCreate(FormLessEditMixin, PermissionsMixin, CreateView): form.instance.owner = self.request.user self.object = form.save() anonymous_url = self.object.get_anonymous_edit_url() - if not self.request.user.is_authenticated: - msg = _( - "Your map has been created! As you are not logged in, here is your " - "secret link to edit the map, please keep it safe:" - + f"
{anonymous_url}" - ) - else: - msg = _("Congratulations, your map has been created!") permissions = self.get_permissions() # User does not have the cookie yet. permissions["anonymous_edit_url"] = anonymous_url @@ -555,7 +547,6 @@ class MapCreate(FormLessEditMixin, PermissionsMixin, CreateView): id=self.object.pk, url=self.object.get_absolute_url(), permissions=permissions, - info=msg, ) if not self.request.user.is_authenticated: key, value = self.object.signed_cookie_elements From cfe6bdf2a8e1bb9d2791e5ad4641a44d11a59dc8 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 31 May 2023 18:40:52 +0200 Subject: [PATCH 09/15] Fix closing the alert when clicking on an action button --- umap/static/umap/js/umap.ui.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/umap/static/umap/js/umap.ui.js b/umap/static/umap/js/umap.ui.js index ea6ee9d5..01e35c13 100644 --- a/umap/static/umap/js/umap.ui.js +++ b/umap/static/umap/js/umap.ui.js @@ -85,7 +85,7 @@ L.U.UI = L.Evented.extend({ L.DomUtil.addClass(this.parent, 'umap-alert') L.DomUtil.addClass(this._alert, level_class) const close = () => { - if (timeoutID !== this.ALERT_ID) { + if (timeoutID && timeoutID !== this.ALERT_ID) { return } // Another alert has been forced this._alert.innerHTML = '' @@ -123,7 +123,7 @@ L.U.UI = L.Evented.extend({ L.DomEvent.on(el, 'click', L.DomEvent.stop).on(el, 'click', close, this) if (action.callback) L.DomEvent.on(el, 'click', () => { - action.callback.bind(action.callbackContext || this.map)() + action.callback.call(action.callbackContext || this.map) close() }) } From d180caa0d567154818e884719d3bfe756f48ac9f Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Thu, 1 Jun 2023 09:32:44 +0200 Subject: [PATCH 10/15] Fix closing alert on callback --- umap/static/umap/js/umap.ui.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/umap/static/umap/js/umap.ui.js b/umap/static/umap/js/umap.ui.js index 01e35c13..bf5d338f 100644 --- a/umap/static/umap/js/umap.ui.js +++ b/umap/static/umap/js/umap.ui.js @@ -120,12 +120,11 @@ L.U.UI = L.Evented.extend({ 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.call(action.callbackContext || this.map) - close() - }) + L.DomEvent.on(el, 'click', L.DomEvent.stop) + if (action.callback) { + L.DomEvent.on(el, 'click', action.callback, action.callbackContext || this.map) + } + L.DomEvent.on(el, 'click', close, this) } } if (e.duration !== Infinity) { From 5460876ff40dea180848fd4a478a9399b86b3b45 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Fri, 2 Jun 2023 17:33:09 +0200 Subject: [PATCH 11/15] Update umap/views.py Co-authored-by: David Larlet <3556+davidbgk@users.noreply.github.com> --- umap/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umap/views.py b/umap/views.py index 8c6374c8..04dc1dd4 100644 --- a/umap/views.py +++ b/umap/views.py @@ -630,7 +630,7 @@ class SendEditLink(FormLessEditMixin, FormView): link = self.object.get_anonymous_edit_url() send_mail( - _("Your secret edit link"), + _("The uMap edit link for your map: %(map_name)s" % {"map_name": self.object.name}), _("Here is your secret edit link: %(link)s" % {"link": link}), settings.FROM_EMAIL, [email], From ca97a4d74569550a4013b8a0e87eaa7c43609561 Mon Sep 17 00:00:00 2001 From: David Larlet Date: Fri, 2 Jun 2023 11:41:40 -0400 Subject: [PATCH 12/15] Bump constrasts on buttons --- umap/static/umap/base.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index 759504a9..13719a88 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -734,7 +734,7 @@ input[type=hidden].blur + .button { .umap-alert .umap-action { margin-left: 10px; background-color: #fff; - color: #999; + color: #000; padding: 5px; border-radius: 4px; } From 725feb8d015608f0d8d80d62377fe421f3c5fb7a Mon Sep 17 00:00:00 2001 From: David Larlet Date: Fri, 2 Jun 2023 11:50:03 -0400 Subject: [PATCH 13/15] Fallback to copy to clipboard without HTTPS --- umap/static/umap/js/umap.js | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 5b9d1176..036a7abf 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -1332,6 +1332,34 @@ L.U.Map.include({ formData.append('name', this.options.name) formData.append('center', JSON.stringify(this.geometry())) formData.append('settings', JSON.stringify(geojson)) + + function copyToClipboard(textToCopy) { + // https://stackoverflow.com/a/65996386 + // Navigator clipboard api needs a secure context (https) + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(textToCopy) + } else { + // Use the 'out of viewport hidden text area' trick + const textArea = document.createElement('textarea') + textArea.value = textToCopy + + // Move textarea out of the viewport so it's not visible + textArea.style.position = 'absolute' + textArea.style.left = '-999999px' + + document.body.prepend(textArea) + textArea.select() + + try { + document.execCommand('copy') + } catch (error) { + console.error(error) + } finally { + textArea.remove() + } + } + } + this.post(this.getSaveUrl(), { data: formData, context: this, @@ -1363,8 +1391,11 @@ L.U.Map.include({ { label: L._('Copy link'), callback: () => { - navigator.clipboard.writeText(data.permissions.anonymous_edit_url) - this.ui.alert({content: L._('Copied!'), level: 'info'}) + copyToClipboard(data.permissions.anonymous_edit_url) + this.ui.alert({ + content: L._('Secret edit link copied to clipboard!'), + level: 'info', + }) }, callbackContext: this, }, From bf4a84de9940c5aaf9a5a2e42e35d7fa77dc958b Mon Sep 17 00:00:00 2001 From: David Larlet Date: Fri, 2 Jun 2023 11:54:09 -0400 Subject: [PATCH 14/15] Apply black on test map views file --- umap/tests/test_map_views.py | 259 +++++++++++++++++------------------ 1 file changed, 129 insertions(+), 130 deletions(-) diff --git a/umap/tests/test_map_views.py b/umap/tests/test_map_views.py index 00741bdb..41227d85 100644 --- a/umap/tests/test_map_views.py +++ b/umap/tests/test_map_views.py @@ -17,49 +17,45 @@ User = get_user_model() @pytest.fixture def post_data(): return { - 'name': 'name', - 'center': '{"type":"Point","coordinates":[13.447265624999998,48.94415123418794]}', # noqa - 'settings': '{"type":"Feature","geometry":{"type":"Point","coordinates":[5.0592041015625,52.05924589011585]},"properties":{"tilelayer":{"maxZoom":20,"url_template":"http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png","minZoom":0,"attribution":"HOT and friends"},"licence":"","description":"","name":"test enrhûmé","tilelayersControl":true,"displayDataBrowserOnLoad":false,"displayPopupFooter":true,"displayCaptionOnLoad":false,"miniMap":true,"moreControl":true,"scaleControl":true,"zoomControl":true,"datalayersControl":true,"zoom":8}}' # noqa + "name": "name", + "center": '{"type":"Point","coordinates":[13.447265624999998,48.94415123418794]}', # noqa + "settings": '{"type":"Feature","geometry":{"type":"Point","coordinates":[5.0592041015625,52.05924589011585]},"properties":{"tilelayer":{"maxZoom":20,"url_template":"http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png","minZoom":0,"attribution":"HOT and friends"},"licence":"","description":"","name":"test enrhûmé","tilelayersControl":true,"displayDataBrowserOnLoad":false,"displayPopupFooter":true,"displayCaptionOnLoad":false,"miniMap":true,"moreControl":true,"scaleControl":true,"zoomControl":true,"datalayersControl":true,"zoom":8}}', # noqa } def test_create(client, user, post_data): - url = reverse('map_create') + url = reverse("map_create") # POST only mendatory fields - name = 'test-map-with-new-name' - post_data['name'] = name + name = "test-map-with-new-name" + post_data["name"] = name client.login(username=user.username, password="123123") response = client.post(url, post_data) assert response.status_code == 200 j = json.loads(response.content.decode()) - created_map = Map.objects.latest('pk') - assert j['id'] == created_map.pk + created_map = Map.objects.latest("pk") + assert j["id"] == created_map.pk assert created_map.name == name assert created_map.center.x == 13.447265624999998 assert created_map.center.y == 48.94415123418794 - assert j['permissions'] == { - 'edit_status': 3, - 'share_status': 1, - 'owner': { - 'id': user.pk, - 'name': 'Joe', - 'url': '/en/user/Joe/' - }, - 'editors': [], - 'anonymous_edit_url': created_map.get_anonymous_edit_url() + assert j["permissions"] == { + "edit_status": 3, + "share_status": 1, + "owner": {"id": user.pk, "name": "Joe", "url": "/en/user/Joe/"}, + "editors": [], + "anonymous_edit_url": created_map.get_anonymous_edit_url(), } def test_map_create_permissions(client, settings): settings.UMAP_ALLOW_ANONYMOUS = False - url = reverse('map_create') + url = reverse("map_create") # POST anonymous response = client.post(url, {}) assert login_required(response) def test_map_update_access(client, map, user): - url = reverse('map_update', kwargs={'map_id': map.pk}) + url = reverse("map_update", kwargs={"map_id": map.pk}) # GET anonymous response = client.get(url) assert login_required(response) @@ -77,7 +73,7 @@ def test_map_update_access(client, map, user): def test_map_update_permissions_access(client, map, user): - url = reverse('map_update_permissions', kwargs={'map_id': map.pk}) + url = reverse("map_update_permissions", kwargs={"map_id": map.pk}) # GET anonymous response = client.get(url) assert login_required(response) @@ -95,22 +91,22 @@ def test_map_update_permissions_access(client, map, user): def test_update(client, map, post_data): - url = reverse('map_update', kwargs={'map_id': map.pk}) + url = reverse("map_update", kwargs={"map_id": map.pk}) # POST only mendatory fields - name = 'new map name' - post_data['name'] = name + name = "new map name" + post_data["name"] = name client.login(username=map.owner.username, password="123123") response = client.post(url, post_data) assert response.status_code == 200 j = json.loads(response.content.decode()) - assert 'html' not in j + assert "html" not in j updated_map = Map.objects.get(pk=map.pk) - assert j['id'] == updated_map.pk + assert j["id"] == updated_map.pk assert updated_map.name == name def test_delete(client, map, datalayer): - url = reverse('map_delete', args=(map.pk, )) + url = reverse("map_delete", args=(map.pk,)) client.login(username=map.owner.username, password="123123") response = client.post(url, {}, follow=True) assert response.status_code == 200 @@ -120,61 +116,58 @@ def test_delete(client, map, datalayer): assert User.objects.filter(pk=map.owner.pk).exists() # Test response is a json j = json.loads(response.content.decode()) - assert 'redirect' in j + assert "redirect" in j def test_wrong_slug_should_redirect_to_canonical(client, map): - url = reverse('map', kwargs={'pk': map.pk, 'slug': 'wrong-slug'}) - canonical = reverse('map', kwargs={'pk': map.pk, - 'slug': map.slug}) + url = reverse("map", kwargs={"pk": map.pk, "slug": "wrong-slug"}) + canonical = reverse("map", kwargs={"pk": map.pk, "slug": map.slug}) response = client.get(url) assert response.status_code == 301 - assert response['Location'] == canonical + assert response["Location"] == canonical def test_wrong_slug_should_redirect_with_query_string(client, map): - url = reverse('map', kwargs={'pk': map.pk, 'slug': 'wrong-slug'}) + url = reverse("map", kwargs={"pk": map.pk, "slug": "wrong-slug"}) url = "{}?allowEdit=0".format(url) - canonical = reverse('map', kwargs={'pk': map.pk, - 'slug': map.slug}) + canonical = reverse("map", kwargs={"pk": map.pk, "slug": map.slug}) canonical = "{}?allowEdit=0".format(canonical) response = client.get(url) assert response.status_code == 301 - assert response['Location'] == canonical + assert response["Location"] == canonical def test_should_not_consider_the_query_string_for_canonical_check(client, map): - url = reverse('map', kwargs={'pk': map.pk, 'slug': map.slug}) + url = reverse("map", kwargs={"pk": map.pk, "slug": map.slug}) url = "{}?allowEdit=0".format(url) response = client.get(url) assert response.status_code == 200 def test_short_url_should_redirect_to_canonical(client, map): - url = reverse('map_short_url', kwargs={'pk': map.pk}) - canonical = reverse('map', kwargs={'pk': map.pk, - 'slug': map.slug}) + url = reverse("map_short_url", kwargs={"pk": map.pk}) + canonical = reverse("map", kwargs={"pk": map.pk, "slug": map.slug}) response = client.get(url) assert response.status_code == 301 - assert response['Location'] == canonical + assert response["Location"] == canonical def test_clone_map_should_create_a_new_instance(client, map): assert Map.objects.count() == 1 - url = reverse('map_clone', kwargs={'map_id': map.pk}) + url = reverse("map_clone", kwargs={"map_id": map.pk}) client.login(username=map.owner.username, password="123123") response = client.post(url) assert response.status_code == 200 assert Map.objects.count() == 2 - clone = Map.objects.latest('pk') + clone = Map.objects.latest("pk") assert clone.pk != map.pk - assert clone.name == u"Clone of " + map.name + assert clone.name == "Clone of " + map.name def test_user_not_allowed_should_not_clone_map(client, map, user, settings): settings.UMAP_ALLOW_ANONYMOUS = False assert Map.objects.count() == 1 - url = reverse('map_clone', kwargs={'map_id': map.pk}) + url = reverse("map_clone", kwargs={"map_id": map.pk}) map.edit_status = map.OWNER map.save() response = client.post(url) @@ -191,7 +184,7 @@ def test_user_not_allowed_should_not_clone_map(client, map, user, settings): def test_clone_should_set_cloner_as_owner(client, map, user): - url = reverse('map_clone', kwargs={'map_id': map.pk}) + url = reverse("map_clone", kwargs={"map_id": map.pk}) map.edit_status = map.EDITORS map.editors.add(user) map.save() @@ -199,32 +192,32 @@ def test_clone_should_set_cloner_as_owner(client, map, user): response = client.post(url) assert response.status_code == 200 assert Map.objects.count() == 2 - clone = Map.objects.latest('pk') + clone = Map.objects.latest("pk") assert clone.pk != map.pk - assert clone.name == u"Clone of " + map.name + assert clone.name == "Clone of " + map.name assert clone.owner == user def test_map_creation_should_allow_unicode_names(client, map, post_data): - url = reverse('map_create') + url = reverse("map_create") # POST only mendatory fields - name = u'Академический' - post_data['name'] = name + name = "Академический" + post_data["name"] = name client.login(username=map.owner.username, password="123123") response = client.post(url, post_data) assert response.status_code == 200 j = json.loads(response.content.decode()) - created_map = Map.objects.latest('pk') - assert j['id'] == created_map.pk + created_map = Map.objects.latest("pk") + assert j["id"] == created_map.pk assert created_map.name == name # Lower case of the russian original name # self.assertEqual(created_map.slug, u"академический") # for now we fallback to "map", see unicode_name branch - assert created_map.slug == 'map' + assert created_map.slug == "map" def test_anonymous_can_access_map_with_share_status_public(client, map): - url = reverse('map', args=(map.slug, map.pk)) + url = reverse("map", args=(map.slug, map.pk)) map.share_status = map.PUBLIC map.save() response = client.get(url) @@ -232,7 +225,7 @@ def test_anonymous_can_access_map_with_share_status_public(client, map): def test_anonymous_can_access_map_with_share_status_open(client, map): - url = reverse('map', args=(map.slug, map.pk)) + url = reverse("map", args=(map.slug, map.pk)) map.share_status = map.OPEN map.save() response = client.get(url) @@ -240,7 +233,7 @@ def test_anonymous_can_access_map_with_share_status_open(client, map): def test_anonymous_cannot_access_map_with_share_status_private(client, map): - url = reverse('map', args=(map.slug, map.pk)) + url = reverse("map", args=(map.slug, map.pk)) map.share_status = map.PRIVATE map.save() response = client.get(url) @@ -248,7 +241,7 @@ def test_anonymous_cannot_access_map_with_share_status_private(client, map): def test_owner_can_access_map_with_share_status_private(client, map): - url = reverse('map', args=(map.slug, map.pk)) + url = reverse("map", args=(map.slug, map.pk)) map.share_status = map.PRIVATE map.save() client.login(username=map.owner.username, password="123123") @@ -257,7 +250,7 @@ def test_owner_can_access_map_with_share_status_private(client, map): def test_editors_can_access_map_with_share_status_private(client, map, user): - url = reverse('map', args=(map.slug, map.pk)) + url = reverse("map", args=(map.slug, map.pk)) map.share_status = map.PRIVATE map.editors.add(user) map.save() @@ -267,7 +260,7 @@ def test_editors_can_access_map_with_share_status_private(client, map, user): def test_anonymous_cannot_access_map_with_share_status_blocked(client, map): - url = reverse('map', args=(map.slug, map.pk)) + url = reverse("map", args=(map.slug, map.pk)) map.share_status = map.BLOCKED map.save() response = client.get(url) @@ -275,7 +268,7 @@ def test_anonymous_cannot_access_map_with_share_status_blocked(client, map): def test_owner_cannot_access_map_with_share_status_blocked(client, map): - url = reverse('map', args=(map.slug, map.pk)) + url = reverse("map", args=(map.slug, map.pk)) map.share_status = map.BLOCKED map.save() client.login(username=map.owner.username, password="123123") @@ -283,8 +276,10 @@ def test_owner_cannot_access_map_with_share_status_blocked(client, map): assert response.status_code == 403 -def test_non_editor_cannot_access_map_if_share_status_private(client, map, user): # noqa - url = reverse('map', args=(map.slug, map.pk)) +def test_non_editor_cannot_access_map_if_share_status_private( + client, map, user +): # noqa + url = reverse("map", args=(map.slug, map.pk)) map.share_status = map.PRIVATE map.save() client.login(username=user.username, password="123123") @@ -293,16 +288,16 @@ def test_non_editor_cannot_access_map_if_share_status_private(client, map, user) def test_map_geojson_view(client, map): - url = reverse('map_geojson', args=(map.pk, )) + url = reverse("map_geojson", args=(map.pk,)) response = client.get(url) j = json.loads(response.content.decode()) - assert 'json' in response['content-type'] - assert 'type' in j + assert "json" in response["content-type"] + assert "type" in j def test_only_owner_can_delete(client, map, user): map.editors.add(user) - url = reverse('map_delete', kwargs={'map_id': map.pk}) + url = reverse("map_delete", kwargs={"map_id": map.pk}) client.login(username=user.username, password="123123") response = client.post(url, {}, follow=True) assert response.status_code == 403 @@ -312,10 +307,10 @@ def test_map_editors_do_not_see_owner_change_input(client, map, user): map.editors.add(user) map.edit_status = map.EDITORS map.save() - url = reverse('map_update_permissions', kwargs={'map_id': map.pk}) + url = reverse("map_update_permissions", kwargs={"map_id": map.pk}) client.login(username=user.username, password="123123") response = client.get(url) - assert 'id_owner' not in response + assert "id_owner" not in response def test_logged_in_user_can_edit_map_editable_by_anonymous(client, map, user): @@ -323,114 +318,116 @@ def test_logged_in_user_can_edit_map_editable_by_anonymous(client, map, user): map.edit_status = map.ANONYMOUS map.save() client.login(username=user.username, password="123123") - url = reverse('map_update', kwargs={'map_id': map.pk}) - new_name = 'this is my new name' + url = reverse("map_update", kwargs={"map_id": map.pk}) + new_name = "this is my new name" data = { - 'center': '{"type":"Point","coordinates":[13.447265624999998,48.94415123418794]}', # noqa - 'name': new_name + "center": '{"type":"Point","coordinates":[13.447265624999998,48.94415123418794]}', # noqa + "name": new_name, } response = client.post(url, data) assert response.status_code == 200 assert Map.objects.get(pk=map.pk).name == new_name -@pytest.mark.usefixtures('allow_anonymous') +@pytest.mark.usefixtures("allow_anonymous") def test_anonymous_create(cookieclient, post_data): - url = reverse('map_create') + url = reverse("map_create") # POST only mendatory fields - name = 'test-map-with-new-name' - post_data['name'] = name + name = "test-map-with-new-name" + post_data["name"] = name response = cookieclient.post(url, post_data) assert response.status_code == 200 j = json.loads(response.content.decode()) - created_map = Map.objects.latest('pk') - assert j['id'] == created_map.pk - assert (created_map.get_anonymous_edit_url() - in j['permissions']['anonymous_edit_url']) + created_map = Map.objects.latest("pk") + assert j["id"] == created_map.pk + assert ( + created_map.get_anonymous_edit_url() in j["permissions"]["anonymous_edit_url"] + ) assert created_map.name == name key, value = created_map.signed_cookie_elements assert key in cookieclient.cookies -@pytest.mark.usefixtures('allow_anonymous') +@pytest.mark.usefixtures("allow_anonymous") def test_anonymous_update_without_cookie_fails(client, anonymap, post_data): # noqa - url = reverse('map_update', kwargs={'map_id': anonymap.pk}) + url = reverse("map_update", kwargs={"map_id": anonymap.pk}) response = client.post(url, post_data) assert response.status_code == 403 -@pytest.mark.usefixtures('allow_anonymous') -def test_anonymous_update_with_cookie_should_work(cookieclient, anonymap, post_data): # noqa - url = reverse('map_update', kwargs={'map_id': anonymap.pk}) +@pytest.mark.usefixtures("allow_anonymous") +def test_anonymous_update_with_cookie_should_work( + cookieclient, anonymap, post_data +): # noqa + url = reverse("map_update", kwargs={"map_id": anonymap.pk}) # POST only mendatory fields - name = 'new map name' - post_data['name'] = name + name = "new map name" + post_data["name"] = name response = cookieclient.post(url, post_data) assert response.status_code == 200 j = json.loads(response.content.decode()) updated_map = Map.objects.get(pk=anonymap.pk) - assert j['id'] == updated_map.pk + assert j["id"] == updated_map.pk -@pytest.mark.usefixtures('allow_anonymous') +@pytest.mark.usefixtures("allow_anonymous") def test_anonymous_delete(cookieclient, anonymap): - url = reverse('map_delete', args=(anonymap.pk, )) + url = reverse("map_delete", args=(anonymap.pk,)) response = cookieclient.post(url, {}, follow=True) assert response.status_code == 200 assert not Map.objects.filter(pk=anonymap.pk).count() # Test response is a json j = json.loads(response.content.decode()) - assert 'redirect' in j + assert "redirect" in j -@pytest.mark.usefixtures('allow_anonymous') +@pytest.mark.usefixtures("allow_anonymous") def test_no_cookie_cant_delete(client, anonymap): - url = reverse('map_delete', args=(anonymap.pk, )) + url = reverse("map_delete", args=(anonymap.pk,)) response = client.post(url, {}, follow=True) assert response.status_code == 403 -@pytest.mark.usefixtures('allow_anonymous') +@pytest.mark.usefixtures("allow_anonymous") def test_anonymous_edit_url(cookieclient, anonymap): url = anonymap.get_anonymous_edit_url() - canonical = reverse('map', kwargs={'pk': anonymap.pk, - 'slug': anonymap.slug}) + canonical = reverse("map", kwargs={"pk": anonymap.pk, "slug": anonymap.slug}) response = cookieclient.get(url) assert response.status_code == 302 - assert response['Location'] == canonical + assert response["Location"] == canonical key, value = anonymap.signed_cookie_elements assert key in cookieclient.cookies -@pytest.mark.usefixtures('allow_anonymous') +@pytest.mark.usefixtures("allow_anonymous") def test_sha1_anonymous_edit_url(cookieclient, anonymap): - signer = Signer(algorithm='sha1') + signer = Signer(algorithm="sha1") signature = signer.sign(anonymap.pk) - url = reverse('map_anonymous_edit_url', kwargs={'signature': signature}) - canonical = reverse('map', kwargs={'pk': anonymap.pk, - 'slug': anonymap.slug}) + url = reverse("map_anonymous_edit_url", kwargs={"signature": signature}) + canonical = reverse("map", kwargs={"pk": anonymap.pk, "slug": anonymap.slug}) response = cookieclient.get(url) assert response.status_code == 302 - assert response['Location'] == canonical + assert response["Location"] == canonical key, value = anonymap.signed_cookie_elements assert key in cookieclient.cookies -@pytest.mark.usefixtures('allow_anonymous') +@pytest.mark.usefixtures("allow_anonymous") def test_bad_anonymous_edit_url_should_return_403(cookieclient, anonymap): url = anonymap.get_anonymous_edit_url() url = reverse( - 'map_anonymous_edit_url', - kwargs={'signature': "%s:badsignature" % anonymap.pk} + "map_anonymous_edit_url", kwargs={"signature": "%s:badsignature" % anonymap.pk} ) response = cookieclient.get(url) assert response.status_code == 403 -@pytest.mark.usefixtures('allow_anonymous') -def test_clone_anonymous_map_should_not_be_possible_if_user_is_not_allowed(client, anonymap, user): # noqa +@pytest.mark.usefixtures("allow_anonymous") +def test_clone_anonymous_map_should_not_be_possible_if_user_is_not_allowed( + client, anonymap, user +): # noqa assert Map.objects.count() == 1 - url = reverse('map_clone', kwargs={'map_id': anonymap.pk}) + url = reverse("map_clone", kwargs={"map_id": anonymap.pk}) anonymap.edit_status = anonymap.OWNER anonymap.save() response = client.post(url) @@ -441,24 +438,26 @@ def test_clone_anonymous_map_should_not_be_possible_if_user_is_not_allowed(clien assert Map.objects.count() == 1 -@pytest.mark.usefixtures('allow_anonymous') -def test_clone_map_should_be_possible_if_edit_status_is_anonymous(client, anonymap): # noqa +@pytest.mark.usefixtures("allow_anonymous") +def test_clone_map_should_be_possible_if_edit_status_is_anonymous( + client, anonymap +): # noqa assert Map.objects.count() == 1 - url = reverse('map_clone', kwargs={'map_id': anonymap.pk}) + url = reverse("map_clone", kwargs={"map_id": anonymap.pk}) anonymap.edit_status = anonymap.ANONYMOUS anonymap.save() response = client.post(url) assert response.status_code == 200 assert Map.objects.count() == 2 - clone = Map.objects.latest('pk') + clone = Map.objects.latest("pk") assert clone.pk != anonymap.pk - assert clone.name == 'Clone of ' + anonymap.name + assert clone.name == "Clone of " + anonymap.name assert clone.owner is None -@pytest.mark.usefixtures('allow_anonymous') +@pytest.mark.usefixtures("allow_anonymous") def test_anyone_can_access_anonymous_map(cookieclient, anonymap): - url = reverse('map', args=(anonymap.slug, anonymap.pk)) + url = reverse("map", args=(anonymap.slug, anonymap.pk)) anonymap.share_status = anonymap.PUBLIC response = cookieclient.get(url) assert response.status_code == 200 @@ -470,9 +469,9 @@ def test_anyone_can_access_anonymous_map(cookieclient, anonymap): assert response.status_code == 200 -@pytest.mark.usefixtures('allow_anonymous') +@pytest.mark.usefixtures("allow_anonymous") def test_map_attach_owner(cookieclient, anonymap, user): - url = reverse('map_attach_owner', kwargs={'map_id': anonymap.pk}) + url = reverse("map_attach_owner", kwargs={"map_id": anonymap.pk}) cookieclient.login(username=user.username, password="123123") assert anonymap.owner is None response = cookieclient.post(url) @@ -481,17 +480,17 @@ def test_map_attach_owner(cookieclient, anonymap, user): assert map.owner == user -@pytest.mark.usefixtures('allow_anonymous') +@pytest.mark.usefixtures("allow_anonymous") def test_map_attach_owner_not_logged_in(cookieclient, anonymap, user): - url = reverse('map_attach_owner', kwargs={'map_id': anonymap.pk}) + url = reverse("map_attach_owner", kwargs={"map_id": anonymap.pk}) assert anonymap.owner is None response = cookieclient.post(url) assert response.status_code == 403 -@pytest.mark.usefixtures('allow_anonymous') +@pytest.mark.usefixtures("allow_anonymous") def test_map_attach_owner_with_already_an_owner(cookieclient, map, user): - url = reverse('map_attach_owner', kwargs={'map_id': map.pk}) + url = reverse("map_attach_owner", kwargs={"map_id": map.pk}) cookieclient.login(username=user.username, password="123123") assert map.owner assert map.owner != user @@ -500,7 +499,7 @@ def test_map_attach_owner_with_already_an_owner(cookieclient, map, user): def test_map_attach_owner_anonymous_not_allowed(cookieclient, anonymap, user): - url = reverse('map_attach_owner', kwargs={'map_id': anonymap.pk}) + url = reverse("map_attach_owner", kwargs={"map_id": anonymap.pk}) cookieclient.login(username=user.username, password="123123") assert anonymap.owner is None response = cookieclient.post(url) @@ -524,11 +523,11 @@ def test_map_attach_owner_anonymous_not_allowed(cookieclient, anonymap, user): def test_create_readonly(client, user, post_data, settings): settings.UMAP_READONLY = True - url = reverse('map_create') + url = reverse("map_create") client.login(username=user.username, password="123123") response = client.post(url, post_data) assert response.status_code == 403 - assert response.content == b'Site is readonly for maintenance' + assert response.content == b"Site is readonly for maintenance" def test_search(client, map): @@ -542,7 +541,7 @@ def test_search(client, map): def test_authenticated_user_can_star_map(client, map, user): - url = reverse('map_star', args=(map.pk,)) + url = reverse("map_star", args=(map.pk,)) client.login(username=user.username, password="123123") assert Star.objects.filter(by=user).count() == 0 response = client.post(url) @@ -551,7 +550,7 @@ def test_authenticated_user_can_star_map(client, map, user): def test_anonymous_cannot_star_map(client, map): - url = reverse('map_star', args=(map.pk,)) + url = reverse("map_star", args=(map.pk,)) assert Star.objects.count() == 0 response = client.post(url) assert response.status_code == 302 @@ -560,12 +559,12 @@ def test_anonymous_cannot_star_map(client, map): def test_user_can_see_their_star(client, map, user): - url = reverse('map_star', args=(map.pk,)) + url = reverse("map_star", args=(map.pk,)) client.login(username=user.username, password="123123") assert Star.objects.filter(by=user).count() == 0 response = client.post(url) assert response.status_code == 200 - url = reverse('user_stars', args=(user.username,)) + url = reverse("user_stars", args=(user.username,)) response = client.get(url) assert response.status_code == 200 assert map.name in response.content.decode() From 4ba1946868b0bea952d0481275deeea0cfc4de35 Mon Sep 17 00:00:00 2001 From: David Larlet Date: Fri, 2 Jun 2023 11:54:44 -0400 Subject: [PATCH 15/15] Fix test about subject Introduced in 5460876ff40dea180848fd4a478a9399b86b3b45 --- umap/tests/test_map_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umap/tests/test_map_views.py b/umap/tests/test_map_views.py index 41227d85..2b7489ec 100644 --- a/umap/tests/test_map_views.py +++ b/umap/tests/test_map_views.py @@ -596,4 +596,4 @@ def test_can_send_link_on_anonymous_map_with_cookie(cookieclient, anonymap): 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" + assert mail.outbox[0].subject == "The uMap edit link for your map: test map"