diff --git a/.gitignore b/.gitignore index d236ae74..5b5d15b3 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ __pycache__/ build/ dist/ *.egg-info/ - +playwright/.auth/ +test-results/ diff --git a/pyproject.toml b/pyproject.toml index 8a99f166..a0869ca9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/pytest.ini b/pytest.ini index d9fcbd49..610ba029 100644 --- a/pytest.ini +++ b/pytest.ini @@ -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 diff --git a/umap/decorators.py b/umap/decorators.py index c096c1f4..b9b5232a 100644 --- a/umap/decorators.py +++ b/umap/decorators.py @@ -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) diff --git a/umap/forms.py b/umap/forms.py index dc16f096..ec94e7a8 100644 --- a/umap/forms.py +++ b/umap/forms.py @@ -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") diff --git a/umap/migrations/0013_datalayer_edit_status.py b/umap/migrations/0013_datalayer_edit_status.py new file mode 100644 index 00000000..c62523df --- /dev/null +++ b/umap/migrations/0013_datalayer_edit_status.py @@ -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", + ), + ), + ] diff --git a/umap/models.py b/umap/models.py index 7b804642..86c69518 100644 --- a/umap/models.py +++ b/umap/models.py @@ -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) diff --git a/umap/static/umap/img/16.svg b/umap/static/umap/img/16.svg index 6fd25734..0c9fccc1 100644 --- a/umap/static/umap/img/16.svg +++ b/umap/static/umap/img/16.svg @@ -37,7 +37,6 @@ - diff --git a/umap/static/umap/img/source/16.svg b/umap/static/umap/img/source/16.svg index 56879165..5009bbcc 100644 --- a/umap/static/umap/img/source/16.svg +++ b/umap/static/umap/img/source/16.svg @@ -1,10 +1,10 @@ - + - - + + @@ -55,7 +55,6 @@ - diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 696e7c3f..2c67911c 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -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, diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index b03979f8..a9398945 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -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) { diff --git a/umap/static/umap/js/umap.datalayer.permissions.js b/umap/static/umap/js/umap.datalayer.permissions.js new file mode 100644 index 00000000..9e3e6211 --- /dev/null +++ b/umap/static/umap/js/umap.datalayer.permissions.js @@ -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) + }, +}) diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index 40ec7927..c3f2e75c 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -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 () { diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 587549a5..37c7c85f 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -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()]) } }) diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 27414e33..784b0f3c 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -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) { diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js index 0a83d688..bc4b5684 100644 --- a/umap/static/umap/js/umap.layer.js +++ b/umap/static/umap/js/umap.layer.js @@ -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 diff --git a/umap/static/umap/js/umap.permissions.js b/umap/static/umap/js/umap.permissions.js index 0454855d..7e277503 100644 --- a/umap/static/umap/js/umap.permissions.js +++ b/umap/static/umap/js/umap.permissions.js @@ -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:
{link}', { - link: this.options.anonymous_edit_url, - }) + const helpText = `${L._('Secret edit link:')}
${ + 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 + ] + }, }) diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css index d830fc0a..3ffe7c52 100644 --- a/umap/static/umap/map.css +++ b/umap/static/umap/map.css @@ -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; } diff --git a/umap/static/umap/test/Map.Export.js b/umap/static/umap/test/Map.Export.js index 1c17a803..09ce3588 100644 --- a/umap/static/umap/test/Map.Export.js +++ b/umap/static/umap/test/Map.Export.js @@ -207,6 +207,7 @@ describe('L.U.Map.Export', function () { _umap_options: { displayOnLoad: true, browsable: true, + editMode: 'advanced', iconClass: 'Default', name: 'Elephants', id: 62, diff --git a/umap/static/umap/test/Map.js b/umap/static/umap/test/Map.js index c6a9eb15..9bcc7cfb 100644 --- a/umap/static/umap/test/Map.js +++ b/umap/static/umap/test/Map.js @@ -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() + }) }) diff --git a/umap/static/umap/test/Permissions.js b/umap/static/umap/test/Permissions.js index 5f4364ac..4fb2ae70 100644 --- a/umap/static/umap/test/Permissions.js +++ b/umap/static/umap/test/Permissions.js @@ -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 }) diff --git a/umap/static/umap/test/_pre.js b/umap/static/umap/test/_pre.js index df561c5d..dda0c79e 100644 --- a/umap/static/umap/test/_pre.js +++ b/umap/static/umap/test/_pre.js @@ -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: { diff --git a/umap/static/umap/test/index.html b/umap/static/umap/test/index.html index cf4928f2..67a8aa59 100644 --- a/umap/static/umap/test/index.html +++ b/umap/static/umap/test/index.html @@ -37,6 +37,7 @@ + diff --git a/umap/templates/umap/js.html b/umap/templates/umap/js.html index 566ec658..5b321fc4 100644 --- a/umap/templates/umap/js.html +++ b/umap/templates/umap/js.html @@ -34,11 +34,12 @@ + + - {% endcompress %} diff --git a/umap/templatetags/umap_tags.py b/umap/templatetags/umap_tags.py index c40691d1..70bc244e 100644 --- a/umap/templatetags/umap_tags.py +++ b/umap/templatetags/umap_tags.py @@ -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, diff --git a/umap/tests/base.py b/umap/tests/base.py index 0beab93a..2b7703e4 100644 --- a/umap/tests/base.py +++ b/umap/tests/base.py @@ -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 diff --git a/umap/tests/conftest.py b/umap/tests/conftest.py index 6d168835..3465994b 100644 --- a/umap/tests/conftest.py +++ b/umap/tests/conftest.py @@ -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 diff --git a/umap/tests/integration/test_anonymous_owned_map.py b/umap/tests/integration/test_anonymous_owned_map.py new file mode 100644 index 00000000..daf999a0 --- /dev/null +++ b/umap/tests/integration/test_anonymous_owned_map.py @@ -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") diff --git a/umap/tests/integration/test_map.py b/umap/tests/integration/test_map.py new file mode 100644 index 00000000..a5dd0dbc --- /dev/null +++ b/umap/tests/integration/test_map.py @@ -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) diff --git a/umap/tests/integration/test_owned_map.py b/umap/tests/integration/test_owned_map.py new file mode 100644 index 00000000..75becdcc --- /dev/null +++ b/umap/tests/integration/test_owned_map.py @@ -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 diff --git a/umap/tests/test_datalayer.py b/umap/tests/test_datalayer.py index 5818a541..3464d430 100644 --- a/umap/tests/test_datalayer.py +++ b/umap/tests/test_datalayer.py @@ -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() diff --git a/umap/tests/test_datalayer_views.py b/umap/tests/test_datalayer_views.py index 43a6d49d..46b94f07 100644 --- a/umap/tests/test_datalayer_views.py +++ b/umap/tests/test_datalayer_views.py @@ -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 diff --git a/umap/tests/test_map.py b/umap/tests/test_map.py index ae193700..d0bb52e7 100644 --- a/umap/tests/test_map.py +++ b/umap/tests/test_map.py @@ -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 diff --git a/umap/tests/test_map_views.py b/umap/tests/test_map_views.py index ab8519ab..01e78c59 100644 --- a/umap/tests/test_map_views.py +++ b/umap/tests/test_map_views.py @@ -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 diff --git a/umap/urls.py b/umap/urls.py index a44aa11d..630ea45a 100644 --- a/umap/urls.py +++ b/umap/urls.py @@ -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[\d]+)/datalayer/update/(?P\d+)/$", - views.DataLayerUpdate.as_view(), - name="datalayer_update", - ), re_path( r"^map/(?P[\d]+)/datalayer/delete/(?P\d+)/$", views.DataLayerDelete.as_view(), name="datalayer_delete", ), + re_path( + r"^map/(?P[\d]+)/datalayer/permissions/(?P\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[\d]+)/datalayer/update/(?P\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( diff --git a/umap/views.py b/umap/views.py index 67d899b4..da460047 100644 --- a/umap/views.py +++ b/umap/views.py @@ -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 # # ############## #