diff --git a/umap/forms.py b/umap/forms.py
index dc16f096..af937ed7 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,83 @@ 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)
+ 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.ANONYMOUS, _("Everyone can edit")),
+ (Map.OWNER, _("Only editable with secret edit link")),
)
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 = (
+ (Map.ANONYMOUS, _("Everyone can edit")),
+ (Map.OWNER, _("Only editable with secret edit link")),
+ )
+
+ 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..16f3691f
--- /dev/null
+++ b/umap/migrations/0013_datalayer_edit_status.py
@@ -0,0 +1,22 @@
+# Generated by Django 4.2.2 on 2023-09-07 06:27
+
+from django.db import migrations, models
+import umap.models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("umap", "0012_datalayer_settings"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="datalayer",
+ name="edit_status",
+ field=models.SmallIntegerField(
+ choices=[(1, "Everyone"), (2, "Editors only"), (3, "Owner only")],
+ default=umap.models.get_default_edit_status,
+ verbose_name="edit status",
+ ),
+ ),
+ ]
diff --git a/umap/models.py b/umap/models.py
index 7b804642..fbeaa559 100644
--- a/umap/models.py
+++ b/umap/models.py
@@ -216,6 +216,9 @@ class Map(NamedModel):
"""
Define if a user can edit or not the instance, according to his account
or the request.
+
+ In ownership mode: only owner and editors
+ In anononymous mode: only "anonymous owners" (having edit cookie set)
"""
can = False
if request and not self.owner:
@@ -223,13 +226,9 @@ class Map(NamedModel):
settings, "UMAP_ALLOW_ANONYMOUS", False
) and self.is_anonymous_owner(request):
can = True
- if self.edit_status == self.ANONYMOUS:
+ if user == self.owner:
can = True
- elif not user.is_authenticated:
- pass
- elif user == self.owner:
- can = True
- elif self.edit_status == self.EDITORS and user in self.editors.all():
+ elif user in self.editors.all():
can = True
return can
@@ -303,6 +302,15 @@ class DataLayer(NamedModel):
Layer to store Features in.
"""
+ ANONYMOUS = 1
+ EDITORS = 2
+ OWNER = 3
+ EDIT_STATUS = (
+ (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 +323,11 @@ class DataLayer(NamedModel):
settings = models.JSONField(
blank=True, null=True, verbose_name=_("settings"), default=dict
)
+ edit_status = models.SmallIntegerField(
+ choices=EDIT_STATUS,
+ default=get_default_edit_status,
+ verbose_name=_("edit status"),
+ )
class Meta:
ordering = ("rank",)
@@ -346,8 +359,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 +367,8 @@ class DataLayer(NamedModel):
"displayOnLoad": self.display_on_load,
}
obj["id"] = self.pk
+ obj["permissions"] = {"edit_status": self.edit_status}
+ obj["allowEdit"] = self.can_edit(user, request)
return obj
def clone(self, map_inst=None):
@@ -413,6 +427,19 @@ 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.
+ """
+ can = self.map.can_edit(user, request)
+ if can:
+ # Owner or editor, no need for further checks.
+ return can
+ if self.edit_status == self.ANONYMOUS:
+ can = True
+ return can
+
class Star(models.Model):
at = models.DateTimeField(auto_now=True)
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..2ee16bab
--- /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')} "${this.datalayer.getName()}"`,
+ selectOptions: this.datalayer.map.options.edit_statuses,
+ },
+ ],
+ ],
+ builder = new L.U.FormBuilder(this, fields),
+ 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..8063f325 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.isRemoteLayer() || this.datalayer.isReadOnly())
},
getSlug: function () {
diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js
index 0a83d688..6568ba23 100644
--- a/umap/static/umap/js/umap.layer.js
+++ b/umap/static/umap/js/umap.layer.js
@@ -261,6 +261,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 +351,13 @@ L.U.DataLayer = L.Evented.extend({
this.map.get(this._dataUrl(), {
callback: function (geojson, response) {
this._last_modified = response.getResponseHeader('Last-Modified')
+ console.log(this.getName(), this.options)
+ // 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']['allowEdit'] = this.options.allowEdit
this.fromUmapGeoJSON(geojson)
this.backupOptions()
this.fire('loaded')
@@ -1182,18 +1190,14 @@ 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 () {
+ return !this.options.allowEdit
+ },
+
save: function () {
if (this.isDeleted) return this.saveDelete()
if (!this.isLoaded()) {
@@ -1220,7 +1224,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..13dcb750 100644
--- a/umap/static/umap/js/umap.permissions.js
+++ b/umap/static/umap/js/umap.permissions.js
@@ -122,6 +122,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' })
},
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..3e7ae9c0 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'] = {}
diff --git a/umap/urls.py b/umap/urls.py
index a44aa11d..ee963ff7 100644
--- a/umap/urls.py
+++ b/umap/urls.py
@@ -154,6 +154,11 @@ map_urls = [
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(
diff --git a/umap/views.py b/umap/views.py
index 67d899b4..a07fbd56 100644
--- a/umap/views.py
+++ b/umap/views.py
@@ -47,6 +47,8 @@ from .forms import (
DEFAULT_CENTER,
AnonymousMapPermissionsForm,
DataLayerForm,
+ DataLayerPermissionsForm,
+ AnonymousDataLayerPermissionsForm,
FlatErrorList,
MapSettingsForm,
SendLinkForm,
@@ -551,11 +553,16 @@ 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)
+ return self.object.can_edit(self.request.user, self.request) or any(
+ d.can_edit(self.request.user, self.request)
+ for d in self.object.datalayer_set.all()
+ )
def get_umap_id(self):
return self.object.pk
@@ -883,7 +890,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 +905,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
@@ -936,6 +947,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 #
# ############## #