Merge pull request #1102 from umap-project/mail-link

Add a button to send edit link by email in anonymous mode
This commit is contained in:
Yohan Boniface 2023-06-02 23:32:34 +02:00 committed by GitHub
commit 6e0c4723a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 345 additions and 179 deletions

View file

@ -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); 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. 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
```

View file

@ -25,6 +25,10 @@ class FlatErrorList(ErrorList):
return u''.join([e for e in self]) return u''.join([e for e in self])
class SendLinkForm(forms.Form):
email = forms.EmailField()
class UpdateMapPermissionsForm(forms.ModelForm): class UpdateMapPermissionsForm(forms.ModelForm):
class Meta: class Meta:
@ -36,8 +40,7 @@ class AnonymousMapPermissionsForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(AnonymousMapPermissionsForm, self).__init__(*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') % self.instance.get_anonymous_edit_url()
help_text = _('Secret edit link is %s') % full_secret_link
self.fields['edit_status'].help_text = _(help_text) self.fields['edit_status'].help_text = _(help_text)
STATUS = ( STATUS = (

View file

@ -1,7 +1,6 @@
import sys import sys
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.conf import settings
from umap.models import Map from umap.models import Map
@ -25,4 +24,4 @@ class Command(BaseCommand):
self.abort('Map with pk {} not found'.format(pk)) self.abort('Map with pk {} not found'.format(pk))
if map_.owner: if map_.owner:
self.abort('Map is not anonymous (owner: {})'.format(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())

View file

@ -162,7 +162,8 @@ class Map(NamedModel):
def get_anonymous_edit_url(self): def get_anonymous_edit_url(self):
signer = Signer() signer = Signer()
signature = signer.sign(self.pk) 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): def is_anonymous_owner(self, request):
if self.owner: if self.owner:

View file

@ -114,6 +114,9 @@ INSTALLED_APPS = (
) )
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 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 # Calculation of directories relative to the project module location
# ============================================================================= # =============================================================================

View file

@ -734,7 +734,7 @@ input[type=hidden].blur + .button {
.umap-alert .umap-action { .umap-alert .umap-action {
margin-left: 10px; margin-left: 10px;
background-color: #fff; background-color: #fff;
color: #999; color: #000;
padding: 5px; padding: 5px;
border-radius: 4px; border-radius: 4px;
} }
@ -748,6 +748,10 @@ input[type=hidden].blur + .button {
.umap-alert .error .umap-action:hover { .umap-alert .error .umap-action:hover {
color: #fff; color: #fff;
} }
.umap-alert input {
padding: 5px;
border-radius: 4px;
}
/* *********** */ /* *********** */
/* Tooltip */ /* Tooltip */

View file

@ -1332,15 +1332,75 @@ L.U.Map.include({
formData.append('name', this.options.name) formData.append('name', this.options.name)
formData.append('center', JSON.stringify(this.geometry())) formData.append('center', JSON.stringify(this.geometry()))
formData.append('settings', JSON.stringify(geojson)) 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(), { this.post(this.getSaveUrl(), {
data: formData, data: formData,
context: this, context: this,
callback: function (data) { callback: function (data) {
let duration = 3000 let duration = 3000,
alert = { content: L._('Map has been saved!'), level: 'info' }
if (!this.options.umap_id) { if (!this.options.umap_id) {
duration = 100000 // we want a longer message at map creation (TODO UGLY) alert.content = L._('Congratulations, your map has been created!')
this.options.umap_id = data.id this.options.umap_id = data.id
this.permissions.setOptions(data.permissions) this.permissions.setOptions(data.permissions)
if (
data.permissions &&
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:'
) + `<br>${data.permissions.anonymous_edit_url}`
alert.actions = [
{
label: L._('Send me the link'),
input: L._('Email'),
callback: this.sendEditLink,
callbackContext: this,
},
{
label: L._('Copy link'),
callback: () => {
copyToClipboard(data.permissions.anonymous_edit_url)
this.ui.alert({
content: L._('Secret edit link copied to clipboard!'),
level: 'info',
})
},
callbackContext: this,
},
]
}
} else if (!this.permissions.isDirty) { } else if (!this.permissions.isDirty) {
// Do not override local changes to permissions, // Do not override local changes to permissions,
// but update in case some other editors changed them in the meantime. // but update in case some other editors changed them in the meantime.
@ -1350,11 +1410,10 @@ L.U.Map.include({
if (history && history.pushState) if (history && history.pushState)
history.pushState({}, this.options.name, data.url) history.pushState({}, this.options.name, data.url)
else window.location = data.url else window.location = data.url
if (data.info) msg = data.info alert.content = data.info || alert.content
else msg = L._('Map has been saved!')
this.once('saved', function () { this.once('saved', function () {
this.isDirty = false this.isDirty = false
this.ui.alert({ content: msg, level: 'info', duration: duration }) this.ui.alert(alert)
}) })
this.ui.closePanel() this.ui.closePanel()
this.permissions.save() this.permissions.save()
@ -1362,6 +1421,20 @@ L.U.Map.include({
}) })
}, },
sendEditLink: function () {
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,
})
},
getEditUrl: function () { getEditUrl: function () {
return L.Util.template(this.options.urls.map_update, { return L.Util.template(this.options.urls.map_update, {
map_id: this.options.umap_id, map_id: this.options.umap_id,

View file

@ -75,7 +75,6 @@ L.U.UI = L.Evented.extend({
}, },
popAlert: function (e) { popAlert: function (e) {
const self = this
if (!e) { if (!e) {
if (this.ALERTS.length) e = this.ALERTS.pop() if (this.ALERTS.length) e = this.ALERTS.pop()
else return else return
@ -85,8 +84,8 @@ L.U.UI = L.Evented.extend({
this._alert.innerHTML = '' this._alert.innerHTML = ''
L.DomUtil.addClass(this.parent, 'umap-alert') L.DomUtil.addClass(this.parent, 'umap-alert')
L.DomUtil.addClass(this._alert, level_class) L.DomUtil.addClass(this._alert, level_class)
function close() { const close = () => {
if (timeoutID !== this.ALERT_ID) { if (timeoutID && timeoutID !== this.ALERT_ID) {
return return
} // Another alert has been forced } // Another alert has been forced
this._alert.innerHTML = '' this._alert.innerHTML = ''
@ -108,26 +107,32 @@ L.U.UI = L.Evented.extend({
) )
L.DomUtil.add('div', '', this._alert, e.content) L.DomUtil.add('div', '', this._alert, e.content)
if (e.actions) { if (e.actions) {
let action, el let action, el, input
for (let i = 0; i < e.actions.length; i++) { for (let i = 0; i < e.actions.length; i++) {
action = e.actions[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 = L.DomUtil.element('a', { className: 'umap-action' }, this._alert)
el.href = '#' el.href = '#'
el.textContent = action.label el.textContent = action.label
L.DomEvent.on(el, 'click', L.DomEvent.stop).on(el, 'click', close, this) L.DomEvent.on(el, 'click', L.DomEvent.stop)
if (action.callback) if (action.callback) {
L.DomEvent.on( L.DomEvent.on(el, 'click', action.callback, action.callbackContext || this.map)
el, }
'click', L.DomEvent.on(el, 'click', close, this)
action.callback,
action.callbackContext || this.map
)
} }
} }
self.ALERT_ID = timeoutID = window.setTimeout( if (e.duration !== Infinity) {
this.ALERT_ID = timeoutID = window.setTimeout(
L.bind(close, this), L.bind(close, this),
e.duration || 3000 e.duration || 3000
) )
}
}, },
tooltip: function (e) { tooltip: function (e) {

View file

@ -4,6 +4,8 @@ from umap.settings.base import * # pylint: disable=W0614,W0401
SECRET_KEY = "justfortests" SECRET_KEY = "justfortests"
COMPRESS_ENABLED = False COMPRESS_ENABLED = False
FROM_EMAIL = "test@test.org"
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
if "TRAVIS" in os.environ: if "TRAVIS" in os.environ:
DATABASES = { DATABASES = {

View file

@ -2,9 +2,10 @@ import json
import pytest import pytest
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core import mail
from django.urls import reverse from django.urls import reverse
from django.core.signing import Signer from django.core.signing import Signer
from umap.models import DataLayer, Map, Star from umap.models import DataLayer, Map, Star
from .base import login_required from .base import login_required
@ -16,50 +17,45 @@ User = get_user_model()
@pytest.fixture @pytest.fixture
def post_data(): def post_data():
return { return {
'name': 'name', "name": "name",
'center': '{"type":"Point","coordinates":[13.447265624999998,48.94415123418794]}', # noqa "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 "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): def test_create(client, user, post_data):
url = reverse('map_create') url = reverse("map_create")
# POST only mendatory fields # POST only mendatory fields
name = 'test-map-with-new-name' name = "test-map-with-new-name"
post_data['name'] = name post_data["name"] = name
client.login(username=user.username, password="123123") client.login(username=user.username, password="123123")
response = client.post(url, post_data) response = client.post(url, post_data)
assert response.status_code == 200 assert response.status_code == 200
j = json.loads(response.content.decode()) j = json.loads(response.content.decode())
created_map = Map.objects.latest('pk') created_map = Map.objects.latest("pk")
assert j['id'] == created_map.pk assert j["id"] == created_map.pk
assert created_map.name == name assert created_map.name == name
assert created_map.center.x == 13.447265624999998 assert created_map.center.x == 13.447265624999998
assert created_map.center.y == 48.94415123418794 assert created_map.center.y == 48.94415123418794
assert j['permissions'] == { assert j["permissions"] == {
'edit_status': 3, "edit_status": 3,
'share_status': 1, "share_status": 1,
'owner': { "owner": {"id": user.pk, "name": "Joe", "url": "/en/user/Joe/"},
'id': user.pk, "editors": [],
'name': 'Joe', "anonymous_edit_url": created_map.get_anonymous_edit_url(),
'url': '/en/user/Joe/'
},
'editors': [],
'anonymous_edit_url': ('http://umap.org'
+ created_map.get_anonymous_edit_url())
} }
def test_map_create_permissions(client, settings): def test_map_create_permissions(client, settings):
settings.UMAP_ALLOW_ANONYMOUS = False settings.UMAP_ALLOW_ANONYMOUS = False
url = reverse('map_create') url = reverse("map_create")
# POST anonymous # POST anonymous
response = client.post(url, {}) response = client.post(url, {})
assert login_required(response) assert login_required(response)
def test_map_update_access(client, map, user): 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 # GET anonymous
response = client.get(url) response = client.get(url)
assert login_required(response) 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): 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 # GET anonymous
response = client.get(url) response = client.get(url)
assert login_required(response) assert login_required(response)
@ -95,22 +91,22 @@ def test_map_update_permissions_access(client, map, user):
def test_update(client, map, post_data): 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 # POST only mendatory fields
name = 'new map name' name = "new map name"
post_data['name'] = name post_data["name"] = name
client.login(username=map.owner.username, password="123123") client.login(username=map.owner.username, password="123123")
response = client.post(url, post_data) response = client.post(url, post_data)
assert response.status_code == 200 assert response.status_code == 200
j = json.loads(response.content.decode()) j = json.loads(response.content.decode())
assert 'html' not in j assert "html" not in j
updated_map = Map.objects.get(pk=map.pk) 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 assert updated_map.name == name
def test_delete(client, map, datalayer): 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") client.login(username=map.owner.username, password="123123")
response = client.post(url, {}, follow=True) response = client.post(url, {}, follow=True)
assert response.status_code == 200 assert response.status_code == 200
@ -120,61 +116,58 @@ def test_delete(client, map, datalayer):
assert User.objects.filter(pk=map.owner.pk).exists() assert User.objects.filter(pk=map.owner.pk).exists()
# Test response is a json # Test response is a json
j = json.loads(response.content.decode()) j = json.loads(response.content.decode())
assert 'redirect' in j assert "redirect" in j
def test_wrong_slug_should_redirect_to_canonical(client, map): def test_wrong_slug_should_redirect_to_canonical(client, map):
url = reverse('map', kwargs={'pk': map.pk, 'slug': 'wrong-slug'}) url = reverse("map", kwargs={"pk": map.pk, "slug": "wrong-slug"})
canonical = reverse('map', kwargs={'pk': map.pk, canonical = reverse("map", kwargs={"pk": map.pk, "slug": map.slug})
'slug': map.slug})
response = client.get(url) response = client.get(url)
assert response.status_code == 301 assert response.status_code == 301
assert response['Location'] == canonical assert response["Location"] == canonical
def test_wrong_slug_should_redirect_with_query_string(client, map): 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) url = "{}?allowEdit=0".format(url)
canonical = reverse('map', kwargs={'pk': map.pk, canonical = reverse("map", kwargs={"pk": map.pk, "slug": map.slug})
'slug': map.slug})
canonical = "{}?allowEdit=0".format(canonical) canonical = "{}?allowEdit=0".format(canonical)
response = client.get(url) response = client.get(url)
assert response.status_code == 301 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): 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) url = "{}?allowEdit=0".format(url)
response = client.get(url) response = client.get(url)
assert response.status_code == 200 assert response.status_code == 200
def test_short_url_should_redirect_to_canonical(client, map): def test_short_url_should_redirect_to_canonical(client, map):
url = reverse('map_short_url', kwargs={'pk': map.pk}) url = reverse("map_short_url", kwargs={"pk": map.pk})
canonical = reverse('map', kwargs={'pk': map.pk, canonical = reverse("map", kwargs={"pk": map.pk, "slug": map.slug})
'slug': map.slug})
response = client.get(url) response = client.get(url)
assert response.status_code == 301 assert response.status_code == 301
assert response['Location'] == canonical assert response["Location"] == canonical
def test_clone_map_should_create_a_new_instance(client, map): def test_clone_map_should_create_a_new_instance(client, map):
assert Map.objects.count() == 1 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") client.login(username=map.owner.username, password="123123")
response = client.post(url) response = client.post(url)
assert response.status_code == 200 assert response.status_code == 200
assert Map.objects.count() == 2 assert Map.objects.count() == 2
clone = Map.objects.latest('pk') clone = Map.objects.latest("pk")
assert clone.pk != map.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): def test_user_not_allowed_should_not_clone_map(client, map, user, settings):
settings.UMAP_ALLOW_ANONYMOUS = False settings.UMAP_ALLOW_ANONYMOUS = False
assert Map.objects.count() == 1 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.edit_status = map.OWNER
map.save() map.save()
response = client.post(url) 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): 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.edit_status = map.EDITORS
map.editors.add(user) map.editors.add(user)
map.save() map.save()
@ -199,32 +192,32 @@ def test_clone_should_set_cloner_as_owner(client, map, user):
response = client.post(url) response = client.post(url)
assert response.status_code == 200 assert response.status_code == 200
assert Map.objects.count() == 2 assert Map.objects.count() == 2
clone = Map.objects.latest('pk') clone = Map.objects.latest("pk")
assert clone.pk != map.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 assert clone.owner == user
def test_map_creation_should_allow_unicode_names(client, map, post_data): def test_map_creation_should_allow_unicode_names(client, map, post_data):
url = reverse('map_create') url = reverse("map_create")
# POST only mendatory fields # POST only mendatory fields
name = u'Академический' name = "Академический"
post_data['name'] = name post_data["name"] = name
client.login(username=map.owner.username, password="123123") client.login(username=map.owner.username, password="123123")
response = client.post(url, post_data) response = client.post(url, post_data)
assert response.status_code == 200 assert response.status_code == 200
j = json.loads(response.content.decode()) j = json.loads(response.content.decode())
created_map = Map.objects.latest('pk') created_map = Map.objects.latest("pk")
assert j['id'] == created_map.pk assert j["id"] == created_map.pk
assert created_map.name == name assert created_map.name == name
# Lower case of the russian original name # Lower case of the russian original name
# self.assertEqual(created_map.slug, u"академический") # self.assertEqual(created_map.slug, u"академический")
# for now we fallback to "map", see unicode_name branch # 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): 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.share_status = map.PUBLIC
map.save() map.save()
response = client.get(url) 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): 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.share_status = map.OPEN
map.save() map.save()
response = client.get(url) 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): 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.share_status = map.PRIVATE
map.save() map.save()
response = client.get(url) 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): 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.share_status = map.PRIVATE
map.save() map.save()
client.login(username=map.owner.username, password="123123") 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): 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.share_status = map.PRIVATE
map.editors.add(user) map.editors.add(user)
map.save() 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): 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.share_status = map.BLOCKED
map.save() map.save()
response = client.get(url) 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): 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.share_status = map.BLOCKED
map.save() map.save()
client.login(username=map.owner.username, password="123123") 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 assert response.status_code == 403
def test_non_editor_cannot_access_map_if_share_status_private(client, map, user): # noqa def test_non_editor_cannot_access_map_if_share_status_private(
url = reverse('map', args=(map.slug, map.pk)) client, map, user
): # noqa
url = reverse("map", args=(map.slug, map.pk))
map.share_status = map.PRIVATE map.share_status = map.PRIVATE
map.save() map.save()
client.login(username=user.username, password="123123") 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): 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) response = client.get(url)
j = json.loads(response.content.decode()) j = json.loads(response.content.decode())
assert 'json' in response['content-type'] assert "json" in response["content-type"]
assert 'type' in j assert "type" in j
def test_only_owner_can_delete(client, map, user): def test_only_owner_can_delete(client, map, user):
map.editors.add(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") client.login(username=user.username, password="123123")
response = client.post(url, {}, follow=True) response = client.post(url, {}, follow=True)
assert response.status_code == 403 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.editors.add(user)
map.edit_status = map.EDITORS map.edit_status = map.EDITORS
map.save() 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") client.login(username=user.username, password="123123")
response = client.get(url) 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): 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.edit_status = map.ANONYMOUS
map.save() map.save()
client.login(username=user.username, password="123123") client.login(username=user.username, password="123123")
url = reverse('map_update', kwargs={'map_id': map.pk}) url = reverse("map_update", kwargs={"map_id": map.pk})
new_name = 'this is my new name' new_name = "this is my new name"
data = { data = {
'center': '{"type":"Point","coordinates":[13.447265624999998,48.94415123418794]}', # noqa "center": '{"type":"Point","coordinates":[13.447265624999998,48.94415123418794]}', # noqa
'name': new_name "name": new_name,
} }
response = client.post(url, data) response = client.post(url, data)
assert response.status_code == 200 assert response.status_code == 200
assert Map.objects.get(pk=map.pk).name == new_name 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): def test_anonymous_create(cookieclient, post_data):
url = reverse('map_create') url = reverse("map_create")
# POST only mendatory fields # POST only mendatory fields
name = 'test-map-with-new-name' name = "test-map-with-new-name"
post_data['name'] = name post_data["name"] = name
response = cookieclient.post(url, post_data) response = cookieclient.post(url, post_data)
assert response.status_code == 200 assert response.status_code == 200
j = json.loads(response.content.decode()) j = json.loads(response.content.decode())
created_map = Map.objects.latest('pk') created_map = Map.objects.latest("pk")
assert j['id'] == created_map.pk assert j["id"] == created_map.pk
assert (created_map.get_anonymous_edit_url() assert (
in j['permissions']['anonymous_edit_url']) created_map.get_anonymous_edit_url() in j["permissions"]["anonymous_edit_url"]
)
assert created_map.name == name assert created_map.name == name
key, value = created_map.signed_cookie_elements key, value = created_map.signed_cookie_elements
assert key in cookieclient.cookies 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 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) response = client.post(url, post_data)
assert response.status_code == 403 assert response.status_code == 403
@pytest.mark.usefixtures('allow_anonymous') @pytest.mark.usefixtures("allow_anonymous")
def test_anonymous_update_with_cookie_should_work(cookieclient, anonymap, post_data): # noqa def test_anonymous_update_with_cookie_should_work(
url = reverse('map_update', kwargs={'map_id': anonymap.pk}) cookieclient, anonymap, post_data
): # noqa
url = reverse("map_update", kwargs={"map_id": anonymap.pk})
# POST only mendatory fields # POST only mendatory fields
name = 'new map name' name = "new map name"
post_data['name'] = name post_data["name"] = name
response = cookieclient.post(url, post_data) response = cookieclient.post(url, post_data)
assert response.status_code == 200 assert response.status_code == 200
j = json.loads(response.content.decode()) j = json.loads(response.content.decode())
updated_map = Map.objects.get(pk=anonymap.pk) 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): 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) response = cookieclient.post(url, {}, follow=True)
assert response.status_code == 200 assert response.status_code == 200
assert not Map.objects.filter(pk=anonymap.pk).count() assert not Map.objects.filter(pk=anonymap.pk).count()
# Test response is a json # Test response is a json
j = json.loads(response.content.decode()) 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): 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) response = client.post(url, {}, follow=True)
assert response.status_code == 403 assert response.status_code == 403
@pytest.mark.usefixtures('allow_anonymous') @pytest.mark.usefixtures("allow_anonymous")
def test_anonymous_edit_url(cookieclient, anonymap): def test_anonymous_edit_url(cookieclient, anonymap):
url = anonymap.get_anonymous_edit_url() url = anonymap.get_anonymous_edit_url()
canonical = reverse('map', kwargs={'pk': anonymap.pk, canonical = reverse("map", kwargs={"pk": anonymap.pk, "slug": anonymap.slug})
'slug': anonymap.slug})
response = cookieclient.get(url) response = cookieclient.get(url)
assert response.status_code == 302 assert response.status_code == 302
assert response['Location'] == canonical assert response["Location"] == canonical
key, value = anonymap.signed_cookie_elements key, value = anonymap.signed_cookie_elements
assert key in cookieclient.cookies assert key in cookieclient.cookies
@pytest.mark.usefixtures('allow_anonymous') @pytest.mark.usefixtures("allow_anonymous")
def test_sha1_anonymous_edit_url(cookieclient, anonymap): def test_sha1_anonymous_edit_url(cookieclient, anonymap):
signer = Signer(algorithm='sha1') signer = Signer(algorithm="sha1")
signature = signer.sign(anonymap.pk) signature = signer.sign(anonymap.pk)
url = reverse('map_anonymous_edit_url', kwargs={'signature': signature}) url = reverse("map_anonymous_edit_url", kwargs={"signature": signature})
canonical = reverse('map', kwargs={'pk': anonymap.pk, canonical = reverse("map", kwargs={"pk": anonymap.pk, "slug": anonymap.slug})
'slug': anonymap.slug})
response = cookieclient.get(url) response = cookieclient.get(url)
assert response.status_code == 302 assert response.status_code == 302
assert response['Location'] == canonical assert response["Location"] == canonical
key, value = anonymap.signed_cookie_elements key, value = anonymap.signed_cookie_elements
assert key in cookieclient.cookies 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): def test_bad_anonymous_edit_url_should_return_403(cookieclient, anonymap):
url = anonymap.get_anonymous_edit_url() url = anonymap.get_anonymous_edit_url()
url = reverse( url = reverse(
'map_anonymous_edit_url', "map_anonymous_edit_url", kwargs={"signature": "%s:badsignature" % anonymap.pk}
kwargs={'signature': "%s:badsignature" % anonymap.pk}
) )
response = cookieclient.get(url) response = cookieclient.get(url)
assert response.status_code == 403 assert response.status_code == 403
@pytest.mark.usefixtures('allow_anonymous') @pytest.mark.usefixtures("allow_anonymous")
def test_clone_anonymous_map_should_not_be_possible_if_user_is_not_allowed(client, anonymap, user): # noqa def test_clone_anonymous_map_should_not_be_possible_if_user_is_not_allowed(
client, anonymap, user
): # noqa
assert Map.objects.count() == 1 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.edit_status = anonymap.OWNER
anonymap.save() anonymap.save()
response = client.post(url) 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 assert Map.objects.count() == 1
@pytest.mark.usefixtures('allow_anonymous') @pytest.mark.usefixtures("allow_anonymous")
def test_clone_map_should_be_possible_if_edit_status_is_anonymous(client, anonymap): # noqa def test_clone_map_should_be_possible_if_edit_status_is_anonymous(
client, anonymap
): # noqa
assert Map.objects.count() == 1 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.edit_status = anonymap.ANONYMOUS
anonymap.save() anonymap.save()
response = client.post(url) response = client.post(url)
assert response.status_code == 200 assert response.status_code == 200
assert Map.objects.count() == 2 assert Map.objects.count() == 2
clone = Map.objects.latest('pk') clone = Map.objects.latest("pk")
assert clone.pk != anonymap.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 assert clone.owner is None
@pytest.mark.usefixtures('allow_anonymous') @pytest.mark.usefixtures("allow_anonymous")
def test_anyone_can_access_anonymous_map(cookieclient, anonymap): 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 anonymap.share_status = anonymap.PUBLIC
response = cookieclient.get(url) response = cookieclient.get(url)
assert response.status_code == 200 assert response.status_code == 200
@ -470,9 +469,9 @@ def test_anyone_can_access_anonymous_map(cookieclient, anonymap):
assert response.status_code == 200 assert response.status_code == 200
@pytest.mark.usefixtures('allow_anonymous') @pytest.mark.usefixtures("allow_anonymous")
def test_map_attach_owner(cookieclient, anonymap, user): 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") cookieclient.login(username=user.username, password="123123")
assert anonymap.owner is None assert anonymap.owner is None
response = cookieclient.post(url) response = cookieclient.post(url)
@ -481,17 +480,17 @@ def test_map_attach_owner(cookieclient, anonymap, user):
assert map.owner == 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): 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 assert anonymap.owner is None
response = cookieclient.post(url) response = cookieclient.post(url)
assert response.status_code == 403 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): 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") cookieclient.login(username=user.username, password="123123")
assert map.owner assert map.owner
assert map.owner != user 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): 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") cookieclient.login(username=user.username, password="123123")
assert anonymap.owner is None assert anonymap.owner is None
response = cookieclient.post(url) 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): def test_create_readonly(client, user, post_data, settings):
settings.UMAP_READONLY = True settings.UMAP_READONLY = True
url = reverse('map_create') url = reverse("map_create")
client.login(username=user.username, password="123123") client.login(username=user.username, password="123123")
response = client.post(url, post_data) response = client.post(url, post_data)
assert response.status_code == 403 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): def test_search(client, map):
@ -542,7 +541,7 @@ def test_search(client, map):
def test_authenticated_user_can_star_map(client, map, user): 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") client.login(username=user.username, password="123123")
assert Star.objects.filter(by=user).count() == 0 assert Star.objects.filter(by=user).count() == 0
response = client.post(url) 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): 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 assert Star.objects.count() == 0
response = client.post(url) response = client.post(url)
assert response.status_code == 302 assert response.status_code == 302
@ -560,12 +559,41 @@ def test_anonymous_cannot_star_map(client, map):
def test_user_can_see_their_star(client, map, user): 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") client.login(username=user.username, password="123123")
assert Star.objects.filter(by=user).count() == 0 assert Star.objects.filter(by=user).count() == 0
response = client.post(url) response = client.post(url)
assert response.status_code == 200 assert response.status_code == 200
url = reverse('user_stars', args=(user.username,)) url = reverse("user_stars", args=(user.username,))
response = client.get(url) response = client.get(url)
assert response.status_code == 200 assert response.status_code == 200
assert map.name in response.content.decode() 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 == "The uMap edit link for your map: test map"

View file

@ -99,8 +99,7 @@ i18n_urls += decorated_patterns(
name="map_star", name="map_star",
), ),
) )
i18n_urls += decorated_patterns( map_urls = [
[map_permissions_check, never_cache],
re_path( re_path(
r"^map/(?P<map_id>[\d]+)/update/settings/$", r"^map/(?P<map_id>[\d]+)/update/settings/$",
views.MapUpdate.as_view(), views.MapUpdate.as_view(),
@ -141,7 +140,16 @@ i18n_urls += decorated_patterns(
views.DataLayerDelete.as_view(), views.DataLayerDelete.as_view(),
name="datalayer_delete", name="datalayer_delete",
), ),
]
if settings.FROM_EMAIL:
map_urls.append(
re_path(
r"^map/(?P<map_id>[\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( urlpatterns += i18n_patterns(
re_path(r"^$", views.home, name="home"), re_path(r"^$", views.home, name="home"),
re_path( re_path(

View file

@ -14,6 +14,7 @@ from django.contrib.auth import logout as do_logout
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.gis.measure import D from django.contrib.gis.measure import D
from django.contrib.postgres.search import SearchQuery, SearchVector 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.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.core.signing import BadSignature, Signer from django.core.signing import BadSignature, Signer
from django.core.validators import URLValidator, ValidationError from django.core.validators import URLValidator, ValidationError
@ -35,7 +36,7 @@ from django.utils.translation import to_locale
from django.views.generic import DetailView, TemplateView, View from django.views.generic import DetailView, TemplateView, View
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView
from django.views.generic.detail import BaseDetailView 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 django.views.generic.list import ListView
from .forms import ( from .forms import (
@ -46,6 +47,7 @@ from .forms import (
DataLayerForm, DataLayerForm,
FlatErrorList, FlatErrorList,
MapSettingsForm, MapSettingsForm,
SendLinkForm,
UpdateMapPermissionsForm, UpdateMapPermissionsForm,
) )
from .models import DataLayer, Licence, Map, Pictogram, Star, TileLayer from .models import DataLayer, Licence, Map, Pictogram, Star, TileLayer
@ -472,13 +474,9 @@ class PermissionsMixin:
for editor in self.object.editors.all() for editor in self.object.editors.all()
] ]
if not self.object.owner and self.object.is_anonymous_owner(self.request): 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 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): class MapView(MapDetailMixin, PermissionsMixin, DetailView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -547,15 +545,7 @@ class MapCreate(FormLessEditMixin, PermissionsMixin, CreateView):
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
form.instance.owner = self.request.user form.instance.owner = self.request.user
self.object = form.save() 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 "
"another computer, please use this link: %(anonymous_url)s"
% {"anonymous_url": anonymous_url}
)
else:
msg = _("Congratulations, your map has been created!")
permissions = self.get_permissions() permissions = self.get_permissions()
# User does not have the cookie yet. # User does not have the cookie yet.
permissions["anonymous_edit_url"] = anonymous_url permissions["anonymous_edit_url"] = anonymous_url
@ -563,7 +553,6 @@ class MapCreate(FormLessEditMixin, PermissionsMixin, CreateView):
id=self.object.pk, id=self.object.pk,
url=self.object.get_absolute_url(), url=self.object.get_absolute_url(),
permissions=permissions, permissions=permissions,
info=msg,
) )
if not self.request.user.is_authenticated: if not self.request.user.is_authenticated:
key, value = self.object.signed_cookie_elements key, value = self.object.signed_cookie_elements
@ -628,6 +617,36 @@ class AttachAnonymousMap(View):
return simple_json_response() return simple_json_response()
class SendEditLink(FormLessEditMixin, FormView):
form_class = SendLinkForm
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()
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(
_("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],
fail_silently=False,
)
return simple_json_response(
info=_("Email sent to %(email)s" % {"email": email})
)
class MapDelete(DeleteView): class MapDelete(DeleteView):
model = Map model = Map
pk_url_kwarg = "map_id" pk_url_kwarg = "map_id"
@ -660,7 +679,7 @@ class MapClone(PermissionsMixin, View):
msg = _( msg = _(
"Your map has been cloned! If you want to edit this map from " "Your map has been cloned! If you want to edit this map from "
"another computer, please use this link: %(anonymous_url)s" "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: else:
msg = _("Congratulations, your map has been cloned!") msg = _("Congratulations, your map has been cloned!")