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 @@
-