Merge pull request #1307 from umap-project/datalayer-editstatus
Allow to define permissions for each datalayer instead of for the whole map
This commit is contained in:
commit
ffae06aac7
36 changed files with 1168 additions and 194 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -17,4 +17,5 @@ __pycache__/
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
playwright/.auth/
|
||||||
|
test-results/
|
||||||
|
|
|
@ -53,7 +53,7 @@ dev = [
|
||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
"factory-boy==3.2.1",
|
"factory-boy==3.2.1",
|
||||||
"playwright==1.37.0",
|
"playwright==1.38.0",
|
||||||
"pytest==6.2.5",
|
"pytest==6.2.5",
|
||||||
"pytest-django==4.5.2",
|
"pytest-django==4.5.2",
|
||||||
"pytest-playwright==0.4.2",
|
"pytest-playwright==0.4.2",
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
[pytest]
|
[pytest]
|
||||||
DJANGO_SETTINGS_MODULE=umap.tests.settings
|
DJANGO_SETTINGS_MODULE=umap.tests.settings
|
||||||
addopts = "--pdbcls=IPython.terminal.debugger:Pdb --no-migrations"
|
addopts = --pdbcls=IPython.terminal.debugger:Pdb --no-migrations
|
||||||
|
|
|
@ -26,9 +26,9 @@ def login_required_if_not_anonymous_allowed(view_func):
|
||||||
return wrapper
|
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)
|
@wraps(view_func)
|
||||||
|
|
|
@ -8,8 +8,12 @@ from django.forms.utils import ErrorList
|
||||||
|
|
||||||
from .models import Map, DataLayer
|
from .models import Map, DataLayer
|
||||||
|
|
||||||
DEFAULT_LATITUDE = settings.LEAFLET_LATITUDE if hasattr(settings, "LEAFLET_LATITUDE") else 51
|
DEFAULT_LATITUDE = (
|
||||||
DEFAULT_LONGITUDE = settings.LEAFLET_LONGITUDE if hasattr(settings, "LEAFLET_LONGITUDE") else 2
|
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)
|
DEFAULT_CENTER = Point(DEFAULT_LONGITUDE, DEFAULT_LATITUDE)
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
@ -21,8 +25,8 @@ class FlatErrorList(ErrorList):
|
||||||
|
|
||||||
def flat(self):
|
def flat(self):
|
||||||
if not self:
|
if not self:
|
||||||
return u''
|
return ""
|
||||||
return u' — '.join([e for e in self])
|
return " — ".join([e for e in self])
|
||||||
|
|
||||||
|
|
||||||
class SendLinkForm(forms.Form):
|
class SendLinkForm(forms.Form):
|
||||||
|
@ -30,69 +34,79 @@ class SendLinkForm(forms.Form):
|
||||||
|
|
||||||
|
|
||||||
class UpdateMapPermissionsForm(forms.ModelForm):
|
class UpdateMapPermissionsForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Map
|
model = Map
|
||||||
fields = ('edit_status', 'editors', 'share_status', 'owner')
|
fields = ("edit_status", "editors", "share_status", "owner")
|
||||||
|
|
||||||
|
|
||||||
class AnonymousMapPermissionsForm(forms.ModelForm):
|
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 = (
|
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)
|
edit_status = forms.ChoiceField(choices=STATUS)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Map
|
model = Map
|
||||||
fields = ('edit_status', )
|
fields = ("edit_status",)
|
||||||
|
|
||||||
|
|
||||||
class DataLayerForm(forms.ModelForm):
|
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:
|
class Meta:
|
||||||
model = DataLayer
|
model = DataLayer
|
||||||
fields = ('geojson', 'name', 'display_on_load', 'rank', 'settings')
|
fields = ("edit_status",)
|
||||||
|
|
||||||
|
|
||||||
class MapSettingsForm(forms.ModelForm):
|
class MapSettingsForm(forms.ModelForm):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(MapSettingsForm, self).__init__(*args, **kwargs)
|
super(MapSettingsForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['slug'].required = False
|
self.fields["slug"].required = False
|
||||||
self.fields['center'].widget.map_srid = 4326
|
self.fields["center"].widget.map_srid = 4326
|
||||||
|
|
||||||
def clean_slug(self):
|
def clean_slug(self):
|
||||||
slug = self.cleaned_data.get('slug', None)
|
slug = self.cleaned_data.get("slug", None)
|
||||||
name = self.cleaned_data.get('name', None)
|
name = self.cleaned_data.get("name", None)
|
||||||
if not slug and name:
|
if not slug and name:
|
||||||
# If name is empty, don't do nothing, validation will raise
|
# If name is empty, don't do nothing, validation will raise
|
||||||
# later on the process because name is required
|
# later on the process because name is required
|
||||||
self.cleaned_data['slug'] = slugify(name) or "map"
|
self.cleaned_data["slug"] = slugify(name) or "map"
|
||||||
return self.cleaned_data['slug'][:50]
|
return self.cleaned_data["slug"][:50]
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def clean_center(self):
|
def clean_center(self):
|
||||||
if not self.cleaned_data['center']:
|
if not self.cleaned_data["center"]:
|
||||||
point = DEFAULT_CENTER
|
point = DEFAULT_CENTER
|
||||||
self.cleaned_data['center'] = point
|
self.cleaned_data["center"] = point
|
||||||
return self.cleaned_data['center']
|
return self.cleaned_data["center"]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
fields = ('settings', 'name', 'center', 'slug')
|
fields = ("settings", "name", "center", "slug")
|
||||||
model = Map
|
model = Map
|
||||||
|
|
||||||
|
|
||||||
class UserProfileForm(forms.ModelForm):
|
class UserProfileForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = ('username', 'first_name', 'last_name')
|
fields = ("username", "first_name", "last_name")
|
||||||
|
|
26
umap/migrations/0013_datalayer_edit_status.py
Normal file
26
umap/migrations/0013_datalayer_edit_status.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# Generated by Django 4.2.2 on 2023-09-19 06:20
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("umap", "0012_datalayer_settings"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="datalayer",
|
||||||
|
name="edit_status",
|
||||||
|
field=models.SmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(0, "Inherit"),
|
||||||
|
(1, "Everyone"),
|
||||||
|
(2, "Editors only"),
|
||||||
|
(3, "Owner only"),
|
||||||
|
],
|
||||||
|
default=0,
|
||||||
|
verbose_name="edit status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -202,7 +202,7 @@ class Map(NamedModel):
|
||||||
return settings.SITE_URL + path
|
return settings.SITE_URL + path
|
||||||
|
|
||||||
def is_anonymous_owner(self, request):
|
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
|
# edit cookies are only valid while map hasn't owner
|
||||||
return False
|
return False
|
||||||
key, value = self.signed_cookie_elements
|
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
|
Define if a user can edit or not the instance, according to his account
|
||||||
or the request.
|
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
|
can = False
|
||||||
if request and not self.owner:
|
if request and not self.owner:
|
||||||
if getattr(
|
if settings.UMAP_ALLOW_ANONYMOUS and self.is_anonymous_owner(request):
|
||||||
settings, "UMAP_ALLOW_ANONYMOUS", False
|
|
||||||
) and self.is_anonymous_owner(request):
|
|
||||||
can = True
|
can = True
|
||||||
if self.edit_status == self.ANONYMOUS:
|
if self.edit_status == self.ANONYMOUS:
|
||||||
can = True
|
can = True
|
||||||
elif not user.is_authenticated:
|
elif user is None:
|
||||||
pass
|
can = False
|
||||||
elif user == self.owner:
|
elif user == self.owner:
|
||||||
can = True
|
can = True
|
||||||
elif self.edit_status == self.EDITORS and user in self.editors.all():
|
elif self.edit_status == self.EDITORS and user in self.editors.all():
|
||||||
|
@ -303,6 +309,17 @@ class DataLayer(NamedModel):
|
||||||
Layer to store Features in.
|
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)
|
map = models.ForeignKey(Map, on_delete=models.CASCADE)
|
||||||
description = models.TextField(blank=True, null=True, verbose_name=_("description"))
|
description = models.TextField(blank=True, null=True, verbose_name=_("description"))
|
||||||
geojson = models.FileField(upload_to=upload_to, blank=True, null=True)
|
geojson = models.FileField(upload_to=upload_to, blank=True, null=True)
|
||||||
|
@ -315,6 +332,11 @@ class DataLayer(NamedModel):
|
||||||
settings = models.JSONField(
|
settings = models.JSONField(
|
||||||
blank=True, null=True, verbose_name=_("settings"), default=dict
|
blank=True, null=True, verbose_name=_("settings"), default=dict
|
||||||
)
|
)
|
||||||
|
edit_status = models.SmallIntegerField(
|
||||||
|
choices=EDIT_STATUS,
|
||||||
|
default=INHERIT,
|
||||||
|
verbose_name=_("edit status"),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("rank",)
|
ordering = ("rank",)
|
||||||
|
@ -346,8 +368,7 @@ class DataLayer(NamedModel):
|
||||||
path.append(str(self.map.pk))
|
path.append(str(self.map.pk))
|
||||||
return os.path.join(*path)
|
return os.path.join(*path)
|
||||||
|
|
||||||
@property
|
def metadata(self, user=None, request=None):
|
||||||
def metadata(self):
|
|
||||||
# Retrocompat: minimal settings for maps not saved after settings property
|
# Retrocompat: minimal settings for maps not saved after settings property
|
||||||
# has been introduced
|
# has been introduced
|
||||||
obj = self.settings or {
|
obj = self.settings or {
|
||||||
|
@ -355,6 +376,8 @@ class DataLayer(NamedModel):
|
||||||
"displayOnLoad": self.display_on_load,
|
"displayOnLoad": self.display_on_load,
|
||||||
}
|
}
|
||||||
obj["id"] = self.pk
|
obj["id"] = self.pk
|
||||||
|
obj["permissions"] = {"edit_status": self.edit_status}
|
||||||
|
obj["editMode"] = "advanced" if self.can_edit(user, request) else 'disabled'
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def clone(self, map_inst=None):
|
def clone(self, map_inst=None):
|
||||||
|
@ -413,6 +436,25 @@ class DataLayer(NamedModel):
|
||||||
if name.startswith(f'{self.pk}_') and name.endswith(".gz"):
|
if name.startswith(f'{self.pk}_') and name.endswith(".gz"):
|
||||||
self.geojson.storage.delete(os.path.join(root, name))
|
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):
|
class Star(models.Model):
|
||||||
at = models.DateTimeField(auto_now=True)
|
at = models.DateTimeField(auto_now=True)
|
||||||
|
|
|
@ -37,7 +37,6 @@
|
||||||
<g id="text4356-2" transform="translate(44,-124)" fill="#fff" stroke="#000" stroke-width=".1">
|
<g id="text4356-2" transform="translate(44,-124)" fill="#fff" stroke="#000" stroke-width=".1">
|
||||||
<path id="path4384-2" d="m35.742 999.44 3.0762-3.0762-3.0664-3.0664 1.1914-1.1914 3.0664 3.0664 3.0566-3.0566 1.1719 1.1816-3.0469 3.0566 3.0664 3.0664-1.1914 1.1914-3.0664-3.0664-3.0762 3.0762-1.1816-1.1816" fill="#fff" stroke="#000" stroke-width=".1"/>
|
<path id="path4384-2" d="m35.742 999.44 3.0762-3.0762-3.0664-3.0664 1.1914-1.1914 3.0664 3.0664 3.0566-3.0566 1.1719 1.1816-3.0469 3.0566 3.0664 3.0664-1.1914 1.1914-3.0664-3.0664-3.0762 3.0762-1.1816-1.1816" fill="#fff" stroke="#000" stroke-width=".1"/>
|
||||||
</g>
|
</g>
|
||||||
<path id="table-0" d="m78 891.36v2h12v-2zm0 3v1h4v-1zm5 0v1h7v-1zm-5 2v1h4v-1zm5 0v1h7v-1zm-5 2v1h4v-1zm5 0v1h7v-1zm-5 2v1h4v-1zm5 0v1h7v-1z" fill="#b3b3b3"/>
|
|
||||||
<path id="path3684-2" d="m63.714 890.36-1.1428 1.1428 2.2857 2.2858 1.1428-1.1429zm-1.7143 1.7143-6.2857 6.2857 2.2857 2.2857 6.2857-6.2857zm-6.2857 6.2857-1.7143 4 4-1.7143z" fill="#b3b3b3"/>
|
<path id="path3684-2" d="m63.714 890.36-1.1428 1.1428 2.2857 2.2858 1.1428-1.1429zm-1.7143 1.7143-6.2857 6.2857 2.2857 2.2857 6.2857-6.2857zm-6.2857 6.2857-1.7143 4 4-1.7143z" fill="#b3b3b3"/>
|
||||||
<path id="table-5-8-8-6" d="m30 914.86v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1z" fill="#464646"/>
|
<path id="table-5-8-8-6" d="m30 914.86v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1z" fill="#464646"/>
|
||||||
<path id="table-5-8-8-6-1" d="m54 914.86v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1z" fill="#f2f2f2"/>
|
<path id="table-5-8-8-6-1" d="m54 914.86v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1zm-2 2v1h1v-1zm2 0v1h10v-1z" fill="#f2f2f2"/>
|
||||||
|
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
@ -1,10 +1,10 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
<svg width="144" height="144" id="svg2" version="1.1" inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" sodipodi:docname="16.svg" inkscape:export-filename="/home/ybon/Code/js/Leaflet.Storage/src/img/16.png" inkscape:export-xdpi="89.996864" inkscape:export-ydpi="89.996864" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
<svg width="144" height="144" id="svg2" version="1.1" inkscape:version="1.3 (0e150ed6c4, 2023-07-21)" sodipodi:docname="16.svg" inkscape:export-filename="/home/ybon/Code/js/Leaflet.Storage/src/img/16.png" inkscape:export-xdpi="89.996864" inkscape:export-ydpi="89.996864" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
<defs id="defs4" />
|
<defs id="defs4" />
|
||||||
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="6.4903658" inkscape:cx="86.975067" inkscape:cy="60.628324" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="true" inkscape:window-width="1920" inkscape:window-height="1019" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" showguides="true" inkscape:guide-bbox="true" inkscape:snap-grids="true" inkscape:snap-to-guides="true" inkscape:showpageshadow="2" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1">
|
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="6.4903658" inkscape:cx="86.89803" inkscape:cy="60.551287" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="true" inkscape:window-width="1920" inkscape:window-height="1019" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" showguides="true" inkscape:guide-bbox="true" inkscape:snap-grids="true" inkscape:snap-to-guides="true" inkscape:showpageshadow="2" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1">
|
||||||
<inkscape:grid type="xygrid" id="grid3004" empspacing="4" visible="true" enabled="true" snapvisiblegridlinesonly="true" originx="0" originy="0" />
|
<inkscape:grid type="xygrid" id="grid3004" empspacing="4" visible="true" enabled="true" snapvisiblegridlinesonly="true" originx="0" originy="0" spacingy="1" spacingx="1" units="px" />
|
||||||
<sodipodi:guide orientation="-1,0" position="24,144" id="guide3084" inkscape:locked="false" inkscape:label="" inkscape:color="rgb(0,134,229)" />
|
<sodipodi:guide orientation="-1,0" position="24,144" id="guide3084" inkscape:locked="false" inkscape:label="" inkscape:color="rgb(0,134,229)" />
|
||||||
<sodipodi:guide orientation="0,1" position="0,96" id="guide3086" inkscape:locked="false" inkscape:label="" inkscape:color="rgb(0,134,229)" />
|
<sodipodi:guide orientation="0,1" position="0,96" id="guide3086" inkscape:locked="false" inkscape:label="" inkscape:color="rgb(0,134,229)" />
|
||||||
<sodipodi:guide orientation="-1,0" position="48,144" id="guide3088" inkscape:locked="false" inkscape:label="" inkscape:color="rgb(0,134,229)" />
|
<sodipodi:guide orientation="-1,0" position="48,144" id="guide3088" inkscape:locked="false" inkscape:label="" inkscape:color="rgb(0,134,229)" />
|
||||||
|
@ -55,7 +55,6 @@
|
||||||
<g id="text4356-2" style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1" transform="translate(44,-124)">
|
<g id="text4356-2" style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1" transform="translate(44,-124)">
|
||||||
<path inkscape:connector-curvature="0" id="path4384-2" style="font-variant:normal;font-stretch:normal;font-size:20px;font-family:Arial;-inkscape-font-specification:Arial;fill:#ffffff;stroke:#000000;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1" d="m 35.742187,999.43835 3.076172,-3.07617 -3.066406,-3.0664 1.191406,-1.19141 3.066407,3.06641 3.05664,-3.05664 1.171875,1.18164 -3.046875,3.05664 3.066406,3.0664 -1.191406,1.19138 -3.066406,-3.06638 -3.076172,3.07618 -1.181641,-1.18165" />
|
<path inkscape:connector-curvature="0" id="path4384-2" style="font-variant:normal;font-stretch:normal;font-size:20px;font-family:Arial;-inkscape-font-specification:Arial;fill:#ffffff;stroke:#000000;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1" d="m 35.742187,999.43835 3.076172,-3.07617 -3.066406,-3.0664 1.191406,-1.19141 3.066407,3.06641 3.05664,-3.05664 1.171875,1.18164 -3.046875,3.05664 3.066406,3.0664 -1.191406,1.19138 -3.066406,-3.06638 -3.076172,3.07618 -1.181641,-1.18165" />
|
||||||
</g>
|
</g>
|
||||||
<path style="fill:#b3b3b3;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 78,891.36216 v 2 h 12 v -2 z m 0,3.00002 v 1 h 4 v -1 z m 5,0 v 1 h 7 v -1 z m -5,2 v 1 h 4 v -1 z m 5,0 v 1 h 7 v -1 z m -5,2 v 1 h 4 v -1 z m 5,0 v 1 h 7 v -1 z m -5,2 v 1 h 4 v -1 z m 5,0 v 1 h 7 v -1 z" id="table-0" inkscape:connector-curvature="0" inkscape:label="table" inkscape:export-filename="/home/ybon/Code/js/leaflet-storage/src/img/browse-data.png" inkscape:export-xdpi="89.996864" inkscape:export-ydpi="89.996864" />
|
|
||||||
<path style="fill:#b3b3b3;fill-opacity:1;stroke:none" d="m 63.71429,890.36216 -1.14285,1.1428 2.28571,2.2858 1.14285,-1.1429 z m -1.71429,1.7143 -6.285714,6.28572 2.285714,2.2857 6.28572,-6.2857 z m -6.285714,6.28572 -1.714286,4 4,-1.7143 z" id="path3684-2" inkscape:connector-curvature="0" sodipodi:nodetypes="cccccccccccccc" />
|
<path style="fill:#b3b3b3;fill-opacity:1;stroke:none" d="m 63.71429,890.36216 -1.14285,1.1428 2.28571,2.2858 1.14285,-1.1429 z m -1.71429,1.7143 -6.285714,6.28572 2.285714,2.2857 6.28572,-6.2857 z m -6.285714,6.28572 -1.714286,4 4,-1.7143 z" id="path3684-2" inkscape:connector-curvature="0" sodipodi:nodetypes="cccccccccccccc" />
|
||||||
<path id="table-5-8-8-6" style="fill:#464646;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 30,914.86218 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z" inkscape:connector-curvature="0" />
|
<path id="table-5-8-8-6" style="fill:#464646;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 30,914.86218 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z" inkscape:connector-curvature="0" />
|
||||||
<path inkscape:connector-curvature="0" id="table-5-8-8-6-1" style="fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 54,914.86218 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z" />
|
<path inkscape:connector-curvature="0" id="table-5-8-8-6-1" style="fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 54,914.86218 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z m -2,2 v 1 h 1 v -1 z m 2,0 v 1 h 10 v -1 z" />
|
||||||
|
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 33 KiB |
|
@ -273,7 +273,12 @@ L.U.ContinueLineAction = L.U.BaseVertexAction.extend({
|
||||||
})
|
})
|
||||||
|
|
||||||
// Leaflet.Toolbar doesn't allow twice same toolbar class…
|
// 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({
|
L.U.DrawToolbar = L.Toolbar.Control.extend({
|
||||||
initialize: function (options) {
|
initialize: function (options) {
|
||||||
L.Toolbar.Control.prototype.initialize.call(this, options)
|
L.Toolbar.Control.prototype.initialize.call(this, options)
|
||||||
|
@ -608,8 +613,10 @@ L.U.DataLayer.include({
|
||||||
edit.title = L._('Edit')
|
edit.title = L._('Edit')
|
||||||
table.title = L._('Edit properties in a table')
|
table.title = L._('Edit properties in a table')
|
||||||
remove.title = L._('Delete layer')
|
remove.title = L._('Delete layer')
|
||||||
L.DomEvent.on(toggle, 'click', this.toggle, this)
|
if (this.isReadOnly()) {
|
||||||
L.DomEvent.on(zoomTo, 'click', this.zoomTo, this)
|
L.DomUtil.addClass(container, 'readonly')
|
||||||
|
}
|
||||||
|
else {
|
||||||
L.DomEvent.on(edit, 'click', this.edit, this)
|
L.DomEvent.on(edit, 'click', this.edit, this)
|
||||||
L.DomEvent.on(table, 'click', this.tableEdit, this)
|
L.DomEvent.on(table, 'click', this.tableEdit, this)
|
||||||
L.DomEvent.on(
|
L.DomEvent.on(
|
||||||
|
@ -623,6 +630,9 @@ L.U.DataLayer.include({
|
||||||
},
|
},
|
||||||
this
|
this
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
L.DomEvent.on(toggle, 'click', this.toggle, this)
|
||||||
|
L.DomEvent.on(zoomTo, 'click', this.zoomTo, this)
|
||||||
L.DomUtil.addClass(container, this.getHidableClass())
|
L.DomUtil.addClass(container, this.getHidableClass())
|
||||||
L.DomUtil.classIf(container, 'off', !this.isVisible())
|
L.DomUtil.classIf(container, 'off', !this.isVisible())
|
||||||
container.dataset.id = L.stamp(this)
|
container.dataset.id = L.stamp(this)
|
||||||
|
@ -993,11 +1003,13 @@ L.U.Map.include({
|
||||||
}
|
}
|
||||||
update()
|
update()
|
||||||
this.once('saved', L.bind(update, this))
|
this.once('saved', L.bind(update, this))
|
||||||
|
logo.href = '/'
|
||||||
|
if (this.options.editMode === 'advanced') {
|
||||||
name.href = '#'
|
name.href = '#'
|
||||||
share_status.href = '#'
|
share_status.href = '#'
|
||||||
logo.href = '/'
|
|
||||||
L.DomEvent.on(name, 'click', this.edit, this)
|
L.DomEvent.on(name, 'click', this.edit, this)
|
||||||
L.DomEvent.on(share_status, 'click', this.permissions.edit, this.permissions)
|
L.DomEvent.on(share_status, 'click', this.permissions.edit, this.permissions)
|
||||||
|
}
|
||||||
this.on('postsync', L.bind(update, this))
|
this.on('postsync', L.bind(update, this))
|
||||||
const save = L.DomUtil.create('a', 'leaflet-control-edit-save button', container)
|
const save = L.DomUtil.create('a', 'leaflet-control-edit-save button', container)
|
||||||
save.href = '#'
|
save.href = '#'
|
||||||
|
@ -1457,7 +1469,7 @@ L.U.IframeExporter = L.Evented.extend({
|
||||||
miniMap: false,
|
miniMap: false,
|
||||||
scrollWheelZoom: false,
|
scrollWheelZoom: false,
|
||||||
zoomControl: true,
|
zoomControl: true,
|
||||||
allowEdit: false,
|
editMode: 'disabled',
|
||||||
moreControl: true,
|
moreControl: true,
|
||||||
searchControl: null,
|
searchControl: null,
|
||||||
tilelayersControl: null,
|
tilelayersControl: null,
|
||||||
|
|
|
@ -257,6 +257,34 @@ L.Util.hasVar = (value) => {
|
||||||
return typeof value === 'string' && value.indexOf('{') != -1
|
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) => {
|
L.DomUtil.add = (tagName, className, container, content) => {
|
||||||
const el = L.DomUtil.create(tagName, className, container)
|
const el = L.DomUtil.create(tagName, className, container)
|
||||||
if (content) {
|
if (content) {
|
||||||
|
|
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 "{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)
|
||||||
|
},
|
||||||
|
})
|
|
@ -40,7 +40,7 @@ L.U.FeatureMixin = {
|
||||||
preInit: function () {},
|
preInit: function () {},
|
||||||
|
|
||||||
isReadOnly: function () {
|
isReadOnly: function () {
|
||||||
return this.datalayer && this.datalayer.isRemoteLayer()
|
return this.datalayer && this.datalayer.isDataReadOnly()
|
||||||
},
|
},
|
||||||
|
|
||||||
getSlug: function () {
|
getSlug: function () {
|
||||||
|
|
|
@ -396,7 +396,7 @@ L.FormBuilder.DataLayerSwitcher = L.FormBuilder.Select.extend({
|
||||||
getOptions: function () {
|
getOptions: function () {
|
||||||
const options = []
|
const options = []
|
||||||
this.builder.map.eachDataLayerReverse((datalayer) => {
|
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()])
|
options.push([L.stamp(datalayer), datalayer.getName()])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,7 +15,7 @@ L.Map.mergeOptions({
|
||||||
default_interactive: true,
|
default_interactive: true,
|
||||||
default_labelDirection: 'auto',
|
default_labelDirection: 'auto',
|
||||||
attributionControl: false,
|
attributionControl: false,
|
||||||
allowEdit: true,
|
editMode: 'advanced',
|
||||||
embedControl: true,
|
embedControl: true,
|
||||||
zoomControl: true,
|
zoomControl: true,
|
||||||
datalayersControl: true,
|
datalayersControl: true,
|
||||||
|
@ -103,7 +103,7 @@ L.U.Map.include({
|
||||||
L.Util.setBooleanFromQueryString(this.options, 'moreControl')
|
L.Util.setBooleanFromQueryString(this.options, 'moreControl')
|
||||||
L.Util.setBooleanFromQueryString(this.options, 'scaleControl')
|
L.Util.setBooleanFromQueryString(this.options, 'scaleControl')
|
||||||
L.Util.setBooleanFromQueryString(this.options, 'miniMap')
|
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, 'displayDataBrowserOnLoad')
|
||||||
L.Util.setBooleanFromQueryString(this.options, 'displayCaptionOnLoad')
|
L.Util.setBooleanFromQueryString(this.options, 'displayCaptionOnLoad')
|
||||||
L.Util.setBooleanFromQueryString(this.options, 'captionBar')
|
L.Util.setBooleanFromQueryString(this.options, 'captionBar')
|
||||||
|
@ -122,7 +122,7 @@ L.U.Map.include({
|
||||||
if (this.datalayersOnLoad)
|
if (this.datalayersOnLoad)
|
||||||
this.datalayersOnLoad = this.datalayersOnLoad.toString().split(',')
|
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
|
let editedFeature = null
|
||||||
const self = this
|
const self = this
|
||||||
|
@ -192,16 +192,15 @@ L.U.Map.include({
|
||||||
this
|
this
|
||||||
)
|
)
|
||||||
|
|
||||||
let isDirty = false // global status
|
let isDirty = false // self status
|
||||||
try {
|
try {
|
||||||
Object.defineProperty(this, 'isDirty', {
|
Object.defineProperty(this, 'isDirty', {
|
||||||
get: function () {
|
get: function () {
|
||||||
return isDirty || this.dirty_datalayers.length
|
return isDirty
|
||||||
},
|
},
|
||||||
set: function (status) {
|
set: function (status) {
|
||||||
if (!isDirty && status) self.fire('isdirty')
|
|
||||||
isDirty = status
|
isDirty = status
|
||||||
self.checkDirty()
|
this.checkDirty()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -220,7 +219,7 @@ L.U.Map.include({
|
||||||
this.isDirty = true
|
this.isDirty = true
|
||||||
this._default_extent = true
|
this._default_extent = true
|
||||||
this.options.name = L._('Untitled map')
|
this.options.name = L._('Untitled map')
|
||||||
this.options.allowEdit = true
|
this.options.editMode = 'advanced'
|
||||||
const datalayer = this.createDataLayer()
|
const datalayer = this.createDataLayer()
|
||||||
datalayer.connectToMap()
|
datalayer.connectToMap()
|
||||||
this.enableEdit()
|
this.enableEdit()
|
||||||
|
@ -238,7 +237,7 @@ L.U.Map.include({
|
||||||
this.slideshow = new L.U.Slideshow(this, this.options.slideshow)
|
this.slideshow = new L.U.Slideshow(this, this.options.slideshow)
|
||||||
this.permissions = new L.U.MapPermissions(this)
|
this.permissions = new L.U.MapPermissions(this)
|
||||||
this.initCaptionBar()
|
this.initCaptionBar()
|
||||||
if (this.options.allowEdit) {
|
if (this.hasEditMode()) {
|
||||||
this.editTools = new L.U.Editable(this)
|
this.editTools = new L.U.Editable(this)
|
||||||
this.ui.on(
|
this.ui.on(
|
||||||
'panel:closed panel:open',
|
'panel:closed panel:open',
|
||||||
|
@ -277,7 +276,7 @@ L.U.Map.include({
|
||||||
this.helpMenuActions = {}
|
this.helpMenuActions = {}
|
||||||
this._controls = {}
|
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.EditControl(this).addTo(this)
|
||||||
|
|
||||||
new L.U.DrawToolbar({ map: this }).addTo(this)
|
new L.U.DrawToolbar({ map: this }).addTo(this)
|
||||||
|
@ -496,7 +495,7 @@ L.U.Map.include({
|
||||||
else this.ui.closePanel()
|
else this.ui.closePanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.options.allowEdit) return
|
if (!this.hasEditMode()) return
|
||||||
|
|
||||||
/* Edit mode only shortcuts */
|
/* Edit mode only shortcuts */
|
||||||
if (key === L.U.Keys.E && modifierKey && !this.editEnabled) {
|
if (key === L.U.Keys.E && modifierKey && !this.editEnabled) {
|
||||||
|
@ -1161,47 +1160,16 @@ L.U.Map.include({
|
||||||
return JSON.stringify(umapfile, null, 2)
|
return JSON.stringify(umapfile, null, 2)
|
||||||
},
|
},
|
||||||
|
|
||||||
save: function () {
|
saveSelf: function () {
|
||||||
if (!this.isDirty) return
|
|
||||||
if (this._default_extent) this.updateExtent()
|
|
||||||
const geojson = {
|
const geojson = {
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
geometry: this.geometry(),
|
geometry: this.geometry(),
|
||||||
properties: this.exportOptions(),
|
properties: this.exportOptions(),
|
||||||
}
|
}
|
||||||
this.backup()
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('name', this.options.name)
|
formData.append('name', this.options.name)
|
||||||
formData.append('center', JSON.stringify(this.geometry()))
|
formData.append('center', JSON.stringify(this.geometry()))
|
||||||
formData.append('settings', JSON.stringify(geojson))
|
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(), {
|
this.post(this.getSaveUrl(), {
|
||||||
data: formData,
|
data: formData,
|
||||||
context: this,
|
context: this,
|
||||||
|
@ -1212,6 +1180,7 @@ L.U.Map.include({
|
||||||
alert.content = L._('Congratulations, your map has been created!')
|
alert.content = L._('Congratulations, your map has been created!')
|
||||||
this.options.umap_id = data.id
|
this.options.umap_id = data.id
|
||||||
this.permissions.setOptions(data.permissions)
|
this.permissions.setOptions(data.permissions)
|
||||||
|
this.permissions.commit()
|
||||||
if (
|
if (
|
||||||
data.permissions &&
|
data.permissions &&
|
||||||
data.permissions.anonymous_edit_url &&
|
data.permissions.anonymous_edit_url &&
|
||||||
|
@ -1233,7 +1202,7 @@ L.U.Map.include({
|
||||||
{
|
{
|
||||||
label: L._('Copy link'),
|
label: L._('Copy link'),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
copyToClipboard(data.permissions.anonymous_edit_url)
|
L.Util.copyToClipboard(data.permissions.anonymous_edit_url)
|
||||||
this.ui.alert({
|
this.ui.alert({
|
||||||
content: L._('Secret edit link copied to clipboard!'),
|
content: L._('Secret edit link copied to clipboard!'),
|
||||||
level: 'info',
|
level: 'info',
|
||||||
|
@ -1247,22 +1216,35 @@ L.U.Map.include({
|
||||||
// Do not override local changes to permissions,
|
// Do not override local changes to permissions,
|
||||||
// but update in case some other editors changed them in the meantime.
|
// but update in case some other editors changed them in the meantime.
|
||||||
this.permissions.setOptions(data.permissions)
|
this.permissions.setOptions(data.permissions)
|
||||||
|
this.permissions.commit()
|
||||||
}
|
}
|
||||||
// Update URL in case the name has changed.
|
// Update URL in case the name has changed.
|
||||||
if (history && history.pushState)
|
if (history && history.pushState)
|
||||||
history.pushState({}, this.options.name, data.url)
|
history.pushState({}, this.options.name, data.url)
|
||||||
else window.location = data.url
|
else window.location = data.url
|
||||||
alert.content = data.info || alert.content
|
alert.content = data.info || alert.content
|
||||||
this.once('saved', function () {
|
this.once('saved', () => this.ui.alert(alert))
|
||||||
this.isDirty = false
|
|
||||||
this.ui.alert(alert)
|
|
||||||
})
|
|
||||||
this.ui.closePanel()
|
this.ui.closePanel()
|
||||||
this.permissions.save()
|
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 () {
|
sendEditLink: function () {
|
||||||
const url = L.Util.template(this.options.urls.map_send_edit_link, {
|
const url = L.Util.template(this.options.urls.map_send_edit_link, {
|
||||||
map_id: this.options.umap_id,
|
map_id: this.options.umap_id,
|
||||||
|
@ -1330,14 +1312,14 @@ L.U.Map.include({
|
||||||
datalayer = this.lastUsedDataLayer
|
datalayer = this.lastUsedDataLayer
|
||||||
if (
|
if (
|
||||||
datalayer &&
|
datalayer &&
|
||||||
!datalayer.isRemoteLayer() &&
|
!datalayer.isDataReadOnly() &&
|
||||||
datalayer.canBrowse() &&
|
datalayer.canBrowse() &&
|
||||||
datalayer.isVisible()
|
datalayer.isVisible()
|
||||||
) {
|
) {
|
||||||
return datalayer
|
return datalayer
|
||||||
}
|
}
|
||||||
datalayer = this.findDataLayer((datalayer) => {
|
datalayer = this.findDataLayer((datalayer) => {
|
||||||
if (!datalayer.isRemoteLayer() && datalayer.canBrowse()) {
|
if (!datalayer.isDataReadOnly() && datalayer.canBrowse()) {
|
||||||
fallback = datalayer
|
fallback = datalayer
|
||||||
if (datalayer.isVisible()) return true
|
if (datalayer.isVisible()) return true
|
||||||
}
|
}
|
||||||
|
@ -1733,20 +1715,28 @@ L.U.Map.include({
|
||||||
_advancedActions: function (container) {
|
_advancedActions: function (container) {
|
||||||
const advancedActions = L.DomUtil.createFieldset(container, L._('Advanced actions'))
|
const advancedActions = L.DomUtil.createFieldset(container, L._('Advanced actions'))
|
||||||
const advancedButtons = L.DomUtil.create('div', 'button-bar half', advancedActions)
|
const advancedButtons = L.DomUtil.create('div', 'button-bar half', advancedActions)
|
||||||
|
if (this.permissions.isOwner()) {
|
||||||
const del = L.DomUtil.create('a', 'button umap-delete', advancedButtons)
|
const del = L.DomUtil.create('a', 'button umap-delete', advancedButtons)
|
||||||
del.href = '#'
|
del.href = '#'
|
||||||
|
del.title = L._('Delete map')
|
||||||
del.textContent = L._('Delete')
|
del.textContent = L._('Delete')
|
||||||
L.DomEvent.on(del, 'click', L.DomEvent.stop).on(del, 'click', this.del, this)
|
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)
|
const clone = L.DomUtil.create('a', 'button umap-clone', advancedButtons)
|
||||||
clone.href = '#'
|
clone.href = '#'
|
||||||
clone.textContent = L._('Clone')
|
clone.textContent = L._('Clone')
|
||||||
clone.title = L._('Clone this map')
|
clone.title = L._('Clone this map')
|
||||||
L.DomEvent.on(clone, 'click', L.DomEvent.stop).on(clone, 'click', this.clone, this)
|
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)
|
const download = L.DomUtil.create('a', 'button umap-download', advancedButtons)
|
||||||
download.href = '#'
|
download.href = '#'
|
||||||
download.textContent = L._('Download')
|
download.textContent = L._('Download')
|
||||||
|
@ -1761,6 +1751,7 @@ L.U.Map.include({
|
||||||
|
|
||||||
edit: function () {
|
edit: function () {
|
||||||
if (!this.editEnabled) return
|
if (!this.editEnabled) return
|
||||||
|
if (this.options.editMode !== 'advanced') return
|
||||||
const container = L.DomUtil.create('div', 'umap-edit-container'),
|
const container = L.DomUtil.create('div', 'umap-edit-container'),
|
||||||
metadataFields = ['options.name', 'options.description'],
|
metadataFields = ['options.name', 'options.description'],
|
||||||
title = L.DomUtil.create('h3', '', container)
|
title = L.DomUtil.create('h3', '', container)
|
||||||
|
@ -1796,6 +1787,10 @@ L.U.Map.include({
|
||||||
this.fire('edit:disabled')
|
this.fire('edit:disabled')
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hasEditMode: function () {
|
||||||
|
return this.options.editMode === 'simple' || this.options.editMode === 'advanced'
|
||||||
|
},
|
||||||
|
|
||||||
getDisplayName: function () {
|
getDisplayName: function () {
|
||||||
return this.options.name || L._('Untitled map')
|
return this.options.name || L._('Untitled map')
|
||||||
},
|
},
|
||||||
|
@ -1952,7 +1947,7 @@ L.U.Map.include({
|
||||||
items = items.concat(e.relatedTarget.getContextMenuItems(e))
|
items = items.concat(e.relatedTarget.getContextMenuItems(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.options.allowEdit) {
|
if (this.hasEditMode()) {
|
||||||
items.push('-')
|
items.push('-')
|
||||||
if (this.editEnabled) {
|
if (this.editEnabled) {
|
||||||
if (!this.isDirty) {
|
if (!this.isDirty) {
|
||||||
|
|
|
@ -193,6 +193,7 @@ L.U.DataLayer = L.Evented.extend({
|
||||||
options: {
|
options: {
|
||||||
displayOnLoad: true,
|
displayOnLoad: true,
|
||||||
browsable: true,
|
browsable: true,
|
||||||
|
editMode: 'advanced',
|
||||||
},
|
},
|
||||||
|
|
||||||
initialize: function (map, data) {
|
initialize: function (map, data) {
|
||||||
|
@ -261,6 +262,7 @@ L.U.DataLayer = L.Evented.extend({
|
||||||
}
|
}
|
||||||
this.backupOptions()
|
this.backupOptions()
|
||||||
this.connectToMap()
|
this.connectToMap()
|
||||||
|
this.permissions = new L.U.DataLayerPermissions(this)
|
||||||
if (this.showAtLoad()) this.show()
|
if (this.showAtLoad()) this.show()
|
||||||
if (!this.umap_id) this.isDirty = true
|
if (!this.umap_id) this.isDirty = true
|
||||||
|
|
||||||
|
@ -350,6 +352,12 @@ L.U.DataLayer = L.Evented.extend({
|
||||||
this.map.get(this._dataUrl(), {
|
this.map.get(this._dataUrl(), {
|
||||||
callback: function (geojson, response) {
|
callback: function (geojson, response) {
|
||||||
this._last_modified = response.getResponseHeader('Last-Modified')
|
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.fromUmapGeoJSON(geojson)
|
||||||
this.backupOptions()
|
this.backupOptions()
|
||||||
this.fire('loaded')
|
this.fire('loaded')
|
||||||
|
@ -489,7 +497,7 @@ L.U.DataLayer = L.Evented.extend({
|
||||||
})
|
})
|
||||||
|
|
||||||
// No browser cache for owners/editors.
|
// 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
|
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 () {
|
getRank: function () {
|
||||||
return this.map.datalayers_index.indexOf(this)
|
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 () {
|
save: function () {
|
||||||
if (this.isDeleted) return this.saveDelete()
|
if (this.isDeleted) return this.saveDelete()
|
||||||
if (!this.isLoaded()) {
|
if (!this.isLoaded()) {
|
||||||
|
@ -1220,7 +1230,7 @@ L.U.DataLayer = L.Evented.extend({
|
||||||
this._loaded = true
|
this._loaded = true
|
||||||
this.redraw() // Needed for reordering features
|
this.redraw() // Needed for reordering features
|
||||||
this.isDirty = false
|
this.isDirty = false
|
||||||
this.map.continueSaving()
|
this.permissions.save()
|
||||||
},
|
},
|
||||||
context: this,
|
context: this,
|
||||||
headers: this._last_modified
|
headers: this._last_modified
|
||||||
|
|
|
@ -20,7 +20,9 @@ L.U.MapPermissions = L.Class.extend({
|
||||||
},
|
},
|
||||||
set: function (status) {
|
set: function (status) {
|
||||||
isDirty = status
|
isDirty = status
|
||||||
if (status) self.map.isDirty = status
|
if (status) {
|
||||||
|
self.map.isDirty = status
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -35,13 +37,13 @@ L.U.MapPermissions = L.Class.extend({
|
||||||
isOwner: function () {
|
isOwner: function () {
|
||||||
return (
|
return (
|
||||||
this.map.options.user &&
|
this.map.options.user &&
|
||||||
this.map.permissions.options.owner &&
|
this.map.options.permissions.owner &&
|
||||||
this.map.options.user.id == this.map.permissions.options.owner.id
|
this.map.options.user.id == this.map.options.permissions.owner.id
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
isAnonymousMap: function () {
|
isAnonymousMap: function () {
|
||||||
return !this.map.permissions.options.owner
|
return !this.map.options.permissions.owner
|
||||||
},
|
},
|
||||||
|
|
||||||
getMap: function () {
|
getMap: function () {
|
||||||
|
@ -49,6 +51,7 @@ L.U.MapPermissions = L.Class.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
edit: function () {
|
edit: function () {
|
||||||
|
if (this.map.options.editMode !== 'advanced') return
|
||||||
if (!this.map.options.umap_id)
|
if (!this.map.options.umap_id)
|
||||||
return this.map.ui.alert({
|
return this.map.ui.alert({
|
||||||
content: L._('Please save the map first'),
|
content: L._('Please save the map first'),
|
||||||
|
@ -59,15 +62,16 @@ L.U.MapPermissions = L.Class.extend({
|
||||||
title = L.DomUtil.create('h4', '', container)
|
title = L.DomUtil.create('h4', '', container)
|
||||||
if (this.isAnonymousMap()) {
|
if (this.isAnonymousMap()) {
|
||||||
if (this.options.anonymous_edit_url) {
|
if (this.options.anonymous_edit_url) {
|
||||||
const helpText = L._('Secret edit link is:<br>{link}', {
|
const helpText = `${L._('Secret edit link:')}<br>${
|
||||||
link: this.options.anonymous_edit_url,
|
this.options.anonymous_edit_url
|
||||||
})
|
}`
|
||||||
|
L.DomUtil.add('p', 'help-text', container, helpText)
|
||||||
fields.push([
|
fields.push([
|
||||||
'options.edit_status',
|
'options.edit_status',
|
||||||
{
|
{
|
||||||
handler: 'IntSelect',
|
handler: 'IntSelect',
|
||||||
label: L._('Who can edit'),
|
label: L._('Who can edit'),
|
||||||
selectOptions: this.map.options.anonymous_edit_statuses,
|
selectOptions: this.map.options.edit_statuses,
|
||||||
helpText: helpText,
|
helpText: helpText,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
@ -122,6 +126,10 @@ L.U.MapPermissions = L.Class.extend({
|
||||||
this
|
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' })
|
this.map.ui.openPanel({ data: { html: container }, className: 'dark' })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -197,6 +205,8 @@ L.U.MapPermissions = L.Class.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
getShareStatusDisplay: function () {
|
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
|
||||||
|
]
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -756,15 +756,18 @@ a.map-name:after {
|
||||||
.umap-toggle-edit {
|
.umap-toggle-edit {
|
||||||
background-position: -44px -48px;
|
background-position: -44px -48px;
|
||||||
}
|
}
|
||||||
|
.readonly .layer-table-edit,
|
||||||
.off .layer-table-edit {
|
.off .layer-table-edit {
|
||||||
background-position: -74px -1px;
|
background-position: -74px -1px;
|
||||||
}
|
}
|
||||||
|
.readonly .layer-edit,
|
||||||
.off .layer-edit {
|
.off .layer-edit {
|
||||||
background-position: -51px -72px;
|
background-position: -51px -72px;
|
||||||
}
|
}
|
||||||
.off .layer-zoom_to {
|
.off .layer-zoom_to {
|
||||||
background-position: -25px -54px;
|
background-position: -25px -54px;
|
||||||
}
|
}
|
||||||
|
.readonly .layer-delete,
|
||||||
.off .layer-delete {
|
.off .layer-delete {
|
||||||
background-position: -122px -121px;
|
background-position: -122px -121px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -207,6 +207,7 @@ describe('L.U.Map.Export', function () {
|
||||||
_umap_options: {
|
_umap_options: {
|
||||||
displayOnLoad: true,
|
displayOnLoad: true,
|
||||||
browsable: true,
|
browsable: true,
|
||||||
|
editMode: 'advanced',
|
||||||
iconClass: 'Default',
|
iconClass: 'Default',
|
||||||
name: 'Elephants',
|
name: 'Elephants',
|
||||||
id: 62,
|
id: 62,
|
||||||
|
|
|
@ -105,7 +105,7 @@ describe('L.U.Map', function () {
|
||||||
window.confirm = oldConfirm
|
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')
|
var button = qs('a.update-map-settings')
|
||||||
assert.ok(button, 'update map info button exists')
|
assert.ok(button, 'update map info button exists')
|
||||||
happen.click(button)
|
happen.click(button)
|
||||||
|
@ -117,7 +117,7 @@ describe('L.U.Map', function () {
|
||||||
this.server.respond()
|
this.server.respond()
|
||||||
assert(window.confirm.calledOnce)
|
assert(window.confirm.calledOnce)
|
||||||
window.confirm.restore()
|
window.confirm.restore()
|
||||||
done()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -34,11 +34,11 @@ describe('L.Permissions', function () {
|
||||||
describe('#anonymous with cookie', function () {
|
describe('#anonymous with cookie', function () {
|
||||||
var button
|
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'
|
this.map.permissions.options.anonymous_edit_url = 'http://anonymous.url'
|
||||||
|
delete this.map.permissions.options.owner
|
||||||
button = qs('a.update-map-permissions')
|
button = qs('a.update-map-permissions')
|
||||||
happen.click(button)
|
happen.click(button)
|
||||||
expect(qs('select[name="edit_status"]')).to.be.ok
|
|
||||||
expect(qs('select[name="share_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-owner')).not.to.be.ok
|
||||||
})
|
})
|
||||||
|
@ -49,9 +49,10 @@ describe('L.Permissions', function () {
|
||||||
|
|
||||||
it('should only allow editors', function () {
|
it('should only allow editors', function () {
|
||||||
this.map.permissions.options.owner = { id: 1, url: '/url', name: 'jojo' }
|
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')
|
button = qs('a.update-map-permissions')
|
||||||
happen.click(button)
|
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('select[name="share_status"]')).not.to.be.ok
|
||||||
expect(qs('input.edit-owner')).not.to.be.ok
|
expect(qs('input.edit-owner')).not.to.be.ok
|
||||||
expect(qs('input.edit-editors')).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' }
|
this.map.options.user = { id: 1, url: '/url', name: 'jojo' }
|
||||||
button = qs('a.update-map-permissions')
|
button = qs('a.update-map-permissions')
|
||||||
happen.click(button)
|
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-owner')).to.be.ok
|
||||||
expect(qs('input.edit-editors')).to.be.ok
|
expect(qs('input.edit-editors')).to.be.ok
|
||||||
})
|
})
|
||||||
|
|
|
@ -190,7 +190,7 @@ function initMap(options) {
|
||||||
name: 'name of the map',
|
name: 'name of the map',
|
||||||
description: 'The description of the map',
|
description: 'The description of the map',
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
allowEdit: true,
|
editMode: 'advanced',
|
||||||
moreControl: true,
|
moreControl: true,
|
||||||
scaleControl: true,
|
scaleControl: true,
|
||||||
miniMap: false,
|
miniMap: false,
|
||||||
|
@ -198,6 +198,20 @@ function initMap(options) {
|
||||||
displayCaptionOnLoad: false,
|
displayCaptionOnLoad: false,
|
||||||
displayPopupFooter: false,
|
displayPopupFooter: false,
|
||||||
displayDataBrowserOnLoad: 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())
|
default_options.properties.datalayers.push(defaultDatalayerData())
|
||||||
|
@ -319,7 +333,11 @@ var RESPONSES = {
|
||||||
datalayer64_GET: {
|
datalayer64_GET: {
|
||||||
crs: null,
|
crs: null,
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
_umap_options: defaultDatalayerData({name: 'hidden', id: 64, displayOnLoad: false }),
|
_umap_options: defaultDatalayerData({
|
||||||
|
name: 'hidden',
|
||||||
|
id: 64,
|
||||||
|
displayOnLoad: false,
|
||||||
|
}),
|
||||||
features: [
|
features: [
|
||||||
{
|
{
|
||||||
geometry: {
|
geometry: {
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
<script src="../js/umap.slideshow.js"></script>
|
<script src="../js/umap.slideshow.js"></script>
|
||||||
<script src="../js/umap.tableeditor.js"></script>
|
<script src="../js/umap.tableeditor.js"></script>
|
||||||
<script src="../js/umap.permissions.js"></script>
|
<script src="../js/umap.permissions.js"></script>
|
||||||
|
<script src="../js/umap.datalayer.permissions.js"></script>
|
||||||
<script src="../js/umap.js"></script>
|
<script src="../js/umap.js"></script>
|
||||||
<script src="../js/umap.ui.js"></script>
|
<script src="../js/umap.ui.js"></script>
|
||||||
<link rel="stylesheet" href="../vendors/leaflet/leaflet.css" />
|
<link rel="stylesheet" href="../vendors/leaflet/leaflet.css" />
|
||||||
|
|
|
@ -34,11 +34,12 @@
|
||||||
<script src="{{ STATIC_URL }}umap/js/umap.forms.js"></script>
|
<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.icon.js"></script>
|
||||||
<script src="{{ STATIC_URL }}umap/js/umap.features.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.layer.js"></script>
|
||||||
<script src="{{ STATIC_URL }}umap/js/umap.controls.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.slideshow.js"></script>
|
||||||
<script src="{{ STATIC_URL }}umap/js/umap.tableeditor.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.js"></script>
|
||||||
<script src="{{ STATIC_URL }}umap/js/umap.ui.js"></script>
|
<script src="{{ STATIC_URL }}umap/js/umap.ui.js"></script>
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
|
|
|
@ -28,7 +28,7 @@ def umap_js(locale=None):
|
||||||
@register.inclusion_tag('umap/map_fragment.html')
|
@register.inclusion_tag('umap/map_fragment.html')
|
||||||
def map_fragment(map_instance, **kwargs):
|
def map_fragment(map_instance, **kwargs):
|
||||||
layers = DataLayer.objects.filter(map=map_instance)
|
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
|
map_settings = map_instance.settings
|
||||||
if "properties" not in map_settings:
|
if "properties" not in map_settings:
|
||||||
map_settings['properties'] = {}
|
map_settings['properties'] = {}
|
||||||
|
@ -37,7 +37,7 @@ def map_fragment(map_instance, **kwargs):
|
||||||
'datalayers': datalayer_data,
|
'datalayers': datalayer_data,
|
||||||
'urls': _urls_for_js(),
|
'urls': _urls_for_js(),
|
||||||
'STATIC_URL': settings.STATIC_URL,
|
'STATIC_URL': settings.STATIC_URL,
|
||||||
"allowEdit": False,
|
"editMode": 'disabled',
|
||||||
'hash': False,
|
'hash': False,
|
||||||
'attributionControl': False,
|
'attributionControl': False,
|
||||||
'scrollWheelZoom': False,
|
'scrollWheelZoom': False,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import json
|
||||||
|
|
||||||
import factory
|
import factory
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from umap.forms import DEFAULT_CENTER
|
from umap.forms import DEFAULT_CENTER
|
||||||
|
@ -9,6 +10,25 @@ from umap.models import DataLayer, Licence, Map, TileLayer
|
||||||
|
|
||||||
User = get_user_model()
|
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):
|
class LicenceFactory(factory.django.DjangoModelFactory):
|
||||||
name = "WTFPL"
|
name = "WTFPL"
|
||||||
|
@ -82,10 +102,18 @@ class DataLayerFactory(factory.django.DjangoModelFactory):
|
||||||
name = "test datalayer"
|
name = "test datalayer"
|
||||||
description = "test description"
|
description = "test description"
|
||||||
display_on_load = True
|
display_on_load = True
|
||||||
settings = {"displayOnLoad": True, "browsable": True, name: "test datalayer"}
|
settings = {"displayOnLoad": True, "browsable": True, "name": name}
|
||||||
geojson = factory.django.FileField(
|
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
|
@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:
|
class Meta:
|
||||||
model = DataLayer
|
model = DataLayer
|
||||||
|
|
|
@ -74,7 +74,7 @@ def allow_anonymous(settings):
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def datalayer(map):
|
def datalayer(map):
|
||||||
return DataLayerFactory(map=map, name="Default Datalayer")
|
return DataLayerFactory(map=map)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
149
umap/tests/integration/test_anonymous_owned_map.py
Normal file
149
umap/tests/integration/test_anonymous_owned_map.py
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
import re
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.core.signing import get_cookie_signer
|
||||||
|
from playwright.sync_api import expect
|
||||||
|
|
||||||
|
from umap.models import DataLayer
|
||||||
|
|
||||||
|
from ..base import DataLayerFactory
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.django_db, pytest.mark.usefixtures("allow_anonymous")]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def owner_session(anonymap, context, live_server):
|
||||||
|
key, value = anonymap.signed_cookie_elements
|
||||||
|
signed = get_cookie_signer(salt=key).sign(value)
|
||||||
|
context.add_cookies([{"name": key, "value": signed, "url": live_server.url}])
|
||||||
|
return context.new_page()
|
||||||
|
|
||||||
|
|
||||||
|
def test_map_load_with_owner(anonymap, live_server, owner_session):
|
||||||
|
owner_session.goto(f"{live_server.url}{anonymap.get_absolute_url()}")
|
||||||
|
map_el = owner_session.locator("#map")
|
||||||
|
expect(map_el).to_be_visible()
|
||||||
|
enable = owner_session.get_by_role("link", name="Edit")
|
||||||
|
expect(enable).to_be_visible()
|
||||||
|
enable.click()
|
||||||
|
disable = owner_session.get_by_role("link", name="Disable editing")
|
||||||
|
expect(disable).to_be_visible()
|
||||||
|
save = owner_session.get_by_title("Save current edits (Ctrl+S)")
|
||||||
|
expect(save).to_be_visible()
|
||||||
|
add_marker = owner_session.get_by_title("Draw a marker")
|
||||||
|
expect(add_marker).to_be_visible()
|
||||||
|
edit_settings = owner_session.get_by_title("Edit map settings")
|
||||||
|
expect(edit_settings).to_be_visible()
|
||||||
|
edit_permissions = owner_session.get_by_title("Update permissions and editors")
|
||||||
|
expect(edit_permissions).to_be_visible()
|
||||||
|
|
||||||
|
|
||||||
|
def test_map_load_with_anonymous(anonymap, live_server, page):
|
||||||
|
page.goto(f"{live_server.url}{anonymap.get_absolute_url()}")
|
||||||
|
map_el = page.locator("#map")
|
||||||
|
expect(map_el).to_be_visible()
|
||||||
|
enable = page.get_by_role("link", name="Edit")
|
||||||
|
expect(enable).to_be_hidden()
|
||||||
|
|
||||||
|
|
||||||
|
def test_map_load_with_anonymous_but_editable_layer(
|
||||||
|
anonymap, live_server, page, datalayer
|
||||||
|
):
|
||||||
|
datalayer.edit_status = DataLayer.ANONYMOUS
|
||||||
|
datalayer.save()
|
||||||
|
page.goto(f"{live_server.url}{anonymap.get_absolute_url()}")
|
||||||
|
map_el = page.locator("#map")
|
||||||
|
expect(map_el).to_be_visible()
|
||||||
|
enable = page.get_by_role("link", name="Edit")
|
||||||
|
expect(enable).to_be_visible()
|
||||||
|
enable.click()
|
||||||
|
disable = page.get_by_role("link", name="Disable editing")
|
||||||
|
expect(disable).to_be_visible()
|
||||||
|
save = page.get_by_title("Save current edits (Ctrl+S)")
|
||||||
|
expect(save).to_be_visible()
|
||||||
|
add_marker = page.get_by_title("Draw a marker")
|
||||||
|
expect(add_marker).to_be_visible()
|
||||||
|
edit_settings = page.get_by_title("Edit map settings")
|
||||||
|
expect(edit_settings).to_be_hidden()
|
||||||
|
edit_permissions = page.get_by_title("Update permissions and editors")
|
||||||
|
expect(edit_permissions).to_be_hidden()
|
||||||
|
|
||||||
|
|
||||||
|
def test_owner_permissions_form(map, datalayer, live_server, owner_session):
|
||||||
|
owner_session.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
|
||||||
|
edit_permissions = owner_session.get_by_title("Update permissions and editors")
|
||||||
|
expect(edit_permissions).to_be_visible()
|
||||||
|
edit_permissions.click()
|
||||||
|
select = owner_session.locator(".umap-field-share_status select")
|
||||||
|
expect(select).to_be_hidden()
|
||||||
|
owner_field = owner_session.locator(".umap-field-owner")
|
||||||
|
expect(owner_field).to_be_hidden()
|
||||||
|
editors_field = owner_session.locator(".umap-field-editors input")
|
||||||
|
expect(editors_field).to_be_hidden()
|
||||||
|
datalayer_label = owner_session.get_by_text('Who can edit "test datalayer"')
|
||||||
|
expect(datalayer_label).to_be_visible()
|
||||||
|
options = owner_session.locator(
|
||||||
|
".datalayer-permissions select[name='edit_status'] option"
|
||||||
|
)
|
||||||
|
expect(options).to_have_count(3)
|
||||||
|
option = owner_session.locator(
|
||||||
|
".datalayer-permissions select[name='edit_status'] option:checked"
|
||||||
|
)
|
||||||
|
expect(option).to_have_text("Inherit")
|
||||||
|
|
||||||
|
|
||||||
|
def test_anonymous_can_add_marker_on_editable_layer(
|
||||||
|
anonymap, datalayer, live_server, page
|
||||||
|
):
|
||||||
|
datalayer.edit_status = DataLayer.OWNER
|
||||||
|
datalayer.name = "Should not be in the select"
|
||||||
|
datalayer.save() # Non editable by anonymous users
|
||||||
|
assert datalayer.map == anonymap
|
||||||
|
other = DataLayerFactory(
|
||||||
|
map=anonymap, edit_status=DataLayer.ANONYMOUS, name="Editable"
|
||||||
|
)
|
||||||
|
assert other.map == anonymap
|
||||||
|
page.goto(f"{live_server.url}{anonymap.get_absolute_url()}?edit")
|
||||||
|
add_marker = page.get_by_title("Draw a marker")
|
||||||
|
expect(add_marker).to_be_visible()
|
||||||
|
marker = page.locator(".leaflet-marker-icon")
|
||||||
|
map_el = page.locator("#map")
|
||||||
|
expect(marker).to_have_count(2)
|
||||||
|
expect(map_el).not_to_have_class(re.compile("umap-ui"))
|
||||||
|
add_marker.click()
|
||||||
|
map_el.click(position={"x": 100, "y": 100})
|
||||||
|
expect(marker).to_have_count(3)
|
||||||
|
# Edit panel is open
|
||||||
|
expect(map_el).to_have_class(re.compile("umap-ui"))
|
||||||
|
datalayer_select = page.locator("select[name='datalayer']")
|
||||||
|
expect(datalayer_select).to_be_visible()
|
||||||
|
options = page.locator("select[name='datalayer'] option")
|
||||||
|
expect(options).to_have_count(1) # Only Editable layer should be listed
|
||||||
|
option = page.locator("select[name='datalayer'] option:checked")
|
||||||
|
expect(option).to_have_text(other.name)
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_change_perms_after_create(tilelayer, live_server, page):
|
||||||
|
page.goto(f"{live_server.url}/en/map/new")
|
||||||
|
save = page.get_by_title("Save current edits")
|
||||||
|
expect(save).to_be_visible()
|
||||||
|
save.click()
|
||||||
|
sleep(1) # Let save ajax go back
|
||||||
|
edit_permissions = page.get_by_title("Update permissions and editors")
|
||||||
|
expect(edit_permissions).to_be_visible()
|
||||||
|
edit_permissions.click()
|
||||||
|
select = page.locator(".umap-field-share_status select")
|
||||||
|
expect(select).to_be_hidden()
|
||||||
|
owner_field = page.locator(".umap-field-owner")
|
||||||
|
expect(owner_field).to_be_hidden()
|
||||||
|
editors_field = page.locator(".umap-field-editors input")
|
||||||
|
expect(editors_field).to_be_hidden()
|
||||||
|
datalayer_label = page.get_by_text('Who can edit "Layer 1"')
|
||||||
|
expect(datalayer_label).to_be_visible()
|
||||||
|
options = page.locator(".datalayer-permissions select[name='edit_status'] option")
|
||||||
|
expect(options).to_have_count(3)
|
||||||
|
option = page.locator(
|
||||||
|
".datalayer-permissions select[name='edit_status'] option:checked"
|
||||||
|
)
|
||||||
|
expect(option).to_have_text("Inherit")
|
37
umap/tests/integration/test_map.py
Normal file
37
umap/tests/integration/test_map.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import pytest
|
||||||
|
from playwright.sync_api import expect
|
||||||
|
|
||||||
|
from umap.models import Map
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_remote_layer_should_not_be_used_as_datalayer_for_created_features(
|
||||||
|
map, live_server, datalayer, page
|
||||||
|
):
|
||||||
|
# Faster than doing a login
|
||||||
|
map.edit_status = Map.ANONYMOUS
|
||||||
|
map.save()
|
||||||
|
datalayer.settings["remoteData"] = {
|
||||||
|
"url": "https://overpass-api.de/api/interpreter?data=[out:xml];node[harbour=yes]({south},{west},{north},{east});out body;",
|
||||||
|
"format": "osm",
|
||||||
|
"from": "10",
|
||||||
|
}
|
||||||
|
datalayer.save()
|
||||||
|
page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
|
||||||
|
toggle = page.get_by_title("See data layers")
|
||||||
|
expect(toggle).to_be_visible()
|
||||||
|
toggle.click()
|
||||||
|
layers = page.locator(".umap-browse-datalayers li")
|
||||||
|
expect(layers).to_have_count(1)
|
||||||
|
map_el = page.locator("#map")
|
||||||
|
add_marker = page.get_by_title("Draw a marker")
|
||||||
|
expect(add_marker).to_be_visible()
|
||||||
|
marker = page.locator(".leaflet-marker-icon")
|
||||||
|
expect(marker).to_have_count(0)
|
||||||
|
add_marker.click()
|
||||||
|
map_el.click(position={"x": 100, "y": 100})
|
||||||
|
expect(marker).to_have_count(1)
|
||||||
|
# A new datalayer has been created to host this created feature
|
||||||
|
# given the remote one cannot accept new features
|
||||||
|
expect(layers).to_have_count(2)
|
214
umap/tests/integration/test_owned_map.py
Normal file
214
umap/tests/integration/test_owned_map.py
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from playwright.sync_api import expect
|
||||||
|
|
||||||
|
from umap.models import DataLayer, Map
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def login(context, settings, live_server):
|
||||||
|
def do_login(user):
|
||||||
|
# TODO use storage state to do login only once per session
|
||||||
|
# https://playwright.dev/python/docs/auth
|
||||||
|
settings.ENABLE_ACCOUNT_LOGIN = True
|
||||||
|
page = context.new_page()
|
||||||
|
page.goto(f"{live_server.url}/en/")
|
||||||
|
page.locator(".login").click()
|
||||||
|
page.get_by_placeholder("Username").fill(user.username)
|
||||||
|
page.get_by_placeholder("Password").fill("123123")
|
||||||
|
page.locator('#login_form input[type="submit"]').click()
|
||||||
|
sleep(1) # Time for ajax login POST to proceed
|
||||||
|
return page
|
||||||
|
|
||||||
|
return do_login
|
||||||
|
|
||||||
|
|
||||||
|
def test_map_update_with_owner(map, live_server, login):
|
||||||
|
page = login(map.owner)
|
||||||
|
page.goto(f"{live_server.url}{map.get_absolute_url()}")
|
||||||
|
map_el = page.locator("#map")
|
||||||
|
expect(map_el).to_be_visible()
|
||||||
|
enable = page.get_by_role("link", name="Edit")
|
||||||
|
expect(enable).to_be_visible()
|
||||||
|
enable.click()
|
||||||
|
disable = page.get_by_role("link", name="Disable editing")
|
||||||
|
expect(disable).to_be_visible()
|
||||||
|
save = page.get_by_title("Save current edits (Ctrl+S)")
|
||||||
|
expect(save).to_be_visible()
|
||||||
|
add_marker = page.get_by_title("Draw a marker")
|
||||||
|
expect(add_marker).to_be_visible()
|
||||||
|
edit_settings = page.get_by_title("Edit map settings")
|
||||||
|
expect(edit_settings).to_be_visible()
|
||||||
|
edit_permissions = page.get_by_title("Update permissions and editors")
|
||||||
|
expect(edit_permissions).to_be_visible()
|
||||||
|
|
||||||
|
|
||||||
|
def test_map_update_with_anonymous(map, live_server, page):
|
||||||
|
page.goto(f"{live_server.url}{map.get_absolute_url()}")
|
||||||
|
map_el = page.locator("#map")
|
||||||
|
expect(map_el).to_be_visible()
|
||||||
|
enable = page.get_by_role("link", name="Edit")
|
||||||
|
expect(enable).to_be_hidden()
|
||||||
|
|
||||||
|
|
||||||
|
def test_map_update_with_anonymous_but_editable_datalayer(
|
||||||
|
map, datalayer, live_server, page
|
||||||
|
):
|
||||||
|
datalayer.edit_status = DataLayer.ANONYMOUS
|
||||||
|
datalayer.save()
|
||||||
|
page.goto(f"{live_server.url}{map.get_absolute_url()}")
|
||||||
|
map_el = page.locator("#map")
|
||||||
|
expect(map_el).to_be_visible()
|
||||||
|
enable = page.get_by_role("link", name="Edit")
|
||||||
|
expect(enable).to_be_visible()
|
||||||
|
enable.click()
|
||||||
|
add_marker = page.get_by_title("Draw a marker")
|
||||||
|
expect(add_marker).to_be_visible()
|
||||||
|
edit_settings = page.get_by_title("Edit map settings")
|
||||||
|
expect(edit_settings).to_be_hidden()
|
||||||
|
edit_permissions = page.get_by_title("Update permissions and editors")
|
||||||
|
expect(edit_permissions).to_be_hidden()
|
||||||
|
|
||||||
|
|
||||||
|
def test_owner_permissions_form(map, datalayer, live_server, login):
|
||||||
|
page = login(map.owner)
|
||||||
|
page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
|
||||||
|
edit_permissions = page.get_by_title("Update permissions and editors")
|
||||||
|
expect(edit_permissions).to_be_visible()
|
||||||
|
edit_permissions.click()
|
||||||
|
select = page.locator(".umap-field-share_status select")
|
||||||
|
expect(select).to_be_visible()
|
||||||
|
# expect(select).to_have_value(Map.PUBLIC) # Does not work
|
||||||
|
owner_field = page.locator(".umap-field-owner")
|
||||||
|
expect(owner_field).to_be_visible()
|
||||||
|
editors_field = page.locator(".umap-field-editors input")
|
||||||
|
expect(editors_field).to_be_visible()
|
||||||
|
datalayer_label = page.get_by_text('Who can edit "test datalayer"')
|
||||||
|
expect(datalayer_label).to_be_visible()
|
||||||
|
options = page.locator(".datalayer-permissions select[name='edit_status'] option")
|
||||||
|
expect(options).to_have_count(4)
|
||||||
|
option = page.locator(
|
||||||
|
".datalayer-permissions select[name='edit_status'] option:checked"
|
||||||
|
)
|
||||||
|
expect(option).to_have_text("Inherit")
|
||||||
|
|
||||||
|
|
||||||
|
def test_map_update_with_editor(map, live_server, login, user):
|
||||||
|
map.edit_status = Map.EDITORS
|
||||||
|
map.editors.add(user)
|
||||||
|
map.save()
|
||||||
|
page = login(user)
|
||||||
|
page.goto(f"{live_server.url}{map.get_absolute_url()}")
|
||||||
|
map_el = page.locator("#map")
|
||||||
|
expect(map_el).to_be_visible()
|
||||||
|
enable = page.get_by_role("link", name="Edit")
|
||||||
|
expect(enable).to_be_visible()
|
||||||
|
enable.click()
|
||||||
|
disable = page.get_by_role("link", name="Disable editing")
|
||||||
|
expect(disable).to_be_visible()
|
||||||
|
save = page.get_by_title("Save current edits (Ctrl+S)")
|
||||||
|
expect(save).to_be_visible()
|
||||||
|
add_marker = page.get_by_title("Draw a marker")
|
||||||
|
expect(add_marker).to_be_visible()
|
||||||
|
edit_settings = page.get_by_title("Edit map settings")
|
||||||
|
expect(edit_settings).to_be_visible()
|
||||||
|
edit_permissions = page.get_by_title("Update permissions and editors")
|
||||||
|
expect(edit_permissions).to_be_visible()
|
||||||
|
|
||||||
|
|
||||||
|
def test_permissions_form_with_editor(map, datalayer, live_server, login, user):
|
||||||
|
map.edit_status = Map.EDITORS
|
||||||
|
map.editors.add(user)
|
||||||
|
map.save()
|
||||||
|
page = login(user)
|
||||||
|
page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
|
||||||
|
edit_permissions = page.get_by_title("Update permissions and editors")
|
||||||
|
expect(edit_permissions).to_be_visible()
|
||||||
|
edit_permissions.click()
|
||||||
|
select = page.locator(".umap-field-share_status select")
|
||||||
|
expect(select).to_be_hidden()
|
||||||
|
# expect(select).to_have_value(Map.PUBLIC) # Does not work
|
||||||
|
owner_field = page.locator(".umap-field-owner")
|
||||||
|
expect(owner_field).to_be_hidden()
|
||||||
|
editors_field = page.locator(".umap-field-editors input")
|
||||||
|
expect(editors_field).to_be_visible()
|
||||||
|
datalayer_label = page.get_by_text('Who can edit "test datalayer"')
|
||||||
|
expect(datalayer_label).to_be_visible()
|
||||||
|
|
||||||
|
|
||||||
|
def test_owner_has_delete_map_button(map, live_server, login):
|
||||||
|
page = login(map.owner)
|
||||||
|
page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
|
||||||
|
settings = page.get_by_title("Edit map settings")
|
||||||
|
expect(settings).to_be_visible()
|
||||||
|
settings.click()
|
||||||
|
advanced = page.get_by_text("Advanced actions")
|
||||||
|
expect(advanced).to_be_visible()
|
||||||
|
advanced.click()
|
||||||
|
delete = page.get_by_role("link", name="Delete")
|
||||||
|
expect(delete).to_be_visible()
|
||||||
|
|
||||||
|
|
||||||
|
def test_editor_do_not_have_delete_map_button(map, live_server, login, user):
|
||||||
|
map.edit_status = Map.EDITORS
|
||||||
|
map.editors.add(user)
|
||||||
|
map.save()
|
||||||
|
page = login(user)
|
||||||
|
page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
|
||||||
|
settings = page.get_by_title("Edit map settings")
|
||||||
|
expect(settings).to_be_visible()
|
||||||
|
settings.click()
|
||||||
|
advanced = page.get_by_text("Advanced actions")
|
||||||
|
expect(advanced).to_be_visible()
|
||||||
|
advanced.click()
|
||||||
|
delete = page.get_by_role("link", name="Delete")
|
||||||
|
expect(delete).to_be_hidden()
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_change_perms_after_create(tilelayer, live_server, login, user):
|
||||||
|
page = login(user)
|
||||||
|
page.goto(f"{live_server.url}/en/map/new")
|
||||||
|
save = page.get_by_title("Save current edits")
|
||||||
|
expect(save).to_be_visible()
|
||||||
|
save.click()
|
||||||
|
sleep(1) # Let save ajax go back
|
||||||
|
edit_permissions = page.get_by_title("Update permissions and editors")
|
||||||
|
expect(edit_permissions).to_be_visible()
|
||||||
|
edit_permissions.click()
|
||||||
|
select = page.locator(".umap-field-share_status select")
|
||||||
|
expect(select).to_be_visible()
|
||||||
|
option = page.locator("select[name='share_status'] option:checked")
|
||||||
|
expect(option).to_have_text("Everyone (public)")
|
||||||
|
owner_field = page.locator(".umap-field-owner")
|
||||||
|
expect(owner_field).to_be_visible()
|
||||||
|
editors_field = page.locator(".umap-field-editors input")
|
||||||
|
expect(editors_field).to_be_visible()
|
||||||
|
datalayer_label = page.get_by_text('Who can edit "Layer 1"')
|
||||||
|
expect(datalayer_label).to_be_visible()
|
||||||
|
options = page.locator(".datalayer-permissions select[name='edit_status'] option")
|
||||||
|
expect(options).to_have_count(4)
|
||||||
|
option = page.locator(
|
||||||
|
".datalayer-permissions select[name='edit_status'] option:checked"
|
||||||
|
)
|
||||||
|
expect(option).to_have_text("Inherit")
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_change_owner(map, live_server, login, user):
|
||||||
|
page = login(map.owner)
|
||||||
|
page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
|
||||||
|
edit_permissions = page.get_by_title("Update permissions and editors")
|
||||||
|
edit_permissions.click()
|
||||||
|
close = page.locator(".umap-field-owner .close")
|
||||||
|
close.click()
|
||||||
|
input = page.locator("input.edit-owner")
|
||||||
|
input.type(user.username)
|
||||||
|
input.press("Tab")
|
||||||
|
save = page.get_by_title("Save current edits")
|
||||||
|
expect(save).to_be_visible()
|
||||||
|
save.click()
|
||||||
|
sleep(1) # Let save ajax go
|
||||||
|
modified = Map.objects.get(pk=map.pk)
|
||||||
|
assert modified.owner == user
|
|
@ -4,6 +4,7 @@ import pytest
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
|
|
||||||
from .base import DataLayerFactory, MapFactory
|
from .base import DataLayerFactory, MapFactory
|
||||||
|
from umap.models import DataLayer, Map
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
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):
|
def test_upload_to(map, datalayer):
|
||||||
map.pk = 302
|
map.pk = 302
|
||||||
datalayer.pk = 17
|
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):
|
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) in files
|
||||||
assert os.path.basename(other + ".gz") in files
|
assert os.path.basename(other + ".gz") in files
|
||||||
assert os.path.basename(older) not 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()
|
||||||
|
|
|
@ -245,3 +245,143 @@ def test_update_readonly(client, datalayer, map, post_data, settings):
|
||||||
client.login(username=map.owner.username, password="123123")
|
client.login(username=map.owner.username, password="123123")
|
||||||
response = client.post(url, post_data, follow=True)
|
response = client.post(url, post_data, follow=True)
|
||||||
assert response.status_code == 403
|
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
|
||||||
|
|
|
@ -60,6 +60,13 @@ def test_logged_in_user_should_be_allowed_for_anonymous_map_with_anonymous_edit_
|
||||||
assert map.can_edit(user, request)
|
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):
|
def test_clone_should_return_new_instance(map, user):
|
||||||
clone = map.clone()
|
clone = map.clone()
|
||||||
assert map.pk != clone.pk
|
assert map.pk != clone.pk
|
||||||
|
|
|
@ -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):
|
def test_wrong_slug_should_redirect_with_query_string(client, map):
|
||||||
url = reverse("map", kwargs={"map_id": map.pk, "slug": "wrong-slug"})
|
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 = reverse("map", kwargs={"map_id": map.pk, "slug": map.slug})
|
||||||
canonical = "{}?allowEdit=0".format(canonical)
|
canonical = "{}?editMode=simple".format(canonical)
|
||||||
response = client.get(url)
|
response = client.get(url)
|
||||||
assert response.status_code == 301
|
assert response.status_code == 301
|
||||||
assert response["Location"] == canonical
|
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):
|
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 = reverse("map", kwargs={"map_id": map.pk, "slug": map.slug})
|
||||||
url = "{}?allowEdit=0".format(url)
|
url = "{}?editMode=simple".format(url)
|
||||||
response = client.get(url)
|
response = client.get(url)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
22
umap/urls.py
22
umap/urls.py
|
@ -13,7 +13,7 @@ from . import views
|
||||||
from .decorators import (
|
from .decorators import (
|
||||||
jsonize_view,
|
jsonize_view,
|
||||||
login_required_if_not_anonymous_allowed,
|
login_required_if_not_anonymous_allowed,
|
||||||
map_permissions_check,
|
can_edit_map,
|
||||||
can_view_map,
|
can_view_map,
|
||||||
)
|
)
|
||||||
from .utils import decorated_patterns
|
from .utils import decorated_patterns
|
||||||
|
@ -144,16 +144,16 @@ map_urls = [
|
||||||
views.DataLayerCreate.as_view(),
|
views.DataLayerCreate.as_view(),
|
||||||
name="datalayer_create",
|
name="datalayer_create",
|
||||||
),
|
),
|
||||||
re_path(
|
|
||||||
r"^map/(?P<map_id>[\d]+)/datalayer/update/(?P<pk>\d+)/$",
|
|
||||||
views.DataLayerUpdate.as_view(),
|
|
||||||
name="datalayer_update",
|
|
||||||
),
|
|
||||||
re_path(
|
re_path(
|
||||||
r"^map/(?P<map_id>[\d]+)/datalayer/delete/(?P<pk>\d+)/$",
|
r"^map/(?P<map_id>[\d]+)/datalayer/delete/(?P<pk>\d+)/$",
|
||||||
views.DataLayerDelete.as_view(),
|
views.DataLayerDelete.as_view(),
|
||||||
name="datalayer_delete",
|
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:
|
if settings.FROM_EMAIL:
|
||||||
map_urls.append(
|
map_urls.append(
|
||||||
|
@ -163,7 +163,15 @@ if settings.FROM_EMAIL:
|
||||||
name="map_send_edit_link",
|
name="map_send_edit_link",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
i18n_urls += decorated_patterns([map_permissions_check, never_cache], *map_urls)
|
datalayer_urls = [
|
||||||
|
re_path(
|
||||||
|
r"^map/(?P<map_id>[\d]+)/datalayer/update/(?P<pk>\d+)/$",
|
||||||
|
views.DataLayerUpdate.as_view(),
|
||||||
|
name="datalayer_update",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
i18n_urls += decorated_patterns([can_edit_map, never_cache], *map_urls)
|
||||||
|
i18n_urls += decorated_patterns([never_cache], *datalayer_urls)
|
||||||
urlpatterns += i18n_patterns(
|
urlpatterns += i18n_patterns(
|
||||||
re_path(r"^$", views.home, name="home"),
|
re_path(r"^$", views.home, name="home"),
|
||||||
re_path(
|
re_path(
|
||||||
|
|
|
@ -45,8 +45,10 @@ from .forms import (
|
||||||
DEFAULT_LATITUDE,
|
DEFAULT_LATITUDE,
|
||||||
DEFAULT_LONGITUDE,
|
DEFAULT_LONGITUDE,
|
||||||
DEFAULT_CENTER,
|
DEFAULT_CENTER,
|
||||||
AnonymousMapPermissionsForm,
|
|
||||||
DataLayerForm,
|
DataLayerForm,
|
||||||
|
DataLayerPermissionsForm,
|
||||||
|
AnonymousDataLayerPermissionsForm,
|
||||||
|
AnonymousMapPermissionsForm,
|
||||||
FlatErrorList,
|
FlatErrorList,
|
||||||
MapSettingsForm,
|
MapSettingsForm,
|
||||||
SendLinkForm,
|
SendLinkForm,
|
||||||
|
@ -445,23 +447,33 @@ class MapDetailMixin:
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
user = self.request.user
|
||||||
properties = {
|
properties = {
|
||||||
"urls": _urls_for_js(),
|
"urls": _urls_for_js(),
|
||||||
"tilelayers": TileLayer.get_list(),
|
"tilelayers": TileLayer.get_list(),
|
||||||
"allowEdit": self.is_edit_allowed(),
|
"editMode": self.edit_mode,
|
||||||
"default_iconUrl": "%sumap/img/marker.png" % settings.STATIC_URL, # noqa
|
"default_iconUrl": "%sumap/img/marker.png" % settings.STATIC_URL, # noqa
|
||||||
"umap_id": self.get_umap_id(),
|
"umap_id": self.get_umap_id(),
|
||||||
"starred": self.is_starred(),
|
"starred": self.is_starred(),
|
||||||
"licences": dict((l.name, l.json) for l in Licence.objects.all()),
|
"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": [
|
"share_statuses": [
|
||||||
(i, str(label)) for i, label in Map.SHARE_STATUS if i != Map.BLOCKED
|
(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,
|
"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():
|
if self.get_short_url():
|
||||||
properties["shortUrl"] = self.get_short_url()
|
properties["shortUrl"] = self.get_short_url()
|
||||||
|
|
||||||
|
@ -474,7 +486,6 @@ class MapDetailMixin:
|
||||||
locale = to_locale(lang)
|
locale = to_locale(lang)
|
||||||
properties["locale"] = locale
|
properties["locale"] = locale
|
||||||
context["locale"] = locale
|
context["locale"] = locale
|
||||||
user = self.request.user
|
|
||||||
if not user.is_anonymous:
|
if not user.is_anonymous:
|
||||||
properties["user"] = {
|
properties["user"] = {
|
||||||
"id": user.pk,
|
"id": user.pk,
|
||||||
|
@ -492,8 +503,9 @@ class MapDetailMixin:
|
||||||
def get_datalayers(self):
|
def get_datalayers(self):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def is_edit_allowed(self):
|
@property
|
||||||
return True
|
def edit_mode(self):
|
||||||
|
return "advanced"
|
||||||
|
|
||||||
def get_umap_id(self):
|
def get_umap_id(self):
|
||||||
return None
|
return None
|
||||||
|
@ -551,11 +563,22 @@ class MapView(MapDetailMixin, PermissionsMixin, DetailView):
|
||||||
return self.object.get_absolute_url()
|
return self.object.get_absolute_url()
|
||||||
|
|
||||||
def get_datalayers(self):
|
def get_datalayers(self):
|
||||||
datalayers = DataLayer.objects.filter(map=self.object)
|
return [
|
||||||
return [l.metadata for l in datalayers]
|
l.metadata(self.request.user, self.request)
|
||||||
|
for l in self.object.datalayer_set.all()
|
||||||
|
]
|
||||||
|
|
||||||
def is_edit_allowed(self):
|
@property
|
||||||
return self.object.can_edit(self.request.user, self.request)
|
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):
|
def get_umap_id(self):
|
||||||
return self.object.pk
|
return self.object.pk
|
||||||
|
@ -883,7 +906,9 @@ class DataLayerCreate(FormLessEditMixin, GZipMixin, CreateView):
|
||||||
form.instance.map = self.kwargs["map_inst"]
|
form.instance.map = self.kwargs["map_inst"]
|
||||||
self.object = form.save()
|
self.object = form.save()
|
||||||
# Simple response with only metadatas (including new id)
|
# 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
|
response["Last-Modified"] = self.last_modified
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -896,7 +921,9 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
|
||||||
self.object = form.save()
|
self.object = form.save()
|
||||||
# Simple response with only metadatas (client should not reload all data
|
# Simple response with only metadatas (client should not reload all data
|
||||||
# on save)
|
# 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
|
response["Last-Modified"] = self.last_modified
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -911,7 +938,9 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
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()
|
return HttpResponseForbidden()
|
||||||
if not self.is_unmodified():
|
if not self.is_unmodified():
|
||||||
return HttpResponse(status=412)
|
return HttpResponse(status=412)
|
||||||
|
@ -936,6 +965,21 @@ class DataLayerVersions(BaseDetailView):
|
||||||
return simple_json_response(versions=self.object.versions)
|
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 #
|
# Picto #
|
||||||
# ############## #
|
# ############## #
|
||||||
|
|
Loading…
Reference in a new issue