Merge pull request #1307 from umap-project/datalayer-editstatus

Allow to define permissions for each datalayer instead of for the whole map
This commit is contained in:
Yohan Boniface 2023-09-22 17:36:26 +02:00 committed by GitHub
commit ffae06aac7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1168 additions and 194 deletions

3
.gitignore vendored
View file

@ -17,4 +17,5 @@ __pycache__/
build/
dist/
*.egg-info/
playwright/.auth/
test-results/

View file

@ -53,7 +53,7 @@ dev = [
]
test = [
"factory-boy==3.2.1",
"playwright==1.37.0",
"playwright==1.38.0",
"pytest==6.2.5",
"pytest-django==4.5.2",
"pytest-playwright==0.4.2",

View file

@ -1,3 +1,3 @@
[pytest]
DJANGO_SETTINGS_MODULE=umap.tests.settings
addopts = "--pdbcls=IPython.terminal.debugger:Pdb --no-migrations"
addopts = --pdbcls=IPython.terminal.debugger:Pdb --no-migrations

View file

@ -26,9 +26,9 @@ def login_required_if_not_anonymous_allowed(view_func):
return wrapper
def map_permissions_check(view_func):
def can_edit_map(view_func):
"""
Used for URLs dealing with the map.
Used for URLs dealing with editing the map.
"""
@wraps(view_func)

View file

@ -8,8 +8,12 @@ from django.forms.utils import ErrorList
from .models import Map, DataLayer
DEFAULT_LATITUDE = settings.LEAFLET_LATITUDE if hasattr(settings, "LEAFLET_LATITUDE") else 51
DEFAULT_LONGITUDE = settings.LEAFLET_LONGITUDE if hasattr(settings, "LEAFLET_LONGITUDE") else 2
DEFAULT_LATITUDE = (
settings.LEAFLET_LATITUDE if hasattr(settings, "LEAFLET_LATITUDE") else 51
)
DEFAULT_LONGITUDE = (
settings.LEAFLET_LONGITUDE if hasattr(settings, "LEAFLET_LONGITUDE") else 2
)
DEFAULT_CENTER = Point(DEFAULT_LONGITUDE, DEFAULT_LATITUDE)
User = get_user_model()
@ -21,8 +25,8 @@ class FlatErrorList(ErrorList):
def flat(self):
if not self:
return u''
return u''.join([e for e in self])
return ""
return "".join([e for e in self])
class SendLinkForm(forms.Form):
@ -30,69 +34,79 @@ class SendLinkForm(forms.Form):
class UpdateMapPermissionsForm(forms.ModelForm):
class Meta:
model = Map
fields = ('edit_status', 'editors', 'share_status', 'owner')
fields = ("edit_status", "editors", "share_status", "owner")
class AnonymousMapPermissionsForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(AnonymousMapPermissionsForm, self).__init__(*args, **kwargs)
help_text = _('Secret edit link is %s') % self.instance.get_anonymous_edit_url()
self.fields['edit_status'].help_text = _(help_text)
STATUS = (
(Map.ANONYMOUS, _('Everyone can edit')),
(Map.OWNER, _('Only editable with secret edit link'))
(Map.OWNER, _("Only editable with secret edit link")),
(Map.ANONYMOUS, _("Everyone can edit")),
)
edit_status = forms.ChoiceField(choices=STATUS)
class Meta:
model = Map
fields = ('edit_status', )
fields = ("edit_status",)
class DataLayerForm(forms.ModelForm):
class Meta:
model = DataLayer
fields = ("geojson", "name", "display_on_load", "rank", "settings")
class DataLayerPermissionsForm(forms.ModelForm):
class Meta:
model = DataLayer
fields = ("edit_status",)
class AnonymousDataLayerPermissionsForm(forms.ModelForm):
STATUS = (
(DataLayer.INHERIT, _("Inherit")),
(DataLayer.OWNER, _("Only editable with secret edit link")),
(DataLayer.ANONYMOUS, _("Everyone can edit")),
)
edit_status = forms.ChoiceField(choices=STATUS)
class Meta:
model = DataLayer
fields = ('geojson', 'name', 'display_on_load', 'rank', 'settings')
fields = ("edit_status",)
class MapSettingsForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MapSettingsForm, self).__init__(*args, **kwargs)
self.fields['slug'].required = False
self.fields['center'].widget.map_srid = 4326
self.fields["slug"].required = False
self.fields["center"].widget.map_srid = 4326
def clean_slug(self):
slug = self.cleaned_data.get('slug', None)
name = self.cleaned_data.get('name', None)
slug = self.cleaned_data.get("slug", None)
name = self.cleaned_data.get("name", None)
if not slug and name:
# If name is empty, don't do nothing, validation will raise
# later on the process because name is required
self.cleaned_data['slug'] = slugify(name) or "map"
return self.cleaned_data['slug'][:50]
self.cleaned_data["slug"] = slugify(name) or "map"
return self.cleaned_data["slug"][:50]
else:
return ""
def clean_center(self):
if not self.cleaned_data['center']:
if not self.cleaned_data["center"]:
point = DEFAULT_CENTER
self.cleaned_data['center'] = point
return self.cleaned_data['center']
self.cleaned_data["center"] = point
return self.cleaned_data["center"]
class Meta:
fields = ('settings', 'name', 'center', 'slug')
fields = ("settings", "name", "center", "slug")
model = Map
class UserProfileForm(forms.ModelForm):
class Meta:
model = User
fields = ('username', 'first_name', 'last_name')
fields = ("username", "first_name", "last_name")

View file

@ -0,0 +1,26 @@
# Generated by Django 4.2.2 on 2023-09-19 06:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("umap", "0012_datalayer_settings"),
]
operations = [
migrations.AddField(
model_name="datalayer",
name="edit_status",
field=models.SmallIntegerField(
choices=[
(0, "Inherit"),
(1, "Everyone"),
(2, "Editors only"),
(3, "Owner only"),
],
default=0,
verbose_name="edit status",
),
),
]

View file

