WIP: move edit_status from Map to DataLayer
This commit is contained in:
parent
73d19e849f
commit
89ab029cab
11 changed files with 228 additions and 51 deletions
|
@ -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")
|
||||
|
|
22
umap/migrations/0013_datalayer_edit_status.py
Normal file
22
umap/migrations/0013_datalayer_edit_status.py
Normal file
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
70
umap/static/umap/js/umap.datalayer.permissions.js
Normal file
70
umap/static/umap/js/umap.datalayer.permissions.js
Normal 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')} "${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)
|
||||
},
|
||||
})
|
|
@ -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 () {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' })
|
||||
},
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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'] = {}
|
||||
|
|
|
@ -154,6 +154,11 @@ map_urls = [
|
|||
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(
|
||||
|
|
|
@ -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 #
|
||||
# ############## #
|
||||
|
|
Loading…
Reference in a new issue