@ -202,7 +202,7 @@ class Map(NamedModel):
return settings.SITE_URL + path
def is_anonymous_owner(self, request):
if self.owner:
if not request or self.owner:
# edit cookies are only valid while map hasn't owner
return False
key, value = self.signed_cookie_elements
@ -216,17 +216,23 @@ class Map(NamedModel):
"""
Define if a user can edit or not the instance, according to his account
or the request.
In owner mode:
- only owner by default (OWNER)
- any editor if mode is EDITORS
- anyone otherwise (ANONYMOUS)
In anonymous owner mode:
- only owner (has ownership cookie) by default (OWNER)
- anyone otherwise (ANONYMOUS)
"""
can = False
if request and not self.owner:
if getattr(
settings, "UMAP_ALLOW_ANONYMOUS", False
) and self.is_anonymous_owner(request):
if settings.UMAP_ALLOW_ANONYMOUS and self.is_anonymous_owner(request):
can = True
if self.edit_status == self.ANONYMOUS:
can = True
elif not user.is_authenticated:
pass
elif user is None:
can = False
elif user == self.owner:
can = True
elif self.edit_status == self.EDITORS and user in self.editors.all():
@ -303,6 +309,17 @@ class DataLayer(NamedModel):
Layer to store Features in.
"""
INHERIT = 0
ANONYMOUS = 1
EDITORS = 2
OWNER = 3
EDIT_STATUS = (
(INHERIT, _("Inherit")),
(ANONYMOUS, _("Everyone")),
(EDITORS, _("Editors only")),
(OWNER, _("Owner only")),
)
map = models.ForeignKey(Map, on_delete=models.CASCADE)
description = models.TextField(blank=True, null=True, verbose_name=_("description"))
geojson = models.FileField(upload_to=upload_to, blank=True, null=True)
@ -315,6 +332,11 @@ class DataLayer(NamedModel):
settings = models.JSONField(
blank=True, null=True, verbose_name=_("settings"), default=dict
)
edit_status = models.SmallIntegerField(
choices=EDIT_STATUS,
default=INHERIT,
verbose_name=_("edit status"),
)
class Meta:
ordering = ("rank",)
@ -346,8 +368,7 @@ class DataLayer(NamedModel):
path.append(str(self.map.pk))
return os.path.join(*path)
@property
def metadata(self):
def metadata(self, user=None, request=None):
# Retrocompat: minimal settings for maps not saved after settings property
# has been introduced
obj = self.settings or {
@ -355,6 +376,8 @@ class DataLayer(NamedModel):
"displayOnLoad": self.display_on_load,
}
obj["id"] = self.pk
obj["permissions"] = {"edit_status": self.edit_status}
obj["editMode"] = "advanced" if self.can_edit(user, request) else 'disabled'
return obj
def clone(self, map_inst=None):
@ -413,6 +436,25 @@ class DataLayer(NamedModel):
if name.startswith(f'{self.pk}_') and name.endswith(".gz"):
self.geojson.storage.delete(os.path.join(root, name))
def can_edit(self, user=None, request=None):
"""
Define if a user can edit or not the instance, according to his account
or the request.
"""
if self.edit_status == self.INHERIT:
return self.map.can_edit(user, request)
can = False
if not self.map.owner:
if settings.UMAP_ALLOW_ANONYMOUS and self.map.is_anonymous_owner(request):
can = True
if self.edit_status == self.ANONYMOUS:
can = True
elif user is not None and user == self.map.owner:
can = True
elif self.edit_status == self.EDITORS and user in self.map.editors.all():
can = True
return can
class Star(models.Model):
at = models.DateTimeField(auto_now=True)

View file

@ -37,7 +37,6 @@
<g id="text4356-2" transform="translate(44,-124)" fill="#fff" stroke="#000" stroke-width=".1">
<path id="path4384-2" d="m35.742 999.44 3.0762-3.0762-3.0664-3.0664 1.1914-1.1914 3.0664 3.0664 3.0566-3.0566 1.1719 1.1816-3.0469 3.0566 3.0664 3.0664-1.1914 1.1914-3.0664-3.0664-3.0762 3.0762-1.1816-1.1816" fill="#fff" stroke="#000" stroke-width=".1"/>
</g>
<path id="table-0" d="m78 891.36v2h12v-2zm0 3v1h4v-1zm5 0v1h7v-1zm-5 2v1h4v-1zm5 0v1h7v-1zm-5 2v1h4v-1zm5 0v1h7v-1zm-5 2v1h4v-1zm5 0v1h7v-1z" fill="#b3b3b3"/>
<path id="path3684-2" d="m63.714 890.36-1.1428 1.1428 2.2857 2.2858 1.1428-1.1429zm-1.7143 1.7143-6.2857 6.2857 2.2857 2.2857 6.2857-6.2857zm-6.2857 6.2857-1.7143 4 4-1.7143z" fill="#b3b3b3"/>
<path id="table-5-8-8-6" d="m30 914.86v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1z" fill="#464646"/>
<path id="table-5-8-8-6-1" d="m54 914.86v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1z" fill="#f2f2f2"/>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="144" height="144" id="svg2" version="1.1" inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" sodipodi:docname="16.svg" inkscape:export-filename="/home/ybon/Code/js/Leaflet.Storage/src/img/16.png" inkscape:export-xdpi="89.996864" inkscape:export-ydpi="89.996864" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
<svg width="144" height="144" id="svg2" version="1.1" inkscape:version="1.3 (0e150ed6c4, 2023-07-21)" sodipodi:docname="16.svg" inkscape:export-filename="/home/ybon/Code/js/Leaflet.Storage/src/img/16.png" inkscape:export-xdpi="89.996864" inkscape:export-ydpi="89.996864" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs id="defs4" />
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="6.4903658" inkscape:cx="86.975067" inkscape:cy="60.628324" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="true" inkscape:window-width="1920" inkscape:window-height="1019" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" showguides="true" inkscape:guide-bbox="true" inkscape:snap-grids="true" inkscape:snap-to-guides="true" inkscape:showpageshadow="2" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1">
<inkscape:grid type="xygrid" id="grid3004" empspacing="4" visible="true" enabled="true" snapvisiblegridlinesonly="true" originx="0" originy="0" />
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="6.4903658" inkscape:cx="86.89803" inkscape:cy="60.551287" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="true" inkscape:window-width="1920" inkscape:window-height="1019" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" showguides="true" inkscape:guide-bbox="true" inkscape:snap-grids="true" inkscape:snap-to-guides="true" inkscape:showpageshadow="2" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1">
<inkscape:grid type="xygrid" id="grid3004" empspacing="4" visible="true" enabled="true" snapvisiblegridlinesonly="true" originx="0" originy="0" spacingy="1" spacingx="1" units="px" />
<sodipodi:guide orientation="-1,0" position="24,144" id="guide3084" inkscape:locked="false" inkscape:label="" inkscape:color="rgb(0,134,229)" />
<sodipodi:guide orientation="0,1" position="0,96" id="guide3086" inkscape:locked="false" inkscape:label="" inkscape:color="rgb(0,134,229)" />
<sodipodi:guide orientation="-1,0" position="48,144" id="guide3088" inkscape:locked="false" inkscape:label="" inkscape:color="rgb(0,134,229)" />
@ -55,7 +55,6 @@
<g id="text4356-2" style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1" transform="translate(44,-124)">
<path inkscape:connector-curvature="0" id="path4384-2" style="font-variant:normal;font-stretch:normal;font-size:20px;font-family:Arial;-inkscape-font-specification:Arial;fill:#ffffff;stroke:#000000;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1" d="m 35.742187,999.43835 3.076172,-3.07617 -3.066406,-3.0664 1.191406,-1.19141 3.066407,3.06641 3.05664,-3.05664 1.171875,1.18164 -3.046875,3.05664 3.066406,3.0664 -1.191406,1.19138 -3.066406,-3.06638 -3.076172,3.07618 -1.181641,-1.18165" />
</g>
<path style="fill:#b3b3b3;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 78,891.36216 v 2 h 12 v -2 z m 0,3.00002 v 1 h 4 v -1 z m 5,0 v 1 h 7 v -1 z m -5,2 v 1 h 4 v -1 z m 5,0 v 1 h 7 v -1 z m -5,2 v 1 h 4 v -1 z m 5,0 v 1 h 7 v -1 z m -5,2 v 1 h 4 v -1 z m 5,0 v 1 h 7 v -1 z" id="table-0" inkscape:connector-curvature="0" inkscape:label="table" inkscape:export-filename="/home/ybon/Code/js/leaflet-storage/src/img/browse-data.png" inkscape:export-xdpi="89.996864" inkscape:export-ydpi="89.996864" />
<path style="fill:#b3b3b3;fill-opacity:1;stroke:none" d="m 63.71429,890.36216 -1.14285,1.1428 2.28571,2.2858 1.14285,-1.1429 z m -1.71429,1.7143 -6.285714,6.28572 2.285714,2.2857 6.28572,-6.2857 z m -6.285714,6.28572 -1.714286,4 4,-1.7143 z" id="path3684-2" inkscape:connector-curvature="0" sodipodi:nodetypes="cccccccccccccc" />
<path id="table-5-8-8-6" style="fill:#464646;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 30,914.86218 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z" inkscape:connector-curvature="0" />
<path inkscape:connector-curvature="0" id="table-5-8-8-6-1" style="fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 54,914.86218 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z" />

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -273,7 +273,12 @@ L.U.ContinueLineAction = L.U.BaseVertexAction.extend({
})
// Leaflet.Toolbar doesn't allow twice same toolbar class…
L.U.SettingsToolbar = L.Toolbar.Control.extend({})
L.U.SettingsToolbar = L.Toolbar.Control.extend({
addTo: function (map) {
if (map.options.editMode !== 'advanced') return
L.Toolbar.Control.prototype.addTo.call(this, map)
},
})
L.U.DrawToolbar = L.Toolbar.Control.extend({
initialize: function (options) {
L.Toolbar.Control.prototype.initialize.call(this, options)
@ -608,21 +613,26 @@ L.U.DataLayer.include({
edit.title = L._('Edit')
table.title = L._('Edit properties in a table')
remove.title = L._('Delete layer')
if (this.isReadOnly()) {
L.DomUtil.addClass(container, 'readonly')
}
else {
L.DomEvent.on(edit, 'click', this.edit, this)
L.DomEvent.on(table, 'click', this.tableEdit, this)
L.DomEvent.on(
remove,
'click',
function () {
if (!this.isVisible()) return
if (!confirm(L._('Are you sure you want to delete this layer?'))) return
this._delete()
this.map.ui.closePanel()
},
this
)
}
L.DomEvent.on(toggle, 'click', this.toggle, this)
L.DomEvent.on(zoomTo, 'click', this.zoomTo, this)
L.DomEvent.on(edit, 'click', this.edit, this)
L.DomEvent.on(table, 'click', this.tableEdit, this)
L.DomEvent.on(
remove,
'click',
function () {
if (!this.isVisible()) return
if (!confirm(L._('Are you sure you want to delete this layer?'))) return
this._delete()
this.map.ui.closePanel()
},
this
)
L.DomUtil.addClass(container, this.getHidableClass())
L.DomUtil.classIf(container, 'off', !this.isVisible())
container.dataset.id = L.stamp(this)
@ -993,11 +1003,13 @@ L.U.Map.include({
}
update()
this.once('saved', L.bind(update, this))
name.href = '#'
share_status.href = '#'
logo.href = '/'
L.DomEvent.on(name, 'click', this.edit, this)
L.DomEvent.on(share_status, 'click', this.permissions.edit, this.permissions)
if (this.options.editMode === 'advanced') {
name.href = '#'
share_status.href = '#'
L.DomEvent.on(name, 'click', this.edit, this)
L.DomEvent.on(share_status, 'click', this.permissions.edit, this.permissions)
}
this.on('postsync', L.bind(update, this))
const save = L.DomUtil.create('a', 'leaflet-control-edit-save button', container)
save.href = '#'
@ -1457,7 +1469,7 @@ L.U.IframeExporter = L.Evented.extend({
miniMap: false,
scrollWheelZoom: false,
zoomControl: true,
allowEdit: false,
editMode: 'disabled',
moreControl: true,
searchControl: null,
tilelayersControl: null,

View file

@ -257,6 +257,34 @@ L.Util.hasVar = (value) => {
return typeof value === 'string' && value.indexOf('{') != -1
}
L.Util.copyToClipboard = function (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()
}
}
}
L.DomUtil.add = (tagName, className, container, content) => {
const el = L.DomUtil.create(tagName, className, container)
if (content) {

View file

@ -0,0 +1,70 @@
L.U.DataLayerPermissions = L.Class.extend({
options: {
edit_status: null,
},
initialize: function (datalayer) {
this.options = L.Util.setOptions(this, datalayer.options.permissions)
this.datalayer = datalayer
let isDirty = false
const self = this
try {
Object.defineProperty(this, 'isDirty', {
get: function () {
return isDirty
},
set: function (status) {
isDirty = status
if (status) self.datalayer.isDirty = status
},
})
} catch (e) {
// Certainly IE8, which has a limited version of defineProperty
}
},
getMap: function () {
return this.datalayer.map
},
edit: function (container) {
const fields = [
[
'options.edit_status',
{
handler: 'IntSelect',
label: L._('Who can edit "{layer}"', { layer: this.datalayer.getName() }),
selectOptions: this.datalayer.map.options.datalayer_edit_statuses,
},
],
],
builder = new L.U.FormBuilder(this, fields, {className: 'umap-form datalayer-permissions'}),
form = builder.build()
container.appendChild(form)
},
getUrl: function () {
return L.Util.template(this.datalayer.map.options.urls.datalayer_permissions, {
map_id: this.datalayer.map.options.umap_id,
pk: this.datalayer.umap_id,
})
},
save: function () {
if (!this.isDirty) return this.datalayer.map.continueSaving()
const formData = new FormData()
formData.append('edit_status', this.options.edit_status)
this.datalayer.map.post(this.getUrl(), {
data: formData,
context: this,
callback: function (data) {
this.commit()
this.isDirty = false
this.datalayer.map.continueSaving()
},
})
},
commit: function () {
L.Util.extend(this.datalayer.options.permissions, this.options)
},
})

View file

@ -40,7 +40,7 @@ L.U.FeatureMixin = {
preInit: function () {},
isReadOnly: function () {
return this.datalayer && this.datalayer.isRemoteLayer()
return this.datalayer && this.datalayer.isDataReadOnly()
},
getSlug: function () {

View file

@ -396,7 +396,7 @@ L.FormBuilder.DataLayerSwitcher = L.FormBuilder.Select.extend({
getOptions: function () {
const options = []
this.builder.map.eachDataLayerReverse((datalayer) => {
if (datalayer.isLoaded() && !datalayer.isRemoteLayer() && datalayer.canBrowse()) {
if (datalayer.isLoaded() && !datalayer.isDataReadOnly() && datalayer.canBrowse()) {
options.push([L.stamp(datalayer), datalayer.getName()])
}
})

View file

@ -15,7 +15,7 @@ L.Map.mergeOptions({
default_interactive: true,
default_labelDirection: 'auto',
attributionControl: false,
allowEdit: true,
editMode: 'advanced',
embedControl: true,
zoomControl: true,
datalayersControl: true,
@ -103,7 +103,7 @@ L.U.Map.include({
L.Util.setBooleanFromQueryString(this.options, 'moreControl')
L.Util.setBooleanFromQueryString(this.options, 'scaleControl')
L.Util.setBooleanFromQueryString(this.options, 'miniMap')
L.Util.setBooleanFromQueryString(this.options, 'allowEdit')
L.Util.setBooleanFromQueryString(this.options, 'editMode')
L.Util.setBooleanFromQueryString(this.options, 'displayDataBrowserOnLoad')
L.Util.setBooleanFromQueryString(this.options, 'displayCaptionOnLoad')
L.Util.setBooleanFromQueryString(this.options, 'captionBar')
@ -122,7 +122,7 @@ L.U.Map.include({
if (this.datalayersOnLoad)
this.datalayersOnLoad = this.datalayersOnLoad.toString().split(',')
if (L.Browser.ielt9) this.options.allowEdit = false // TODO include ie9
if (L.Browser.ielt9) this.options.editMode = 'disabled' // TODO include ie9
let editedFeature = null
const self = this
@ -192,16 +192,15 @@ L.U.Map.include({
this
)
let isDirty = false // global status
let isDirty = false // self status
try {
Object.defineProperty(this, 'isDirty', {
get: function () {
return isDirty || this.dirty_datalayers.length
return isDirty
},
set: function (status) {
if (!isDirty && status) self.fire('isdirty')
isDirty = status
self.checkDirty()
this.checkDirty()
},
})
} catch (e) {
@ -220,7 +219,7 @@ L.U.Map.include({
this.isDirty = true
this._default_extent = true
this.options.name = L._('Untitled map')
this.options.allowEdit = true
this.options.editMode = 'advanced'
const datalayer = this.createDataLayer()
datalayer.connectToMap()
this.enableEdit()
@ -238,7 +237,7 @@ L.U.Map.include({
this.slideshow = new L.U.Slideshow(this, this.options.slideshow)
this.permissions = new L.U.MapPermissions(this)
this.initCaptionBar()
if (this.options.allowEdit) {
if (this.hasEditMode()) {
this.editTools = new L.U.Editable(this)
this.ui.on(
'panel:closed panel:open',
@ -277,7 +276,7 @@ L.U.Map.include({
this.helpMenuActions = {}
this._controls = {}
if (this.options.allowEdit && !this.options.noControl) {
if (this.hasEditMode() && !this.options.noControl) {
new L.U.EditControl(this).addTo(this)
new L.U.DrawToolbar({ map: this }).addTo(this)
@ -496,7 +495,7 @@ L.U.Map.include({
else this.ui.closePanel()
}
if (!this.options.allowEdit) return
if (!this.hasEditMode()) return
/* Edit mode only shortcuts */
if (key === L.U.Keys.E && modifierKey && !this.editEnabled) {
@ -1161,47 +1160,16 @@ L.U.Map.include({
return JSON.stringify(umapfile, null, 2)
},
save: function () {
if (!this.isDirty) return
if (this._default_extent) this.updateExtent()
saveSelf: function () {
const geojson = {
type: 'Feature',
geometry: this.geometry(),
properties: this.exportOptions(),
}
this.backup()
const formData = new FormData()
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,
@ -1212,6 +1180,7 @@ L.U.Map.include({
alert.content = L._('Congratulations, your map has been created!')
this.options.umap_id = data.id
this.permissions.setOptions(data.permissions)
this.permissions.commit()
if (
data.permissions &&
data.permissions.anonymous_edit_url &&
@ -1233,7 +1202,7 @@ L.U.Map.include({
{
label: L._('Copy link'),
callback: () => {
copyToClipboard(data.permissions.anonymous_edit_url)
L.Util.copyToClipboard(data.permissions.anonymous_edit_url)
this.ui.alert({
content: L._('Secret edit link copied to clipboard!'),
level: 'info',
@ -1247,22 +1216,35 @@ L.U.Map.include({
// Do not override local changes to permissions,
// but update in case some other editors changed them in the meantime.
this.permissions.setOptions(data.permissions)
this.permissions.commit()
}
// Update URL in case the name has changed.
if (history && history.pushState)
history.pushState({}, this.options.name, data.url)
else window.location = data.url
alert.content = data.info || alert.content
this.once('saved', function () {
this.isDirty = false
this.ui.alert(alert)
})
this.once('saved', () => this.ui.alert(alert))
this.ui.closePanel()
this.permissions.save()
},
})
},
save: function () {
if (!this.isDirty) return
if (this._default_extent) this.updateExtent()
this.backup()
this.once('saved', () => {
this.isDirty = false
})
if (this.options.editMode === 'advanced') {
// Only save the map if the user has the rights to do so.
this.saveSelf()
} else {
this.permissions.save()
}
},
sendEditLink: function () {
const url = L.Util.template(this.options.urls.map_send_edit_link, {
map_id: this.options.umap_id,
@ -1330,14 +1312,14 @@ L.U.Map.include({
datalayer = this.lastUsedDataLayer
if (
datalayer &&
!datalayer.isRemoteLayer() &&
!datalayer.isDataReadOnly() &&
datalayer.canBrowse() &&
datalayer.isVisible()
) {
return datalayer
}
datalayer = this.findDataLayer((datalayer) => {
if (!datalayer.isRemoteLayer() && datalayer.canBrowse()) {
if (!datalayer.isDataReadOnly() && datalayer.canBrowse()) {
fallback = datalayer
if (datalayer.isVisible()) return true
}
@ -1733,20 +1715,28 @@ L.U.Map.include({
_advancedActions: function (container) {
const advancedActions = L.DomUtil.createFieldset(container, L._('Advanced actions'))
const advancedButtons = L.DomUtil.create('div', 'button-bar half', advancedActions)
const del = L.DomUtil.create('a', 'button umap-delete', advancedButtons)
del.href = '#'
del.textContent = L._('Delete')
L.DomEvent.on(del, 'click', L.DomEvent.stop).on(del, 'click', this.del, this)
if (this.permissions.isOwner()) {
const del = L.DomUtil.create('a', 'button umap-delete', advancedButtons)
del.href = '#'
del.title = L._('Delete map')
del.textContent = L._('Delete')
L.DomEvent.on(del, 'click', L.DomEvent.stop).on(del, 'click', this.del, this)
const empty = L.DomUtil.create('a', 'button umap-empty', advancedButtons)
empty.href = '#'
empty.textContent = L._('Empty')
empty.title = L._('Delete all layers')
L.DomEvent.on(empty, 'click', L.DomEvent.stop).on(
empty,
'click',
this.empty,
this
)
}
const clone = L.DomUtil.create('a', 'button umap-clone', advancedButtons)
clone.href = '#'
clone.textContent = L._('Clone')
clone.title = L._('Clone this map')
L.DomEvent.on(clone, 'click', L.DomEvent.stop).on(clone, 'click', this.clone, this)
const empty = L.DomUtil.create('a', 'button umap-empty', advancedButtons)
empty.href = '#'
empty.textContent = L._('Empty')
empty.title = L._('Delete all layers')
L.DomEvent.on(empty, 'click', L.DomEvent.stop).on(empty, 'click', this.empty, this)
const download = L.DomUtil.create('a', 'button umap-download', advancedButtons)
download.href = '#'
download.textContent = L._('Download')
@ -1761,6 +1751,7 @@ L.U.Map.include({
edit: function () {
if (!this.editEnabled) return
if (this.options.editMode !== 'advanced') return
const container = L.DomUtil.create('div', 'umap-edit-container'),
metadataFields = ['options.name', 'options.description'],
title = L.DomUtil.create('h3', '', container)
@ -1796,6 +1787,10 @@ L.U.Map.include({
this.fire('edit:disabled')
},
hasEditMode: function () {
return this.options.editMode === 'simple' || this.options.editMode === 'advanced'
},
getDisplayName: function () {
return this.options.name || L._('Untitled map')
},
@ -1952,7 +1947,7 @@ L.U.Map.include({
items = items.concat(e.relatedTarget.getContextMenuItems(e))
}
}
if (this.options.allowEdit) {
if (this.hasEditMode()) {
items.push('-')
if (this.editEnabled) {
if (!this.isDirty) {

View file

@ -193,6 +193,7 @@ L.U.DataLayer = L.Evented.extend({
options: {
displayOnLoad: true,
browsable: true,
editMode: 'advanced',
},
initialize: function (map, data) {
@ -201,8 +202,8 @@ L.U.DataLayer = L.Evented.extend({
this._layers = {}
this._geojson = null
this._propertiesIndex = []
this._loaded = false // Are layer metadata loaded
this._dataloaded = false // Are layer data loaded
this._loaded = false // Are layer metadata loaded
this._dataloaded = false // Are layer data loaded
this.parentPane = this.map.getPane('overlayPane')
this.pane = this.map.createPane(`datalayer${L.stamp(this)}`, this.parentPane)
@ -261,6 +262,7 @@ L.U.DataLayer = L.Evented.extend({
}
this.backupOptions()
this.connectToMap()
this.permissions = new L.U.DataLayerPermissions(this)
if (this.showAtLoad()) this.show()
if (!this.umap_id) this.isDirty = true
@ -350,6 +352,12 @@ L.U.DataLayer = L.Evented.extend({
this.map.get(this._dataUrl(), {
callback: function (geojson, response) {
this._last_modified = response.getResponseHeader('Last-Modified')
// FIXME: for now this property is set dynamically from backend
// And thus it's not in the geojson file in the server
// So do not let all options to be reset
// Fix is a proper migration so all datalayers settings are
// in DB, and we remove it from geojson flat files.
geojson['_umap_options']['editMode'] = this.options.editMode
this.fromUmapGeoJSON(geojson)
this.backupOptions()
this.fire('loaded')
@ -489,7 +497,7 @@ L.U.DataLayer = L.Evented.extend({
})
// No browser cache for owners/editors.
if (this.map.options.allowEdit) url = `${url}?${Date.now()}`
if (this.map.hasEditMode()) url = `${url}?${Date.now()}`
return url
},
@ -1182,18 +1190,20 @@ L.U.DataLayer = L.Evented.extend({
}
},
metadata: function () {
return {
id: this.umap_id,
name: this.options.name,
displayOnLoad: this.options.displayOnLoad,
}
},
getRank: function () {
return this.map.datalayers_index.indexOf(this)
},
isReadOnly: function () {
// isReadOnly must return true if unset
return this.options.editMode === 'disabled'
},
isDataReadOnly: function () {
// This layer cannot accept features
return this.isReadOnly() || this.isRemoteLayer()
},
save: function () {
if (this.isDeleted) return this.saveDelete()
if (!this.isLoaded()) {
@ -1220,7 +1230,7 @@ L.U.DataLayer = L.Evented.extend({
this._loaded = true
this.redraw() // Needed for reordering features
this.isDirty = false
this.map.continueSaving()
this.permissions.save()
},
context: this,
headers: this._last_modified

View file

@ -20,7 +20,9 @@ L.U.MapPermissions = L.Class.extend({
},
set: function (status) {
isDirty = status
if (status) self.map.isDirty = status
if (status) {
self.map.isDirty = status
}
},
})
} catch (e) {
@ -35,13 +37,13 @@ L.U.MapPermissions = L.Class.extend({
isOwner: function () {
return (
this.map.options.user &&
this.map.permissions.options.owner &&
this.map.options.user.id == this.map.permissions.options.owner.id
this.map.options.permissions.owner &&
this.map.options.user.id == this.map.options.permissions.owner.id
)
},
isAnonymousMap: function () {
return !this.map.permissions.options.owner
return !this.map.options.permissions.owner
},
getMap: function () {
@ -49,6 +51,7 @@ L.U.MapPermissions = L.Class.extend({
},
edit: function () {
if (this.map.options.editMode !== 'advanced') return
if (!this.map.options.umap_id)
return this.map.ui.alert({
content: L._('Please save the map first'),
@ -59,15 +62,16 @@ L.U.MapPermissions = L.Class.extend({
title = L.DomUtil.create('h4', '', container)
if (this.isAnonymousMap()) {
if (this.options.anonymous_edit_url) {
const helpText = L._('Secret edit link is:<br>{link}', {
link: this.options.anonymous_edit_url,
})
const helpText = `${L._('Secret edit link:')}<br>${
this.options.anonymous_edit_url
}`
L.DomUtil.add('p', 'help-text', container, helpText)
fields.push([
'options.edit_status',
{
handler: 'IntSelect',
label: L._('Who can edit'),
selectOptions: this.map.options.anonymous_edit_statuses,
selectOptions: this.map.options.edit_statuses,
helpText: helpText,
},
])
@ -122,6 +126,10 @@ L.U.MapPermissions = L.Class.extend({
this
)
}
L.DomUtil.add('h3', '', container, L._('Datalayers'))
this.map.eachDataLayer((datalayer) => {
datalayer.permissions.edit(container)
})
this.map.ui.openPanel({ data: { html: container }, className: 'dark' })
},
@ -197,6 +205,8 @@ L.U.MapPermissions = L.Class.extend({
},
getShareStatusDisplay: function () {
return Object.fromEntries(this.map.options.share_statuses)[this.options.share_status]
}
return Object.fromEntries(this.map.options.share_statuses)[
this.options.share_status
]
},
})

View file

@ -756,15 +756,18 @@ a.map-name:after {
.umap-toggle-edit {
background-position: -44px -48px;
}
.readonly .layer-table-edit,
.off .layer-table-edit {
background-position: -74px -1px;
}
.readonly .layer-edit,
.off .layer-edit {
background-position: -51px -72px;
}
.off .layer-zoom_to {
background-position: -25px -54px;
}
.readonly .layer-delete,
.off .layer-delete {
background-position: -122px -121px;
}

View file

@ -207,6 +207,7 @@ describe('L.U.Map.Export', function () {
_umap_options: {
displayOnLoad: true,
browsable: true,
editMode: 'advanced',
iconClass: 'Default',
name: 'Elephants',
id: 62,

View file

@ -105,7 +105,7 @@ describe('L.U.Map', function () {
window.confirm = oldConfirm
})
it('should ask for confirmation on delete link click', function (done) {
it('should ask for confirmation on delete link click', function () {
var button = qs('a.update-map-settings')
assert.ok(button, 'update map info button exists')
happen.click(button)
@ -117,7 +117,7 @@ describe('L.U.Map', function () {
this.server.respond()
assert(window.confirm.calledOnce)
window.confirm.restore()
done()
})
})

View file

@ -34,11 +34,11 @@ describe('L.Permissions', function () {
describe('#anonymous with cookie', function () {
var button
it('should only allow edit_status', function () {
it('should not allow share_status nor owner', function () {
this.map.permissions.options.anonymous_edit_url = 'http://anonymous.url'
delete this.map.permissions.options.owner
button = qs('a.update-map-permissions')
happen.click(button)
expect(qs('select[name="edit_status"]')).to.be.ok
expect(qs('select[name="share_status"]')).not.to.be.ok
expect(qs('input.edit-owner')).not.to.be.ok
})
@ -49,9 +49,10 @@ describe('L.Permissions', function () {
it('should only allow editors', function () {
this.map.permissions.options.owner = { id: 1, url: '/url', name: 'jojo' }
delete this.map.permissions.options.anonymous_edit_url
delete this.map.options.user
button = qs('a.update-map-permissions')
happen.click(button)
expect(qs('select[name="edit_status"]')).not.to.be.ok
expect(qs('select[name="share_status"]')).not.to.be.ok
expect(qs('input.edit-owner')).not.to.be.ok
expect(qs('input.edit-editors')).to.be.ok
@ -66,8 +67,6 @@ describe('L.Permissions', function () {
this.map.options.user = { id: 1, url: '/url', name: 'jojo' }
button = qs('a.update-map-permissions')
happen.click(button)
expect(qs('select[name="edit_status"]')).to.be.ok
expect(qs('select[name="share_status"]')).to.be.ok
expect(qs('input.edit-owner')).to.be.ok
expect(qs('input.edit-editors')).to.be.ok
})

View file

@ -190,7 +190,7 @@ function initMap(options) {
name: 'name of the map',
description: 'The description of the map',
locale: 'en',
allowEdit: true,
editMode: 'advanced',
moreControl: true,
scaleControl: true,
miniMap: false,
@ -198,6 +198,20 @@ function initMap(options) {
displayCaptionOnLoad: false,
displayPopupFooter: false,
displayDataBrowserOnLoad: false,
permissions: {
share_status: 1,
owner: {
id: 1,
name: 'ybon',
url: '/en/user/ybon/',
},
editors: [],
},
user: {
id: 1,
name: 'foofoo',
url: '/en/me',
},
},
}
default_options.properties.datalayers.push(defaultDatalayerData())
@ -319,7 +333,11 @@ var RESPONSES = {
datalayer64_GET: {
crs: null,
type: 'FeatureCollection',
_umap_options: defaultDatalayerData({name: 'hidden', id: 64, displayOnLoad: false }),
_umap_options: defaultDatalayerData({
name: 'hidden',
id: 64,
displayOnLoad: false,
}),
features: [
{
geometry: {

View file

@ -37,6 +37,7 @@
<script src="../js/umap.slideshow.js"></script>
<script src="../js/umap.tableeditor.js"></script>
<script src="../js/umap.permissions.js"></script>
<script src="../js/umap.datalayer.permissions.js"></script>
<script src="../js/umap.js"></script>
<script src="../js/umap.ui.js"></script>
<link rel="stylesheet" href="../vendors/leaflet/leaflet.css" />

View file

@ -34,11 +34,12 @@
<script src="{{ STATIC_URL }}umap/js/umap.forms.js"></script>
<script src="{{ STATIC_URL }}umap/js/umap.icon.js"></script>
<script src="{{ STATIC_URL }}umap/js/umap.features.js"></script>
<script src="{{ STATIC_URL }}umap/js/umap.permissions.js"></script>
<script src="{{ STATIC_URL }}umap/js/umap.datalayer.permissions.js"></script>
<script src="{{ STATIC_URL }}umap/js/umap.layer.js"></script>
<script src="{{ STATIC_URL }}umap/js/umap.controls.js"></script>
<script src="{{ STATIC_URL }}umap/js/umap.slideshow.js"></script>
<script src="{{ STATIC_URL }}umap/js/umap.tableeditor.js"></script>
<script src="{{ STATIC_URL }}umap/js/umap.permissions.js"></script>
<script src="{{ STATIC_URL }}umap/js/umap.js"></script>
<script src="{{ STATIC_URL }}umap/js/umap.ui.js"></script>
{% endcompress %}

View file

@ -28,7 +28,7 @@ def umap_js(locale=None):
@register.inclusion_tag('umap/map_fragment.html')
def map_fragment(map_instance, **kwargs):
layers = DataLayer.objects.filter(map=map_instance)
datalayer_data = [c.metadata for c in layers]
datalayer_data = [c.metadata() for c in layers]
map_settings = map_instance.settings
if "properties" not in map_settings:
map_settings['properties'] = {}
@ -37,7 +37,7 @@ def map_fragment(map_instance, **kwargs):
'datalayers': datalayer_data,
'urls': _urls_for_js(),
'STATIC_URL': settings.STATIC_URL,
"allowEdit": False,
"editMode": 'disabled',
'hash': False,
'attributionControl': False,
'scrollWheelZoom': False,

View file

@ -2,6 +2,7 @@ import json
import factory
from django.contrib.auth import get_user_model
from django.core.files.base import ContentFile
from django.urls import reverse
from umap.forms import DEFAULT_CENTER
@ -9,6 +10,25 @@ from umap.models import DataLayer, Licence, Map, TileLayer
User = get_user_model()
DATALAYER_DATA = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [13.68896484375, 48.55297816440071],
},
"properties": {
"_umap_options": {"color": "DarkCyan", "iconClass": "Ball"},
"name": "Here",
"description": "Da place anonymous again 755",
},
}
],
"_umap_options": {"displayOnLoad": True, "name": "Donau", "id": 926},
}
class LicenceFactory(factory.django.DjangoModelFactory):
name = "WTFPL"
@ -82,10 +102,18 @@ class DataLayerFactory(factory.django.DjangoModelFactory):
name = "test datalayer"
description = "test description"
display_on_load = True
settings = {"displayOnLoad": True, "browsable": True, name: "test datalayer"}
geojson = factory.django.FileField(
data="""{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[13.68896484375,48.55297816440071]},"properties":{"_umap_options":{"color":"DarkCyan","iconClass":"Ball"},"name":"Here","description":"Da place anonymous again 755"}}],"_umap_options":{"displayOnLoad":true,"name":"Donau","id":926}}"""
) # noqa
settings = {"displayOnLoad": True, "browsable": True, "name": name}
geojson = factory.django.FileField()
@factory.post_generation
def geojson_data(obj, create, extracted, **kwargs):
# Make sure DB settings and file settings are aligned.
# At some point, file settings should be removed, but we are not there yet.
data = DATALAYER_DATA.copy()
obj.settings["name"] = obj.name
data["_umap_options"] = obj.settings
with open(obj.geojson.path, mode="w") as f:
f.write(json.dumps(data))
class Meta:
model = DataLayer

View file

@ -74,7 +74,7 @@ def allow_anonymous(settings):
@pytest.fixture
def datalayer(map):
return DataLayerFactory(map=map, name="Default Datalayer")
return DataLayerFactory(map=map)
@pytest.fixture

View file

@ -0,0 +1,149 @@
import re
from time import sleep
import pytest
from django.core.signing import get_cookie_signer
from playwright.sync_api import expect
from umap.models import DataLayer
from ..base import DataLayerFactory
pytestmark = [pytest.mark.django_db, pytest.mark.usefixtures("allow_anonymous")]
@pytest.fixture
def owner_session(anonymap, context, live_server):
key, value = anonymap.signed_cookie_elements
signed = get_cookie_signer(salt=key).sign(value)
context.add_cookies([{"name": key, "value": signed, "url": live_server.url}])
return context.new_page()
def test_map_load_with_owner(anonymap, live_server, owner_session):
owner_session.goto(f"{live_server.url}{anonymap.get_absolute_url()}")
map_el = owner_session.locator("#map")
expect(map_el).to_be_visible()
enable = owner_session.get_by_role("link", name="Edit")
expect(enable).to_be_visible()
enable.click()
disable = owner_session.get_by_role("link", name="Disable editing")
expect(disable).to_be_visible()
save = owner_session.get_by_title("Save current edits (Ctrl+S)")
expect(save).to_be_visible()
add_marker = owner_session.get_by_title("Draw a marker")
expect(add_marker).to_be_visible()
edit_settings = owner_session.get_by_title("Edit map settings")
expect(edit_settings).to_be_visible()
edit_permissions = owner_session.get_by_title("Update permissions and editors")
expect(edit_permissions).to_be_visible()
def test_map_load_with_anonymous(anonymap, live_server, page):
page.goto(f"{live_server.url}{anonymap.get_absolute_url()}")
map_el = page.locator("#map")
expect(map_el).to_be_visible()
enable = page.get_by_role("link", name="Edit")
expect(enable).to_be_hidden()
def test_map_load_with_anonymous_but_editable_layer(
anonymap, live_server, page, datalayer
):
datalayer.edit_status = DataLayer.ANONYMOUS
datalayer.save()
page.goto(f"{live_server.url}{anonymap.get_absolute_url()}")
map_el = page.locator("#map")
expect(map_el).to_be_visible()
enable = page.get_by_role("link", name="Edit")
expect(enable).to_be_visible()
enable.click()
disable = page.get_by_role("link", name="Disable editing")
expect(disable).to_be_visible()
save = page.get_by_title("Save current edits (Ctrl+S)")
expect(save).to_be_visible()
add_marker = page.get_by_title("Draw a marker")
expect(add_marker).to_be_visible()
edit_settings = page.get_by_title("Edit map settings")
expect(edit_settings).to_be_hidden()
edit_permissions = page.get_by_title("Update permissions and editors")
expect(edit_permissions).to_be_hidden()
def test_owner_permissions_form(map, datalayer, live_server, owner_session):
owner_session.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
edit_permissions = owner_session.get_by_title("Update permissions and editors")
expect(edit_permissions).to_be_visible()
edit_permissions.click()
select = owner_session.locator(".umap-field-share_status select")
expect(select).to_be_hidden()
owner_field = owner_session.locator(".umap-field-owner")
expect(owner_field).to_be_hidden()
editors_field = owner_session.locator(".umap-field-editors input")
expect(editors_field).to_be_hidden()
datalayer_label = owner_session.get_by_text('Who can edit "test datalayer"')
expect(datalayer_label).to_be_visible()
options = owner_session.locator(
".datalayer-permissions select[name='edit_status'] option"
)
expect(options).to_have_count(3)
option = owner_session.locator(
".datalayer-permissions select[name='edit_status'] option:checked"
)
expect(option).to_have_text("Inherit")
def test_anonymous_can_add_marker_on_editable_layer(
anonymap, datalayer, live_server, page
):
datalayer.edit_status = DataLayer.OWNER
datalayer.name = "Should not be in the select"
datalayer.save() # Non editable by anonymous users
assert datalayer.map == anonymap
other = DataLayerFactory(
map=anonymap, edit_status=DataLayer.ANONYMOUS, name="Editable"
)
assert other.map == anonymap
page.goto(f"{live_server.url}{anonymap.get_absolute_url()}?edit")
add_marker = page.get_by_title("Draw a marker")
expect(add_marker).to_be_visible()
marker = page.locator(".leaflet-marker-icon")
map_el = page.locator("#map")
expect(marker).to_have_count(2)
expect(map_el).not_to_have_class(re.compile("umap-ui"))
add_marker.click()
map_el.click(position={"x": 100, "y": 100})
expect(marker).to_have_count(3)
# Edit panel is open
expect(map_el).to_have_class(re.compile("umap-ui"))
datalayer_select = page.locator("select[name='datalayer']")
expect(datalayer_select).to_be_visible()
options = page.locator("select[name='datalayer'] option")
expect(options).to_have_count(1) # Only Editable layer should be listed
option = page.locator("select[name='datalayer'] option:checked")
expect(option).to_have_text(other.name)
def test_can_change_perms_after_create(tilelayer, live_server, page):
page.goto(f"{live_server.url}/en/map/new")
save = page.get_by_title("Save current edits")
expect(save).to_be_visible()
save.click()
sleep(1) # Let save ajax go back
edit_permissions = page.get_by_title("Update permissions and editors")
expect(edit_permissions).to_be_visible()
edit_permissions.click()
select = page.locator(".umap-field-share_status select")
expect(select).to_be_hidden()
owner_field = page.locator(".umap-field-owner")
expect(owner_field).to_be_hidden()
editors_field = page.locator(".umap-field-editors input")
expect(editors_field).to_be_hidden()
datalayer_label = page.get_by_text('Who can edit "Layer 1"')
expect(datalayer_label).to_be_visible()
options = page.locator(".datalayer-permissions select[name='edit_status'] option")
expect(options).to_have_count(3)
option = page.locator(
".datalayer-permissions select[name='edit_status'] option:checked"
)
expect(option).to_have_text("Inherit")

View file

@ -0,0 +1,37 @@
import pytest
from playwright.sync_api import expect
from umap.models import Map
pytestmark = pytest.mark.django_db
def test_remote_layer_should_not_be_used_as_datalayer_for_created_features(
map, live_server, datalayer, page
):
# Faster than doing a login
map.edit_status = Map.ANONYMOUS
map.save()
datalayer.settings["remoteData"] = {
"url": "https://overpass-api.de/api/interpreter?data=[out:xml];node[harbour=yes]({south},{west},{north},{east});out body;",
"format": "osm",
"from": "10",
}
datalayer.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
toggle = page.get_by_title("See data layers")
expect(toggle).to_be_visible()
toggle.click()
layers = page.locator(".umap-browse-datalayers li")
expect(layers).to_have_count(1)
map_el = page.locator("#map")
add_marker = page.get_by_title("Draw a marker")
expect(add_marker).to_be_visible()
marker = page.locator(".leaflet-marker-icon")
expect(marker).to_have_count(0)
add_marker.click()
map_el.click(position={"x": 100, "y": 100})
expect(marker).to_have_count(1)
# A new datalayer has been created to host this created feature
# given the remote one cannot accept new features
expect(layers).to_have_count(2)

View file

@ -0,0 +1,214 @@
from time import sleep
import pytest
from playwright.sync_api import expect
from umap.models import DataLayer, Map
pytestmark = pytest.mark.django_db
@pytest.fixture
def login(context, settings, live_server):
def do_login(user):
# TODO use storage state to do login only once per session
# https://playwright.dev/python/docs/auth
settings.ENABLE_ACCOUNT_LOGIN = True
page = context.new_page()
page.goto(f"{live_server.url}/en/")
page.locator(".login").click()
page.get_by_placeholder("Username").fill(user.username)
page.get_by_placeholder("Password").fill("123123")
page.locator('#login_form input[type="submit"]').click()
sleep(1) # Time for ajax login POST to proceed
return page
return do_login
def test_map_update_with_owner(map, live_server, login):
page = login(map.owner)
page.goto(f"{live_server.url}{map.get_absolute_url()}")
map_el = page.locator("#map")
expect(map_el).to_be_visible()
enable = page.get_by_role("link", name="Edit")
expect(enable).to_be_visible()
enable.click()
disable = page.get_by_role("link", name="Disable editing")
expect(disable).to_be_visible()
save = page.get_by_title("Save current edits (Ctrl+S)")
expect(save).to_be_visible()
add_marker = page.get_by_title("Draw a marker")
expect(add_marker).to_be_visible()
edit_settings = page.get_by_title("Edit map settings")
expect(edit_settings).to_be_visible()
edit_permissions = page.get_by_title("Update permissions and editors")
expect(edit_permissions).to_be_visible()
def test_map_update_with_anonymous(map, live_server, page):
page.goto(f"{live_server.url}{map.get_absolute_url()}")
map_el = page.locator("#map")
expect(map_el).to_be_visible()
enable = page.get_by_role("link", name="Edit")
expect(enable).to_be_hidden()
def test_map_update_with_anonymous_but_editable_datalayer(
map, datalayer, live_server, page
):
datalayer.edit_status = DataLayer.ANONYMOUS
datalayer.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}")
map_el = page.locator("#map")
expect(map_el).to_be_visible()
enable = page.get_by_role("link", name="Edit")
expect(enable).to_be_visible()
enable.click()
add_marker = page.get_by_title("Draw a marker")
expect(add_marker).to_be_visible()
edit_settings = page.get_by_title("Edit map settings")
expect(edit_settings).to_be_hidden()
edit_permissions = page.get_by_title("Update permissions and editors")
expect(edit_permissions).to_be_hidden()
def test_owner_permissions_form(map, datalayer, live_server, login):
page = login(map.owner)
page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
edit_permissions = page.get_by_title("Update permissions and editors")
expect(edit_permissions).to_be_visible()
edit_permissions.click()
select = page.locator(".umap-field-share_status select")
expect(select).to_be_visible()
# expect(select).to_have_value(Map.PUBLIC) # Does not work
owner_field = page.locator(".umap-field-owner")
expect(owner_field).to_be_visible()
editors_field = page.locator(".umap-field-editors input")
expect(editors_field).to_be_visible()
datalayer_label = page.get_by_text('Who can edit "test datalayer"')
expect(datalayer_label).to_be_visible()
options = page.locator(".datalayer-permissions select[name='edit_status'] option")
expect(options).to_have_count(4)
option = page.locator(
".datalayer-permissions select[name='edit_status'] option:checked"
)
expect(option).to_have_text("Inherit")
def test_map_update_with_editor(map, live_server, login, user):
map.edit_status = Map.EDITORS
map.editors.add(user)
map.save()
page = login(user)
page.goto(f"{live_server.url}{map.get_absolute_url()}")
map_el = page.locator("#map")
expect(map_el).to_be_visible()
enable = page.get_by_role("link", name="Edit")
expect(enable).to_be_visible()
enable.click()
disable = page.get_by_role("link", name="Disable editing")
expect(disable).to_be_visible()
save = page.get_by_title("Save current edits (Ctrl+S)")
expect(save).to_be_visible()
add_marker = page.get_by_title("Draw a marker")
expect(add_marker).to_be_visible()
edit_settings = page.get_by_title("Edit map settings")
expect(edit_settings).to_be_visible()
edit_permissions = page.get_by_title("Update permissions and editors")
expect(edit_permissions).to_be_visible()
def test_permissions_form_with_editor(map, datalayer, live_server, login, user):
map.edit_status = Map.EDITORS
map.editors.add(user)
map.save()
page = login(user)
page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
edit_permissions = page.get_by_title("Update permissions and editors")
expect(edit_permissions).to_be_visible()
edit_permissions.click()
select = page.locator(".umap-field-share_status select")
expect(select).to_be_hidden()
# expect(select).to_have_value(Map.PUBLIC) # Does not work
owner_field = page.locator(".umap-field-owner")
expect(owner_field).to_be_hidden()
editors_field = page.locator(".umap-field-editors input")
expect(editors_field).to_be_visible()
datalayer_label = page.get_by_text('Who can edit "test datalayer"')
expect(datalayer_label).to_be_visible()
def test_owner_has_delete_map_button(map, live_server, login):
page = login(map.owner)
page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
settings = page.get_by_title("Edit map settings")
expect(settings).to_be_visible()
settings.click()
advanced = page.get_by_text("Advanced actions")
expect(advanced).to_be_visible()
advanced.click()
delete = page.get_by_role("link", name="Delete")
expect(delete).to_be_visible()
def test_editor_do_not_have_delete_map_button(map, live_server, login, user):
map.edit_status = Map.EDITORS
map.editors.add(user)
map.save()
page = login(user)
page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
settings = page.get_by_title("Edit map settings")
expect(settings).to_be_visible()
settings.click()
advanced = page.get_by_text("Advanced actions")
expect(advanced).to_be_visible()
advanced.click()
delete = page.get_by_role("link", name="Delete")
expect(delete).to_be_hidden()
def test_can_change_perms_after_create(tilelayer, live_server, login, user):
page = login(user)
page.goto(f"{live_server.url}/en/map/new")
save = page.get_by_title("Save current edits")
expect(save).to_be_visible()
save.click()
sleep(1) # Let save ajax go back
edit_permissions = page.get_by_title("Update permissions and editors")
expect(edit_permissions).to_be_visible()
edit_permissions.click()
select = page.locator(".umap-field-share_status select")
expect(select).to_be_visible()
option = page.locator("select[name='share_status'] option:checked")
expect(option).to_have_text("Everyone (public)")
owner_field = page.locator(".umap-field-owner")
expect(owner_field).to_be_visible()
editors_field = page.locator(".umap-field-editors input")
expect(editors_field).to_be_visible()
datalayer_label = page.get_by_text('Who can edit "Layer 1"')
expect(datalayer_label).to_be_visible()
options = page.locator(".datalayer-permissions select[name='edit_status'] option")
expect(options).to_have_count(4)
option = page.locator(
".datalayer-permissions select[name='edit_status'] option:checked"
)
expect(option).to_have_text("Inherit")
def test_can_change_owner(map, live_server, login, user):
page = login(map.owner)
page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
edit_permissions = page.get_by_title("Update permissions and editors")
edit_permissions.click()
close = page.locator(".umap-field-owner .close")
close.click()
input = page.locator("input.edit-owner")
input.type(user.username)
input.press("Tab")
save = page.get_by_title("Save current edits")
expect(save).to_be_visible()
save.click()
sleep(1) # Let save ajax go
modified = Map.objects.get(pk=map.pk)
assert modified.owner == user

View file

@ -4,6 +4,7 @@ import pytest
from django.core.files.base import ContentFile
from .base import DataLayerFactory, MapFactory
from umap.models import DataLayer, Map
pytestmark = pytest.mark.django_db
@ -21,7 +22,7 @@ def test_datalayers_should_be_ordered_by_rank(map, datalayer):
def test_upload_to(map, datalayer):
map.pk = 302
datalayer.pk = 17
assert datalayer.upload_to().startswith('datalayer/2/0/302/17_')
assert datalayer.upload_to().startswith("datalayer/2/0/302/17_")
def test_save_should_use_pk_as_name(map, datalayer):
@ -81,3 +82,120 @@ def test_should_remove_old_versions_on_save(datalayer, map, settings):
assert os.path.basename(other) in files
assert os.path.basename(other + ".gz") in files
assert os.path.basename(older) not in files
assert os.path.basename(older + ".gz") not in files
def test_anonymous_cannot_edit_in_editors_mode(datalayer):
datalayer.edit_status = DataLayer.EDITORS
datalayer.save()
assert not datalayer.can_edit()
def test_owner_can_edit_in_editors_mode(datalayer, user):
datalayer.edit_status = DataLayer.EDITORS
datalayer.save()
assert datalayer.can_edit(datalayer.map.owner)
def test_editor_can_edit_in_editors_mode(datalayer, user):
map = datalayer.map
map.editors.add(user)
map.save()
datalayer.edit_status = DataLayer.EDITORS
datalayer.save()
assert datalayer.can_edit(user)
def test_anonymous_can_edit_in_public_mode(datalayer):
datalayer.edit_status = DataLayer.ANONYMOUS
datalayer.save()
assert datalayer.can_edit()
def test_owner_can_edit_in_public_mode(datalayer, user):
datalayer.edit_status = DataLayer.ANONYMOUS
datalayer.save()
assert datalayer.can_edit(datalayer.map.owner)
def test_editor_can_edit_in_public_mode(datalayer, user):
map = datalayer.map
map.editors.add(user)
map.save()
datalayer.edit_status = DataLayer.ANONYMOUS
datalayer.save()
assert datalayer.can_edit(user)
def test_anonymous_cannot_edit_in_anonymous_owner_mode(datalayer):
datalayer.edit_status = DataLayer.OWNER
datalayer.save()
map = datalayer.map
map.owner = None
map.save()
assert not datalayer.can_edit()
def test_owner_can_edit_in_inherit_mode_and_map_in_owner_mode(datalayer):
datalayer.edit_status = DataLayer.INHERIT
datalayer.save()
map = datalayer.map
map.edit_status = Map.OWNER
map.save()
assert datalayer.can_edit(map.owner)
def test_editors_cannot_edit_in_inherit_mode_and_map_in_owner_mode(datalayer, user):
datalayer.edit_status = DataLayer.INHERIT
datalayer.save()
map = datalayer.map
map.editors.add(user)
map.edit_status = Map.OWNER
map.save()
assert not datalayer.can_edit(user)
def test_anonymous_cannot_edit_in_inherit_mode_and_map_in_owner_mode(datalayer):
datalayer.edit_status = DataLayer.INHERIT
datalayer.save()
map = datalayer.map
map.edit_status = Map.OWNER
map.save()
assert not datalayer.can_edit()
def test_owner_can_edit_in_inherit_mode_and_map_in_editors_mode(datalayer):
datalayer.edit_status = DataLayer.INHERIT
datalayer.save()
map = datalayer.map
map.edit_status = Map.EDITORS
map.save()
assert datalayer.can_edit(map.owner)
def test_editors_can_edit_in_inherit_mode_and_map_in_editors_mode(datalayer, user):
datalayer.edit_status = DataLayer.INHERIT
datalayer.save()
map = datalayer.map
map.editors.add(user)
map.edit_status = Map.EDITORS
map.save()
assert datalayer.can_edit(user)
def test_anonymous_cannot_edit_in_inherit_mode_and_map_in_editors_mode(datalayer):
datalayer.edit_status = DataLayer.INHERIT
datalayer.save()
map = datalayer.map
map.edit_status = Map.EDITORS
map.save()
assert not datalayer.can_edit()
def test_anonymous_can_edit_in_inherit_mode_and_map_in_public_mode(datalayer):
datalayer.edit_status = DataLayer.INHERIT
datalayer.save()
map = datalayer.map
map.edit_status = Map.ANONYMOUS
map.save()
assert datalayer.can_edit()

View file

@ -245,3 +245,143 @@ def test_update_readonly(client, datalayer, map, post_data, settings):
client.login(username=map.owner.username, password="123123")
response = client.post(url, post_data, follow=True)
assert response.status_code == 403
@pytest.mark.usefixtures("allow_anonymous")
def test_anonymous_owner_can_edit_in_anonymous_owner_mode(
datalayer, cookieclient, anonymap, post_data
):
datalayer.edit_status = DataLayer.OWNER
datalayer.save()
url = reverse("datalayer_update", args=(anonymap.pk, datalayer.pk))
name = "new name"
post_data["name"] = name
response = cookieclient.post(url, post_data, follow=True)
assert response.status_code == 200
modified_datalayer = DataLayer.objects.get(pk=datalayer.pk)
assert modified_datalayer.name == name
@pytest.mark.usefixtures("allow_anonymous")
def test_anonymous_can_edit_in_anonymous_owner_but_public_mode(
datalayer, client, anonymap, post_data
):
datalayer.edit_status = DataLayer.ANONYMOUS
datalayer.save()
url = reverse("datalayer_update", args=(anonymap.pk, datalayer.pk))
name = "new name"
post_data["name"] = name
response = client.post(url, post_data, follow=True)
assert response.status_code == 200
modified_datalayer = DataLayer.objects.get(pk=datalayer.pk)
assert modified_datalayer.name == name
@pytest.mark.usefixtures("allow_anonymous")
def test_anonymous_cannot_edit_in_anonymous_owner_mode(
datalayer, client, anonymap, post_data
):
datalayer.edit_status = DataLayer.OWNER
datalayer.save()
url = reverse("datalayer_update", args=(anonymap.pk, datalayer.pk))
name = "new name"
post_data["name"] = name
response = client.post(url, post_data, follow=True)
assert response.status_code == 403
def test_anonymous_cannot_edit_in_owner_mode(datalayer, client, map, post_data):
datalayer.edit_status = DataLayer.OWNER
datalayer.save()
url = reverse("datalayer_update", args=(map.pk, datalayer.pk))
name = "new name"
post_data["name"] = name
response = client.post(url, post_data, follow=True)
assert response.status_code == 403
def test_anonymous_can_edit_in_owner_but_public_mode(datalayer, client, map, post_data):
datalayer.edit_status = DataLayer.ANONYMOUS
datalayer.save()
url = reverse("datalayer_update", args=(map.pk, datalayer.pk))
name = "new name"
post_data["name"] = name
response = client.post(url, post_data, follow=True)
assert response.status_code == 200
modified_datalayer = DataLayer.objects.get(pk=datalayer.pk)
assert modified_datalayer.name == name
def test_owner_can_edit_in_owner_mode(datalayer, client, map, post_data):
client.login(username=map.owner.username, password="123123")
datalayer.edit_status = DataLayer.OWNER
datalayer.save()
url = reverse("datalayer_update", args=(map.pk, datalayer.pk))
name = "new name"
post_data["name"] = name
response = client.post(url, post_data, follow=True)
assert response.status_code == 200
modified_datalayer = DataLayer.objects.get(pk=datalayer.pk)
assert modified_datalayer.name == name
def test_editor_can_edit_in_editors_mode(datalayer, client, map, post_data):
client.login(username=map.owner.username, password="123123")
datalayer.edit_status = DataLayer.EDITORS
datalayer.save()
url = reverse("datalayer_update", args=(map.pk, datalayer.pk))
name = "new name"
post_data["name"] = name
response = client.post(url, post_data, follow=True)
assert response.status_code == 200
modified_datalayer = DataLayer.objects.get(pk=datalayer.pk)
assert modified_datalayer.name == name
@pytest.mark.usefixtures("allow_anonymous")
def test_anonymous_owner_can_edit_if_inherit_and_map_in_owner_mode(
datalayer, cookieclient, anonymap, post_data
):
anonymap.edit_status = Map.OWNER
anonymap.save()
datalayer.edit_status = DataLayer.INHERIT
datalayer.save()
url = reverse("datalayer_update", args=(anonymap.pk, datalayer.pk))
name = "new name"
post_data["name"] = name
response = cookieclient.post(url, post_data, follow=True)
assert response.status_code == 200
modified_datalayer = DataLayer.objects.get(pk=datalayer.pk)
assert modified_datalayer.name == name
@pytest.mark.usefixtures("allow_anonymous")
def test_anonymous_user_cannot_edit_if_inherit_and_map_in_owner_mode(
datalayer, client, anonymap, post_data
):
anonymap.edit_status = Map.OWNER
anonymap.save()
datalayer.edit_status = DataLayer.INHERIT
datalayer.save()
url = reverse("datalayer_update", args=(anonymap.pk, datalayer.pk))
name = "new name"
post_data["name"] = name
response = client.post(url, post_data, follow=True)
assert response.status_code == 403
@pytest.mark.usefixtures("allow_anonymous")
def test_anonymous_user_can_edit_if_inherit_and_map_in_public_mode(
datalayer, client, anonymap, post_data
):
anonymap.edit_status = Map.ANONYMOUS
anonymap.save()
datalayer.edit_status = DataLayer.INHERIT
datalayer.save()
url = reverse("datalayer_update", args=(anonymap.pk, datalayer.pk))
name = "new name"
post_data["name"] = name
response = client.post(url, post_data, follow=True)
assert response.status_code == 200
modified_datalayer = DataLayer.objects.get(pk=datalayer.pk)
assert modified_datalayer.name == name

View file

@ -60,6 +60,13 @@ def test_logged_in_user_should_be_allowed_for_anonymous_map_with_anonymous_edit_
assert map.can_edit(user, request)
def test_anonymous_user_should_not_be_allowed_for_anonymous_map(map, user, rf): # noqa
map.owner = None
map.edit_status = map.OWNER
map.save()
assert not map.can_edit()
def test_clone_should_return_new_instance(map, user):
clone = map.clone()
assert map.pk != clone.pk

View file

@ -128,9 +128,9 @@ def test_wrong_slug_should_redirect_to_canonical(client, map):
def test_wrong_slug_should_redirect_with_query_string(client, map):
url = reverse("map", kwargs={"map_id": map.pk, "slug": "wrong-slug"})
url = "{}?allowEdit=0".format(url)
url = "{}?editMode=simple".format(url)
canonical = reverse("map", kwargs={"map_id": map.pk, "slug": map.slug})
canonical = "{}?allowEdit=0".format(canonical)
canonical = "{}?editMode=simple".format(canonical)
response = client.get(url)
assert response.status_code == 301
assert response["Location"] == canonical
@ -138,7 +138,7 @@ def test_wrong_slug_should_redirect_with_query_string(client, map):
def test_should_not_consider_the_query_string_for_canonical_check(client, map):
url = reverse("map", kwargs={"map_id": map.pk, "slug": map.slug})
url = "{}?allowEdit=0".format(url)
url = "{}?editMode=simple".format(url)
response = client.get(url)
assert response.status_code == 200

View file

@ -13,7 +13,7 @@ from . import views
from .decorators import (
jsonize_view,
login_required_if_not_anonymous_allowed,
map_permissions_check,
can_edit_map,
can_view_map,
)
from .utils import decorated_patterns
@ -144,16 +144,16 @@ map_urls = [
views.DataLayerCreate.as_view(),
name="datalayer_create",
),
re_path(
r"^map/(?P<map_id>[\d]+)/datalayer/update/(?P<pk>\d+)/$",
views.DataLayerUpdate.as_view(),
name="datalayer_update",
),
re_path(
r"^map/(?P<map_id>[\d]+)/datalayer/delete/(?P<pk>\d+)/$",
views.DataLayerDelete.as_view(),
name="datalayer_delete",
),
re_path(
r"^map/(?P<map_id>[\d]+)/datalayer/permissions/(?P<pk>\d+)/$",
views.UpdateDataLayerPermissions.as_view(),
name="datalayer_permissions",
),
]
if settings.FROM_EMAIL:
map_urls.append(
@ -163,7 +163,15 @@ if settings.FROM_EMAIL:
name="map_send_edit_link",
)
)
i18n_urls += decorated_patterns([map_permissions_check, never_cache], *map_urls)
datalayer_urls = [
re_path(
r"^map/(?P<map_id>[\d]+)/datalayer/update/(?P<pk>\d+)/$",
views.DataLayerUpdate.as_view(),
name="datalayer_update",
),
]
i18n_urls += decorated_patterns([can_edit_map, never_cache], *map_urls)
i18n_urls += decorated_patterns([never_cache], *datalayer_urls)
urlpatterns += i18n_patterns(
re_path(r"^$", views.home, name="home"),
re_path(

View file

@ -45,8 +45,10 @@ from .forms import (
DEFAULT_LATITUDE,
DEFAULT_LONGITUDE,
DEFAULT_CENTER,
AnonymousMapPermissionsForm,
DataLayerForm,
DataLayerPermissionsForm,
AnonymousDataLayerPermissionsForm,
AnonymousMapPermissionsForm,
FlatErrorList,
MapSettingsForm,
SendLinkForm,
@ -445,23 +447,33 @@ class MapDetailMixin:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user = self.request.user
properties = {
"urls": _urls_for_js(),
"tilelayers": TileLayer.get_list(),
"allowEdit": self.is_edit_allowed(),
"editMode": self.edit_mode,
"default_iconUrl": "%sumap/img/marker.png" % settings.STATIC_URL, # noqa
"umap_id": self.get_umap_id(),
"starred": self.is_starred(),
"licences": dict((l.name, l.json) for l in Licence.objects.all()),
"edit_statuses": [(i, str(label)) for i, label in Map.EDIT_STATUS],
"share_statuses": [
(i, str(label)) for i, label in Map.SHARE_STATUS if i != Map.BLOCKED
],
"anonymous_edit_statuses": [
(i, str(label)) for i, label in AnonymousMapPermissionsForm.STATUS
],
"umap_version": VERSION,
}
created = bool(getattr(self, "object", None))
if (created and self.object.owner) or (not created and not user.is_anonymous):
map_statuses = Map.EDIT_STATUS
datalayer_statuses = DataLayer.EDIT_STATUS
else:
map_statuses = AnonymousMapPermissionsForm.STATUS
datalayer_statuses = AnonymousDataLayerPermissionsForm.STATUS
properties["edit_statuses"] = [
(i, str(label)) for i, label in map_statuses
]
properties["datalayer_edit_statuses"] = [
(i, str(label)) for i, label in datalayer_statuses
]
if self.get_short_url():
properties["shortUrl"] = self.get_short_url()
@ -474,7 +486,6 @@ class MapDetailMixin:
locale = to_locale(lang)
properties["locale"] = locale
context["locale"] = locale
user = self.request.user
if not user.is_anonymous:
properties["user"] = {
"id": user.pk,
@ -492,8 +503,9 @@ class MapDetailMixin:
def get_datalayers(self):
return []
def is_edit_allowed(self):
return True
@property
def edit_mode(self):
return "advanced"
def get_umap_id(self):
return None
@ -551,11 +563,22 @@ class MapView(MapDetailMixin, PermissionsMixin, DetailView):
return self.object.get_absolute_url()
def get_datalayers(self):
datalayers = DataLayer.objects.filter(map=self.object)
return [l.metadata for l in datalayers]
return [
l.metadata(self.request.user, self.request)
for l in self.object.datalayer_set.all()
]
def is_edit_allowed(self):
return self.object.can_edit(self.request.user, self.request)
@property
def edit_mode(self):
edit_mode = "disabled"
if self.object.can_edit(self.request.user, self.request):
edit_mode = "advanced"
elif any(
d.can_edit(self.request.user, self.request)
for d in self.object.datalayer_set.all()
):
edit_mode = "simple"
return edit_mode
def get_umap_id(self):
return self.object.pk
@ -883,7 +906,9 @@ class DataLayerCreate(FormLessEditMixin, GZipMixin, CreateView):
form.instance.map = self.kwargs["map_inst"]
self.object = form.save()
# Simple response with only metadatas (including new id)
response = simple_json_response(**self.object.metadata)
response = simple_json_response(
**self.object.metadata(self.request.user, self.request)
)
response["Last-Modified"] = self.last_modified
return response
@ -896,7 +921,9 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
self.object = form.save()
# Simple response with only metadatas (client should not reload all data
# on save)
response = simple_json_response(**self.object.metadata)
response = simple_json_response(
**self.object.metadata(self.request.user, self.request)
)
response["Last-Modified"] = self.last_modified
return response
@ -911,7 +938,9 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.map != self.kwargs["map_inst"]:
if self.object.map.pk != int(self.kwargs["map_id"]):
return HttpResponseForbidden()
if not self.object.can_edit(user=self.request.user, request=self.request):
return HttpResponseForbidden()
if not self.is_unmodified():
return HttpResponse(status=412)
@ -936,6 +965,21 @@ class DataLayerVersions(BaseDetailView):
return simple_json_response(versions=self.object.versions)
class UpdateDataLayerPermissions(FormLessEditMixin, UpdateView):
model = DataLayer
pk_url_kwarg = "pk"
def get_form_class(self):
if self.object.map.owner:
return DataLayerPermissionsForm
else:
return AnonymousDataLayerPermissionsForm
def form_valid(self, form):
self.object = form.save()
return simple_json_response(info=_("Permissions updated with success!"))
# ############## #
# Picto #
# ############## #