mirror of
https://github.com/umap-project/umap.git
synced 2025-04-29 03:42:37 +02:00
commit
aeee58b42a
34 changed files with 852 additions and 108 deletions
|
@ -1,6 +1,6 @@
|
||||||
from django.contrib.gis import admin
|
from django.contrib.gis import admin
|
||||||
|
|
||||||
from .models import DataLayer, Licence, Map, Pictogram, TileLayer
|
from .models import DataLayer, Licence, Map, Pictogram, Team, TileLayer
|
||||||
|
|
||||||
|
|
||||||
class TileLayerAdmin(admin.ModelAdmin):
|
class TileLayerAdmin(admin.ModelAdmin):
|
||||||
|
@ -26,8 +26,13 @@ class PictogramAdmin(admin.ModelAdmin):
|
||||||
list_filter = ("category",)
|
list_filter = ("category",)
|
||||||
|
|
||||||
|
|
||||||
|
class TeamAdmin(admin.ModelAdmin):
|
||||||
|
filter_horizontal = ("users",)
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Map, MapAdmin)
|
admin.site.register(Map, MapAdmin)
|
||||||
admin.site.register(DataLayer)
|
admin.site.register(DataLayer)
|
||||||
admin.site.register(Pictogram, PictogramAdmin)
|
admin.site.register(Pictogram, PictogramAdmin)
|
||||||
admin.site.register(TileLayer, TileLayerAdmin)
|
admin.site.register(TileLayer, TileLayerAdmin)
|
||||||
admin.site.register(Licence)
|
admin.site.register(Licence)
|
||||||
|
admin.site.register(Team, TeamAdmin)
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django.http import HttpResponseForbidden
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
from .models import Map
|
from .models import Map, Team
|
||||||
from .views import simple_json_response
|
from .views import simple_json_response
|
||||||
|
|
||||||
LOGIN_URL = getattr(settings, "LOGIN_URL", "login")
|
LOGIN_URL = getattr(settings, "LOGIN_URL", "login")
|
||||||
|
@ -35,7 +35,7 @@ def can_edit_map(view_func):
|
||||||
map_inst = get_object_or_404(Map, pk=kwargs["map_id"])
|
map_inst = get_object_or_404(Map, pk=kwargs["map_id"])
|
||||||
user = request.user
|
user = request.user
|
||||||
kwargs["map_inst"] = map_inst # Avoid rerequesting the map in the view
|
kwargs["map_inst"] = map_inst # Avoid rerequesting the map in the view
|
||||||
if map_inst.edit_status >= map_inst.EDITORS:
|
if map_inst.edit_status >= map_inst.COLLABORATORS:
|
||||||
can_edit = map_inst.can_edit(user=user, request=request)
|
can_edit = map_inst.can_edit(user=user, request=request)
|
||||||
if not can_edit:
|
if not can_edit:
|
||||||
if map_inst.owner and not user.is_authenticated:
|
if map_inst.owner and not user.is_authenticated:
|
||||||
|
@ -60,3 +60,14 @@ def can_view_map(view_func):
|
||||||
return view_func(request, *args, **kwargs)
|
return view_func(request, *args, **kwargs)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def team_members_only(view_func):
|
||||||
|
@wraps(view_func)
|
||||||
|
def wrapper(request, *args, **kwargs):
|
||||||
|
team = get_object_or_404(Team, pk=kwargs["pk"])
|
||||||
|
if not request.user.is_authenticated or team not in request.user.teams.all():
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
return view_func(request, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.forms.utils import ErrorList
|
||||||
from django.template.defaultfilters import slugify
|
from django.template.defaultfilters import slugify
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .models import DataLayer, Map
|
from .models import DataLayer, Map, Team
|
||||||
|
|
||||||
DEFAULT_LATITUDE = (
|
DEFAULT_LATITUDE = (
|
||||||
settings.LEAFLET_LATITUDE if hasattr(settings, "LEAFLET_LATITUDE") else 51
|
settings.LEAFLET_LATITUDE if hasattr(settings, "LEAFLET_LATITUDE") else 51
|
||||||
|
@ -36,7 +36,7 @@ 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", "team")
|
||||||
|
|
||||||
|
|
||||||
class AnonymousMapPermissionsForm(forms.ModelForm):
|
class AnonymousMapPermissionsForm(forms.ModelForm):
|
||||||
|
@ -110,3 +110,27 @@ class UserProfileForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = ("username", "first_name", "last_name")
|
fields = ("username", "first_name", "last_name")
|
||||||
|
|
||||||
|
|
||||||
|
class TeamMembersField(forms.ModelMultipleChoiceField):
|
||||||
|
def set_choices(self, choices):
|
||||||
|
iterator = self.iterator(self)
|
||||||
|
# Override queryset so to expose only selected choices:
|
||||||
|
# - we don't want a select with 100000 options
|
||||||
|
# - the select values will be used by the autocomplete widget to display
|
||||||
|
# already existing members of the team
|
||||||
|
iterator.queryset = choices
|
||||||
|
self.choices = iterator
|
||||||
|
|
||||||
|
|
||||||
|
class TeamForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Team
|
||||||
|
fields = ["name", "description", "members"]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["members"].set_choices(self.initial["members"])
|
||||||
|
self.fields["members"].widget.attrs["hidden"] = "hidden"
|
||||||
|
|
||||||
|
members = TeamMembersField(queryset=User.objects.all())
|
||||||
|
|
94
umap/migrations/0022_add_team.py
Normal file
94
umap/migrations/0022_add_team.py
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
# Generated by Django 5.1 on 2024-08-15 11:33
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import umap.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [("umap", "0021_remove_map_description")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Team",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(max_length=200, unique=True, verbose_name="name"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"description",
|
||||||
|
models.TextField(blank=True, null=True, verbose_name="description"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"users",
|
||||||
|
models.ManyToManyField(
|
||||||
|
related_name="teams", to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="map",
|
||||||
|
name="team",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="umap.team",
|
||||||
|
verbose_name="team",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="datalayer",
|
||||||
|
name="edit_status",
|
||||||
|
field=models.SmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(0, "Inherit"),
|
||||||
|
(1, "Everyone"),
|
||||||
|
(2, "Editors and team only"),
|
||||||
|
(3, "Owner only"),
|
||||||
|
],
|
||||||
|
default=0,
|
||||||
|
verbose_name="edit status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="map",
|
||||||
|
name="edit_status",
|
||||||
|
field=models.SmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(1, "Everyone"),
|
||||||
|
(2, "Editors and team only"),
|
||||||
|
(3, "Owner only"),
|
||||||
|
],
|
||||||
|
default=umap.models.get_default_edit_status,
|
||||||
|
verbose_name="edit status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="map",
|
||||||
|
name="share_status",
|
||||||
|
field=models.SmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(1, "Everyone (public)"),
|
||||||
|
(2, "Anyone with link"),
|
||||||
|
(3, "Editors and team only"),
|
||||||
|
(9, "Blocked"),
|
||||||
|
],
|
||||||
|
default=umap.models.get_default_share_status,
|
||||||
|
verbose_name="share status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -49,6 +49,26 @@ def get_default_edit_status():
|
||||||
return settings.UMAP_DEFAULT_EDIT_STATUS or Map.OWNER
|
return settings.UMAP_DEFAULT_EDIT_STATUS or Map.OWNER
|
||||||
|
|
||||||
|
|
||||||
|
class Team(models.Model):
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=200, verbose_name=_("name"), unique=True, blank=False, null=False
|
||||||
|
)
|
||||||
|
description = models.TextField(blank=True, null=True, verbose_name=_("description"))
|
||||||
|
users = models.ManyToManyField(User, related_name="teams")
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_url(self):
|
||||||
|
return reverse("team_maps", kwargs={"pk": self.pk})
|
||||||
|
|
||||||
|
def get_metadata(self):
|
||||||
|
return {"id": self.pk, "name": self.name, "url": self.get_url()}
|
||||||
|
|
||||||
|
|
||||||
class NamedModel(models.Model):
|
class NamedModel(models.Model):
|
||||||
name = models.CharField(max_length=200, verbose_name=_("name"))
|
name = models.CharField(max_length=200, verbose_name=_("name"))
|
||||||
|
|
||||||
|
@ -137,7 +157,7 @@ class Map(NamedModel):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ANONYMOUS = 1
|
ANONYMOUS = 1
|
||||||
EDITORS = 2
|
COLLABORATORS = 2
|
||||||
OWNER = 3
|
OWNER = 3
|
||||||
PUBLIC = 1
|
PUBLIC = 1
|
||||||
OPEN = 2
|
OPEN = 2
|
||||||
|
@ -145,13 +165,13 @@ class Map(NamedModel):
|
||||||
BLOCKED = 9
|
BLOCKED = 9
|
||||||
EDIT_STATUS = (
|
EDIT_STATUS = (
|
||||||
(ANONYMOUS, _("Everyone")),
|
(ANONYMOUS, _("Everyone")),
|
||||||
(EDITORS, _("Editors only")),
|
(COLLABORATORS, _("Editors and team only")),
|
||||||
(OWNER, _("Owner only")),
|
(OWNER, _("Owner only")),
|
||||||
)
|
)
|
||||||
SHARE_STATUS = (
|
SHARE_STATUS = (
|
||||||
(PUBLIC, _("Everyone (public)")),
|
(PUBLIC, _("Everyone (public)")),
|
||||||
(OPEN, _("Anyone with link")),
|
(OPEN, _("Anyone with link")),
|
||||||
(PRIVATE, _("Editors only")),
|
(PRIVATE, _("Editors and team only")),
|
||||||
(BLOCKED, _("Blocked")),
|
(BLOCKED, _("Blocked")),
|
||||||
)
|
)
|
||||||
slug = models.SlugField(db_index=True)
|
slug = models.SlugField(db_index=True)
|
||||||
|
@ -180,6 +200,13 @@ class Map(NamedModel):
|
||||||
editors = models.ManyToManyField(
|
editors = models.ManyToManyField(
|
||||||
settings.AUTH_USER_MODEL, blank=True, verbose_name=_("editors")
|
settings.AUTH_USER_MODEL, blank=True, verbose_name=_("editors")
|
||||||
)
|
)
|
||||||
|
team = models.ForeignKey(
|
||||||
|
Team,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_("team"),
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
)
|
||||||
edit_status = models.SmallIntegerField(
|
edit_status = models.SmallIntegerField(
|
||||||
choices=EDIT_STATUS,
|
choices=EDIT_STATUS,
|
||||||
default=get_default_edit_status,
|
default=get_default_edit_status,
|
||||||
|
@ -251,6 +278,9 @@ class Map(NamedModel):
|
||||||
path = reverse("map_anonymous_edit_url", kwargs={"signature": signature})
|
path = reverse("map_anonymous_edit_url", kwargs={"signature": signature})
|
||||||
return settings.SITE_URL + path
|
return settings.SITE_URL + path
|
||||||
|
|
||||||
|
def get_author(self):
|
||||||
|
return self.team or self.owner
|
||||||
|
|
||||||
def is_owner(self, user=None, request=None):
|
def is_owner(self, user=None, request=None):
|
||||||
if user and self.owner == user:
|
if user and self.owner == user:
|
||||||
return True
|
return True
|
||||||
|
@ -281,7 +311,7 @@ class Map(NamedModel):
|
||||||
|
|
||||||
In owner mode:
|
In owner mode:
|
||||||
- only owner by default (OWNER)
|
- only owner by default (OWNER)
|
||||||
- any editor if mode is EDITORS
|
- any editor or team member if mode is COLLABORATORS
|
||||||
- anyone otherwise (ANONYMOUS)
|
- anyone otherwise (ANONYMOUS)
|
||||||
In anonymous owner mode:
|
In anonymous owner mode:
|
||||||
- only owner (has ownership cookie) by default (OWNER)
|
- only owner (has ownership cookie) by default (OWNER)
|
||||||
|
@ -297,8 +327,9 @@ class Map(NamedModel):
|
||||||
can = False
|
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.COLLABORATORS:
|
||||||
can = True
|
if user in self.editors.all() or self.team in user.teams.all():
|
||||||
|
can = True
|
||||||
return can
|
return can
|
||||||
|
|
||||||
def can_view(self, request):
|
def can_view(self, request):
|
||||||
|
@ -308,12 +339,15 @@ class Map(NamedModel):
|
||||||
can = True
|
can = True
|
||||||
elif self.share_status in [self.PUBLIC, self.OPEN]:
|
elif self.share_status in [self.PUBLIC, self.OPEN]:
|
||||||
can = True
|
can = True
|
||||||
|
elif not request.user.is_authenticated:
|
||||||
|
can = False
|
||||||
elif request.user == self.owner:
|
elif request.user == self.owner:
|
||||||
can = True
|
can = True
|
||||||
else:
|
else:
|
||||||
can = not (
|
can = not (
|
||||||
self.share_status == self.PRIVATE
|
self.share_status == self.PRIVATE
|
||||||
and request.user not in self.editors.all()
|
and request.user not in self.editors.all()
|
||||||
|
and self.team not in request.user.teams.all()
|
||||||
)
|
)
|
||||||
return can
|
return can
|
||||||
|
|
||||||
|
@ -383,12 +417,12 @@ class DataLayer(NamedModel):
|
||||||
|
|
||||||
INHERIT = 0
|
INHERIT = 0
|
||||||
ANONYMOUS = 1
|
ANONYMOUS = 1
|
||||||
EDITORS = 2
|
COLLABORATORS = 2
|
||||||
OWNER = 3
|
OWNER = 3
|
||||||
EDIT_STATUS = (
|
EDIT_STATUS = (
|
||||||
(INHERIT, _("Inherit")),
|
(INHERIT, _("Inherit")),
|
||||||
(ANONYMOUS, _("Everyone")),
|
(ANONYMOUS, _("Everyone")),
|
||||||
(EDITORS, _("Editors only")),
|
(COLLABORATORS, _("Editors and team only")),
|
||||||
(OWNER, _("Owner only")),
|
(OWNER, _("Owner only")),
|
||||||
)
|
)
|
||||||
uuid = models.UUIDField(
|
uuid = models.UUIDField(
|
||||||
|
@ -538,8 +572,9 @@ class DataLayer(NamedModel):
|
||||||
can = True
|
can = True
|
||||||
elif user is not None and user == self.map.owner:
|
elif user is not None and user == self.map.owner:
|
||||||
can = True
|
can = True
|
||||||
elif self.edit_status == self.EDITORS and user in self.map.editors.all():
|
elif user is not None and self.edit_status == self.COLLABORATORS:
|
||||||
can = True
|
if user in self.map.editors.all() or self.map.team in user.teams.all():
|
||||||
|
can = True
|
||||||
return can
|
return can
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -345,7 +345,7 @@ input + .error {
|
||||||
input[type="file"] + .error {
|
input[type="file"] + .error {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
input:invalid {
|
input[value]:invalid {
|
||||||
border-color: red;
|
border-color: red;
|
||||||
background-color: darkred;
|
background-color: darkred;
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,6 +133,8 @@ h2.section {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
h2.tabs {
|
h2.tabs {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--color-darkBlue);
|
color: var(--color-darkBlue);
|
||||||
text-align: start;
|
text-align: start;
|
||||||
|
@ -144,7 +146,6 @@ h2.tabs a {
|
||||||
text-decoration-thickness: 3px;
|
text-decoration-thickness: 3px;
|
||||||
text-decoration-skip-ink: none;
|
text-decoration-skip-ink: none;
|
||||||
text-underline-offset: 7px;
|
text-underline-offset: 7px;
|
||||||
margin-inline-end: 2rem;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
h2.tabs a:not(.selected) {
|
h2.tabs a:not(.selected) {
|
||||||
|
@ -193,26 +194,15 @@ input[type="submit"],
|
||||||
.button-primary {
|
.button-primary {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.wrapper input[type="submit"]:hover {
|
|
||||||
background-color: #35537c;
|
|
||||||
}
|
|
||||||
.wrapper .neutral, .wrapper input[type="submit"].neutral {
|
.wrapper .neutral, .wrapper input[type="submit"].neutral {
|
||||||
background-color: var(--button-neutral-background);
|
background-color: var(--button-neutral-background);
|
||||||
color: var(--button-neutral-color);
|
color: var(--button-neutral-color);
|
||||||
}
|
}
|
||||||
.wrapper.somber {
|
|
||||||
background-color: #2E3641;
|
|
||||||
color: #efefef;
|
|
||||||
padding-top: 20px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
.wrapper.somber .row {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
.wrapper .button,
|
.wrapper .button,
|
||||||
.wrapper input {
|
.wrapper input {
|
||||||
height: 56px;
|
height: 56px;
|
||||||
line-height: 43px;
|
line-height: 43px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* **************************** */
|
/* **************************** */
|
||||||
|
@ -273,9 +263,7 @@ ul.umap-autocomplete {
|
||||||
border: 1px solid #202425;
|
border: 1px solid #202425;
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
color: #eeeeec;
|
color: #eeeeec;
|
||||||
}
|
margin-bottom: 7px;
|
||||||
.umap-multiresult li + li {
|
|
||||||
margin-top: 7px;
|
|
||||||
}
|
}
|
||||||
.umap-singleresult div .close,
|
.umap-singleresult div .close,
|
||||||
.umap-multiresult li .close {
|
.umap-multiresult li .close {
|
||||||
|
|
|
@ -9,8 +9,8 @@ import { Request, ServerRequest } from './request.js'
|
||||||
import { escapeHTML, generateId } from './utils.js'
|
import { escapeHTML, generateId } from './utils.js'
|
||||||
|
|
||||||
export class BaseAutocomplete {
|
export class BaseAutocomplete {
|
||||||
constructor(el, options) {
|
constructor(parent, options) {
|
||||||
this.el = el
|
this.parent = parent
|
||||||
this.options = {
|
this.options = {
|
||||||
placeholder: translate('Start typing...'),
|
placeholder: translate('Start typing...'),
|
||||||
emptyMessage: translate('No result'),
|
emptyMessage: translate('No result'),
|
||||||
|
@ -43,7 +43,7 @@ export class BaseAutocomplete {
|
||||||
this.input = DomUtil.element({
|
this.input = DomUtil.element({
|
||||||
tagName: 'input',
|
tagName: 'input',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
parent: this.el,
|
parent: this.parent,
|
||||||
placeholder: this.options.placeholder,
|
placeholder: this.options.placeholder,
|
||||||
autocomplete: 'off',
|
autocomplete: 'off',
|
||||||
className: this.options.className,
|
className: this.options.className,
|
||||||
|
|
|
@ -20,7 +20,7 @@ export default class Caption {
|
||||||
const container = DomUtil.create('div', 'umap-caption')
|
const container = DomUtil.create('div', 'umap-caption')
|
||||||
const hgroup = DomUtil.element({ tagName: 'hgroup', parent: container })
|
const hgroup = DomUtil.element({ tagName: 'hgroup', parent: container })
|
||||||
DomUtil.createTitle(hgroup, this.map.options.name, 'icon-caption icon-block')
|
DomUtil.createTitle(hgroup, this.map.options.name, 'icon-caption icon-block')
|
||||||
this.map.permissions.addOwnerLink('h4', hgroup)
|
this.map.addAuthorLink('h4', hgroup)
|
||||||
if (this.map.options.description) {
|
if (this.map.options.description) {
|
||||||
const description = DomUtil.element({
|
const description = DomUtil.element({
|
||||||
tagName: 'div',
|
tagName: 'div',
|
||||||
|
|
|
@ -25,6 +25,7 @@ export class MapPermissions {
|
||||||
this.options = Object.assign(
|
this.options = Object.assign(
|
||||||
{
|
{
|
||||||
owner: null,
|
owner: null,
|
||||||
|
team: null,
|
||||||
editors: [],
|
editors: [],
|
||||||
share_status: null,
|
share_status: null,
|
||||||
edit_status: null,
|
edit_status: null,
|
||||||
|
@ -96,6 +97,16 @@ export class MapPermissions {
|
||||||
'options.owner',
|
'options.owner',
|
||||||
{ handler: 'ManageOwner', label: translate("Map's owner") },
|
{ handler: 'ManageOwner', label: translate("Map's owner") },
|
||||||
])
|
])
|
||||||
|
if (this.map.options.user?.teams?.length) {
|
||||||
|
fields.push([
|
||||||
|
'options.team',
|
||||||
|
{
|
||||||
|
handler: 'ManageTeam',
|
||||||
|
label: translate('Attach map to a team'),
|
||||||
|
teams: this.map.options.user.teams,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fields.push([
|
fields.push([
|
||||||
'options.editors',
|
'options.editors',
|
||||||
|
@ -150,6 +161,7 @@ export class MapPermissions {
|
||||||
formData.append('edit_status', this.options.edit_status)
|
formData.append('edit_status', this.options.edit_status)
|
||||||
if (this.isOwner()) {
|
if (this.isOwner()) {
|
||||||
formData.append('owner', this.options.owner?.id)
|
formData.append('owner', this.options.owner?.id)
|
||||||
|
formData.append('team', this.options.team?.id || '')
|
||||||
formData.append('share_status', this.options.share_status)
|
formData.append('share_status', this.options.share_status)
|
||||||
}
|
}
|
||||||
const [data, response, error] = await this.map.server.post(
|
const [data, response, error] = await this.map.server.post(
|
||||||
|
@ -177,23 +189,6 @@ export class MapPermissions {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
addOwnerLink(element, container) {
|
|
||||||
if (this.options.owner?.name && this.options.owner.url) {
|
|
||||||
const ownerContainer = DomUtil.add(
|
|
||||||
element,
|
|
||||||
'umap-map-owner',
|
|
||||||
container,
|
|
||||||
` ${translate('by')} `
|
|
||||||
)
|
|
||||||
DomUtil.createLink(
|
|
||||||
'',
|
|
||||||
ownerContainer,
|
|
||||||
this.options.owner.name,
|
|
||||||
this.options.owner.url
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commit() {
|
commit() {
|
||||||
this.map.options.permissions = Object.assign(
|
this.map.options.permissions = Object.assign(
|
||||||
this.map.options.permissions,
|
this.map.options.permissions,
|
||||||
|
|
|
@ -1086,6 +1086,23 @@ L.FormBuilder.ManageEditors = L.FormBuilder.Element.extend({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
L.FormBuilder.ManageTeam = L.FormBuilder.IntSelect.extend({
|
||||||
|
getOptions: function () {
|
||||||
|
return [[null, L._('None')]].concat(
|
||||||
|
this.options.teams.map((team) => [team.id, team.name])
|
||||||
|
)
|
||||||
|
},
|
||||||
|
toHTML: function () {
|
||||||
|
return this.get()?.id
|
||||||
|
},
|
||||||
|
toJS: function () {
|
||||||
|
const value = this.value()
|
||||||
|
for (const team of this.options.teams) {
|
||||||
|
if (team.id === value) return team
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
U.FormBuilder = L.FormBuilder.extend({
|
U.FormBuilder = L.FormBuilder.extend({
|
||||||
options: {
|
options: {
|
||||||
className: 'umap-form',
|
className: 'umap-form',
|
||||||
|
|
|
@ -1582,7 +1582,7 @@ U.Map = L.Map.extend({
|
||||||
)
|
)
|
||||||
const name = L.DomUtil.create('h3', '', container)
|
const name = L.DomUtil.create('h3', '', container)
|
||||||
L.DomEvent.disableClickPropagation(container)
|
L.DomEvent.disableClickPropagation(container)
|
||||||
this.permissions.addOwnerLink('span', container)
|
this.addAuthorLink('span', container)
|
||||||
if (this.getOption('captionMenus')) {
|
if (this.getOption('captionMenus')) {
|
||||||
L.DomUtil.createButton(
|
L.DomUtil.createButton(
|
||||||
'umap-about-link flat',
|
'umap-about-link flat',
|
||||||
|
@ -1887,4 +1887,21 @@ U.Map = L.Map.extend({
|
||||||
.filter((val, idx, arr) => arr.indexOf(val) === idx)
|
.filter((val, idx, arr) => arr.indexOf(val) === idx)
|
||||||
.sort(U.Utils.naturalSort)
|
.sort(U.Utils.naturalSort)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addAuthorLink: function (element, container) {
|
||||||
|
if (this.options.author?.name) {
|
||||||
|
const authorContainer = L.DomUtil.add(
|
||||||
|
element,
|
||||||
|
'umap-map-author',
|
||||||
|
container,
|
||||||
|
` ${L._('by')} `
|
||||||
|
)
|
||||||
|
L.DomUtil.createLink(
|
||||||
|
'',
|
||||||
|
authorContainer,
|
||||||
|
this.options.author.name,
|
||||||
|
this.options.author.url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -922,7 +922,7 @@ a.umap-control-caption,
|
||||||
.datalayer-name {
|
.datalayer-name {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.umap-caption .umap-map-owner {
|
.umap-caption .umap-map-author {
|
||||||
padding-inline-start: 31px;
|
padding-inline-start: 31px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,7 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block maincontent %}
|
{% block maincontent %}
|
||||||
<div class="row">
|
{% include "umap/dashboard_menu.html" with selected="profile" %}
|
||||||
<h2 class="section tabs">
|
|
||||||
<a href="{% url "user_dashboard" %}">{% trans "My Maps" %}</a>
|
|
||||||
<a class="selected" href="{% url 'user_profile' %}">{% trans "My Profile" %}</a>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% if form.non_field_errors %}
|
{% if form.non_field_errors %}
|
||||||
|
|
15
umap/templates/umap/dashboard_menu.html
Normal file
15
umap/templates/umap/dashboard_menu.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<h2 class="section tabs">
|
||||||
|
{% if selected == "maps" %}
|
||||||
|
<a class="selected" href="{% url 'user_dashboard' %}">{% blocktranslate with count=maps.paginator.count %}My Maps ({{ count }}){% endblocktranslate %}</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'user_dashboard' %}">{% trans "My Maps" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
<a {% if selected == "profile" %}class="selected"{% endif %}
|
||||||
|
href="{% url 'user_profile' %}">{% trans "My profile" %}</a>
|
||||||
|
<a {% if selected == "teams" %}class="selected"{% endif %}
|
||||||
|
href="{% url 'user_teams' %}">{% trans "My teams" %}</a>
|
||||||
|
</h2>
|
||||||
|
</div>
|
|
@ -6,9 +6,9 @@
|
||||||
{% map_fragment map_inst prefix=prefix page=request.GET.p %}
|
{% map_fragment map_inst prefix=prefix page=request.GET.p %}
|
||||||
<div class="legend">
|
<div class="legend">
|
||||||
<a href="{{ map_inst.get_absolute_url }}">{{ map_inst.name }}</a>
|
<a href="{{ map_inst.get_absolute_url }}">{{ map_inst.name }}</a>
|
||||||
{% if map_inst.owner %}
|
{% with author=map_inst.get_author %}
|
||||||
<em>{% trans "by" %} <a href="{{ map_inst.owner.get_url }}">{{ map_inst.owner }}</a></em>
|
<em>{% trans "by" %} <a href="{{ author.get_url }}">{{ author }}</a></em>
|
||||||
{% endif %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
19
umap/templates/umap/team_confirm_delete.html
Normal file
19
umap/templates/umap/team_confirm_delete.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends "umap/content.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block maincontent %}
|
||||||
|
{% include "umap/dashboard_menu.html" with selected="teams" %}
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="row">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>
|
||||||
|
Are you sure you want to delete "{{ object }}"?
|
||||||
|
</p>
|
||||||
|
{{ form }}
|
||||||
|
<input type="submit" value="Confirm">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock maincontent %}
|
27
umap/templates/umap/team_detail.html
Normal file
27
umap/templates/umap/team_detail.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends "umap/content.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block maincontent %}
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col wide">
|
||||||
|
<h2 class="section">
|
||||||
|
{% blocktrans %}Browse {{ current_team }}'s maps{% endblocktrans %}
|
||||||
|
</h2>
|
||||||
|
{% if current_team.description %}
|
||||||
|
<p>{{ current_team.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="map_list row">
|
||||||
|
{% if maps %}
|
||||||
|
{% include "umap/map_list.html" %}
|
||||||
|
{% else %}
|
||||||
|
<div>
|
||||||
|
{% blocktrans %}{{ current_team }} has no public maps.{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock maincontent %}
|
60
umap/templates/umap/team_form.html
Normal file
60
umap/templates/umap/team_form.html
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
{% extends "umap/content.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block maincontent %}
|
||||||
|
{% include "umap/dashboard_menu.html" with selected="teams" %}
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="row">
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<ul class="form-errors">
|
||||||
|
{% for error in form.non_field_errors %}
|
||||||
|
<li>
|
||||||
|
{{ error }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
<form id="team_form" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form }}
|
||||||
|
<input type="submit" value="{% trans "Save" %}" />
|
||||||
|
</form>
|
||||||
|
{% if team.users.count == 1 %}
|
||||||
|
<a href="{% url 'team_delete' team.pk %}">{% trans "Delete this team" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="module" defer>
|
||||||
|
const form = document.querySelector("#team_form")
|
||||||
|
const select = form.querySelector('#id_members')
|
||||||
|
if (select) {
|
||||||
|
function onSelect({item: {value, label}}) {
|
||||||
|
const option = document.createElement('option')
|
||||||
|
option.value = value
|
||||||
|
option.textContent = label
|
||||||
|
option.selected = "selected"
|
||||||
|
select.appendChild(option)
|
||||||
|
}
|
||||||
|
function onUnselect({item: {value, label}}) {
|
||||||
|
const option = select.querySelector(`[value="${value}"]`)
|
||||||
|
select.removeChild(option)
|
||||||
|
}
|
||||||
|
const options = {
|
||||||
|
className: 'edit-team-members',
|
||||||
|
on_select: onSelect,
|
||||||
|
on_unselect: onUnselect,
|
||||||
|
placeholder: "{% trans "Add user" %}"
|
||||||
|
}
|
||||||
|
const autocomplete = new U.AjaxAutocompleteMultiple(form, options)
|
||||||
|
for (const option of select.options) {
|
||||||
|
autocomplete.displaySelected({
|
||||||
|
item: { value: option.value, label: option.textContent },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const submit = form.querySelector('input[type="submit"]')
|
||||||
|
// Move it after the autocomplete widget.
|
||||||
|
form.appendChild(submit)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock maincontent %}
|
|
@ -7,13 +7,7 @@
|
||||||
{% endblock head_title %}
|
{% endblock head_title %}
|
||||||
{% block maincontent %}
|
{% block maincontent %}
|
||||||
{% trans "Search my maps" as placeholder %}
|
{% trans "Search my maps" as placeholder %}
|
||||||
<div class="row">
|
{% include "umap/dashboard_menu.html" with selected="maps" %}
|
||||||
<h2 class="section tabs">
|
|
||||||
<a class="selected" href="{% url 'user_dashboard' %}">{% blocktranslate with count=maps.paginator.count %}My Maps ({{ count }}){% endblocktranslate %}
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'user_profile' %}">{% trans "My profile" %}</a>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
|
|
51
umap/templates/umap/user_teams.html
Normal file
51
umap/templates/umap/user_teams.html
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
{% extends "umap/content.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block maincontent %}
|
||||||
|
{% include "umap/dashboard_menu.html" with selected="teams" %}
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="row">
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
{% blocktrans %}Name{% endblocktrans %}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{% blocktrans %}Users{% endblocktrans %}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{% blocktrans %}Actions{% endblocktrans %}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for team in teams %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<a href="{% url 'team_maps' team.pk %}">{{ team }}</a>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
{% for user in team.users.all %}
|
||||||
|
{{ user }}{% if not forloop.last %}, {% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'team_update' team.pk %}"
|
||||||
|
class="icon-link"
|
||||||
|
title="{% translate "Edit" %}">
|
||||||
|
<span class="icon-dashboard icon-edit"></span>
|
||||||
|
<span class="sr-only">{% translate "Edit" %}</span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<a class="button" href="{% url 'team_new' %}">{% trans "New team" %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock maincontent %}
|
|
@ -7,7 +7,7 @@ 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
|
||||||
from umap.models import DataLayer, Licence, Map, TileLayer
|
from umap.models import DataLayer, Licence, Map, Team, TileLayer
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
@ -58,6 +58,13 @@ class UserFactory(factory.django.DjangoModelFactory):
|
||||||
model = User
|
model = User
|
||||||
|
|
||||||
|
|
||||||
|
class TeamFactory(factory.django.DjangoModelFactory):
|
||||||
|
name = "Awesome Team"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Team
|
||||||
|
|
||||||
|
|
||||||
class MapFactory(factory.django.DjangoModelFactory):
|
class MapFactory(factory.django.DjangoModelFactory):
|
||||||
name = "test map"
|
name = "test map"
|
||||||
slug = "test-map"
|
slug = "test-map"
|
||||||
|
|
|
@ -11,6 +11,7 @@ from .base import (
|
||||||
DataLayerFactory,
|
DataLayerFactory,
|
||||||
LicenceFactory,
|
LicenceFactory,
|
||||||
MapFactory,
|
MapFactory,
|
||||||
|
TeamFactory,
|
||||||
TileLayerFactory,
|
TileLayerFactory,
|
||||||
UserFactory,
|
UserFactory,
|
||||||
)
|
)
|
||||||
|
@ -29,6 +30,11 @@ def pytest_runtest_teardown():
|
||||||
cache.clear()
|
cache.clear()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def team():
|
||||||
|
return TeamFactory()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def user():
|
def user():
|
||||||
return UserFactory(password="123123")
|
return UserFactory(password="123123")
|
||||||
|
|
|
@ -25,3 +25,23 @@ def test_caption(live_server, page, map):
|
||||||
panel.locator(".datalayer-legend .off").get_by_text(non_loaded.name)
|
panel.locator(".datalayer-legend .off").get_by_text(non_loaded.name)
|
||||||
).to_be_visible()
|
).to_be_visible()
|
||||||
expect(panel.locator(".datalayer-legend").get_by_text(hidden.name)).to_be_hidden()
|
expect(panel.locator(".datalayer-legend").get_by_text(hidden.name)).to_be_hidden()
|
||||||
|
|
||||||
|
|
||||||
|
def test_caption_should_display_owner_as_author(live_server, page, map):
|
||||||
|
map.settings["properties"]["onLoadPanel"] = "caption"
|
||||||
|
map.save()
|
||||||
|
page.goto(f"{live_server.url}{map.get_absolute_url()}")
|
||||||
|
panel = page.locator(".panel.left.on")
|
||||||
|
expect(panel).to_be_visible()
|
||||||
|
expect(panel.get_by_text("By Gabriel")).to_be_visible()
|
||||||
|
|
||||||
|
|
||||||
|
def test_caption_should_display_team_as_author(live_server, page, map, team):
|
||||||
|
map.settings["properties"]["onLoadPanel"] = "caption"
|
||||||
|
map.team = team
|
||||||
|
map.save()
|
||||||
|
page.goto(f"{live_server.url}{map.get_absolute_url()}")
|
||||||
|
panel = page.locator(".panel.left.on")
|
||||||
|
expect(panel).to_be_visible()
|
||||||
|
expect(panel.get_by_text("By Gabriel")).to_be_hidden()
|
||||||
|
expect(panel.get_by_text("By Awesome Team")).to_be_visible()
|
||||||
|
|
|
@ -81,7 +81,7 @@ def test_owner_permissions_form(map, datalayer, live_server, login):
|
||||||
|
|
||||||
|
|
||||||
def test_map_update_with_editor(map, live_server, login, user):
|
def test_map_update_with_editor(map, live_server, login, user):
|
||||||
map.edit_status = Map.EDITORS
|
map.edit_status = Map.COLLABORATORS
|
||||||
map.editors.add(user)
|
map.editors.add(user)
|
||||||
map.save()
|
map.save()
|
||||||
page = login(user)
|
page = login(user)
|
||||||
|
@ -104,7 +104,7 @@ def test_map_update_with_editor(map, live_server, login, user):
|
||||||
|
|
||||||
|
|
||||||
def test_permissions_form_with_editor(map, datalayer, live_server, login, user):
|
def test_permissions_form_with_editor(map, datalayer, live_server, login, user):
|
||||||
map.edit_status = Map.EDITORS
|
map.edit_status = Map.COLLABORATORS
|
||||||
map.editors.add(user)
|
map.editors.add(user)
|
||||||
map.save()
|
map.save()
|
||||||
page = login(user)
|
page = login(user)
|
||||||
|
@ -141,7 +141,7 @@ def test_owner_has_delete_map_button(map, live_server, login):
|
||||||
|
|
||||||
|
|
||||||
def test_editor_do_not_have_delete_map_button(map, live_server, login, user):
|
def test_editor_do_not_have_delete_map_button(map, live_server, login, user):
|
||||||
map.edit_status = Map.EDITORS
|
map.edit_status = Map.COLLABORATORS
|
||||||
map.editors.add(user)
|
map.editors.add(user)
|
||||||
map.save()
|
map.save()
|
||||||
page = login(user)
|
page = login(user)
|
||||||
|
@ -241,3 +241,19 @@ def test_can_delete_datalayer(live_server, map, login, datalayer):
|
||||||
expect(markers).to_have_count(0)
|
expect(markers).to_have_count(0)
|
||||||
# FIXME does not work, resolve to 1 element, even if this command is empty:
|
# FIXME does not work, resolve to 1 element, even if this command is empty:
|
||||||
expect(layers).to_have_count(0)
|
expect(layers).to_have_count(0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_set_team(map, live_server, login, team):
|
||||||
|
map.owner.teams.add(team)
|
||||||
|
map.owner.save()
|
||||||
|
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()
|
||||||
|
page.locator("select[name=team]").select_option(str(team.pk))
|
||||||
|
save = page.get_by_role("button", name="Save")
|
||||||
|
expect(save).to_be_visible()
|
||||||
|
with page.expect_response(re.compile(r".*/update/permissions/.*")):
|
||||||
|
save.click()
|
||||||
|
modified = Map.objects.get(pk=map.pk)
|
||||||
|
assert modified.team == team
|
||||||
|
|
47
umap/tests/integration/test_team.py
Normal file
47
umap/tests/integration/test_team.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from umap.models import Team
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_add_user_to_team(live_server, map, user, team, login):
|
||||||
|
map.owner.teams.add(team)
|
||||||
|
map.owner.save()
|
||||||
|
assert Team.objects.count() == 1
|
||||||
|
page = login(map.owner)
|
||||||
|
with page.expect_navigation():
|
||||||
|
page.get_by_role("link", name="My teams").click()
|
||||||
|
with page.expect_navigation():
|
||||||
|
page.get_by_role("link", name="Edit").click()
|
||||||
|
page.get_by_placeholder("Add user").click()
|
||||||
|
with page.expect_response(re.compile(r".*/agnocomplete/.*")):
|
||||||
|
page.get_by_placeholder("Add user").press_sequentially("joe")
|
||||||
|
page.get_by_text("Joe").click()
|
||||||
|
page.get_by_role("button", name="Save").click()
|
||||||
|
assert Team.objects.count() == 1
|
||||||
|
modified = Team.objects.first()
|
||||||
|
assert user in modified.users.all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_remove_user_from_team(live_server, map, user, user2, team, login):
|
||||||
|
map.owner.teams.add(team)
|
||||||
|
map.owner.save()
|
||||||
|
user.teams.add(team)
|
||||||
|
user.save()
|
||||||
|
user2.teams.add(team)
|
||||||
|
user2.save()
|
||||||
|
assert Team.objects.count() == 1
|
||||||
|
page = login(map.owner)
|
||||||
|
with page.expect_navigation():
|
||||||
|
page.get_by_role("link", name="My teams").click()
|
||||||
|
with page.expect_navigation():
|
||||||
|
page.get_by_role("link", name="Edit").click()
|
||||||
|
page.locator("li").filter(has_text="Averell").locator(".close").click()
|
||||||
|
page.get_by_role("button", name="Save").click()
|
||||||
|
assert Team.objects.count() == 1
|
||||||
|
modified = Team.objects.first()
|
||||||
|
assert user in modified.users.all()
|
||||||
|
assert user2 not in modified.users.all()
|
|
@ -101,22 +101,33 @@ def test_should_remove_old_versions_on_save(map, settings):
|
||||||
|
|
||||||
|
|
||||||
def test_anonymous_cannot_edit_in_editors_mode(datalayer):
|
def test_anonymous_cannot_edit_in_editors_mode(datalayer):
|
||||||
datalayer.edit_status = DataLayer.EDITORS
|
datalayer.edit_status = DataLayer.COLLABORATORS
|
||||||
datalayer.save()
|
datalayer.save()
|
||||||
assert not datalayer.can_edit()
|
assert not datalayer.can_edit()
|
||||||
|
|
||||||
|
|
||||||
def test_owner_can_edit_in_editors_mode(datalayer, user):
|
def test_owner_can_edit_in_editors_mode(datalayer, user):
|
||||||
datalayer.edit_status = DataLayer.EDITORS
|
datalayer.edit_status = DataLayer.COLLABORATORS
|
||||||
datalayer.save()
|
datalayer.save()
|
||||||
assert datalayer.can_edit(datalayer.map.owner)
|
assert datalayer.can_edit(datalayer.map.owner)
|
||||||
|
|
||||||
|
|
||||||
def test_editor_can_edit_in_editors_mode(datalayer, user):
|
def test_editor_can_edit_in_collaborators_mode(datalayer, user):
|
||||||
map = datalayer.map
|
map = datalayer.map
|
||||||
map.editors.add(user)
|
map.editors.add(user)
|
||||||
map.save()
|
map.save()
|
||||||
datalayer.edit_status = DataLayer.EDITORS
|
datalayer.edit_status = DataLayer.COLLABORATORS
|
||||||
|
datalayer.save()
|
||||||
|
assert datalayer.can_edit(user)
|
||||||
|
|
||||||
|
|
||||||
|
def test_team_members_can_edit_in_collaborators_mode(datalayer, user, team):
|
||||||
|
user.teams.add(team)
|
||||||
|
user.save()
|
||||||
|
map = datalayer.map
|
||||||
|
map.team = team
|
||||||
|
map.save()
|
||||||
|
datalayer.edit_status = DataLayer.COLLABORATORS
|
||||||
datalayer.save()
|
datalayer.save()
|
||||||
assert datalayer.can_edit(user)
|
assert datalayer.can_edit(user)
|
||||||
|
|
||||||
|
@ -170,6 +181,20 @@ def test_editors_cannot_edit_in_inherit_mode_and_map_in_owner_mode(datalayer, us
|
||||||
assert not datalayer.can_edit(user)
|
assert not datalayer.can_edit(user)
|
||||||
|
|
||||||
|
|
||||||
|
def test_team_members_cannot_edit_in_inherit_mode_and_map_in_owner_mode(
|
||||||
|
datalayer, user, team
|
||||||
|
):
|
||||||
|
datalayer.edit_status = DataLayer.INHERIT
|
||||||
|
datalayer.save()
|
||||||
|
user.teams.add(team)
|
||||||
|
team.save()
|
||||||
|
map = datalayer.map
|
||||||
|
map.team = team
|
||||||
|
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):
|
def test_anonymous_cannot_edit_in_inherit_mode_and_map_in_owner_mode(datalayer):
|
||||||
datalayer.edit_status = DataLayer.INHERIT
|
datalayer.edit_status = DataLayer.INHERIT
|
||||||
datalayer.save()
|
datalayer.save()
|
||||||
|
@ -183,7 +208,7 @@ def test_owner_can_edit_in_inherit_mode_and_map_in_editors_mode(datalayer):
|
||||||
datalayer.edit_status = DataLayer.INHERIT
|
datalayer.edit_status = DataLayer.INHERIT
|
||||||
datalayer.save()
|
datalayer.save()
|
||||||
map = datalayer.map
|
map = datalayer.map
|
||||||
map.edit_status = Map.EDITORS
|
map.edit_status = Map.COLLABORATORS
|
||||||
map.save()
|
map.save()
|
||||||
assert datalayer.can_edit(map.owner)
|
assert datalayer.can_edit(map.owner)
|
||||||
|
|
||||||
|
@ -193,7 +218,7 @@ def test_editors_can_edit_in_inherit_mode_and_map_in_editors_mode(datalayer, use
|
||||||
datalayer.save()
|
datalayer.save()
|
||||||
map = datalayer.map
|
map = datalayer.map
|
||||||
map.editors.add(user)
|
map.editors.add(user)
|
||||||
map.edit_status = Map.EDITORS
|
map.edit_status = Map.COLLABORATORS
|
||||||
map.save()
|
map.save()
|
||||||
assert datalayer.can_edit(user)
|
assert datalayer.can_edit(user)
|
||||||
|
|
||||||
|
@ -202,7 +227,7 @@ def test_anonymous_cannot_edit_in_inherit_mode_and_map_in_editors_mode(datalayer
|
||||||
datalayer.edit_status = DataLayer.INHERIT
|
datalayer.edit_status = DataLayer.INHERIT
|
||||||
datalayer.save()
|
datalayer.save()
|
||||||
map = datalayer.map
|
map = datalayer.map
|
||||||
map.edit_status = Map.EDITORS
|
map.edit_status = Map.COLLABORATORS
|
||||||
map.save()
|
map.save()
|
||||||
assert not datalayer.can_edit()
|
assert not datalayer.can_edit()
|
||||||
|
|
||||||
|
|
|
@ -379,7 +379,7 @@ def test_owner_can_edit_in_owner_mode(datalayer, client, map, post_data):
|
||||||
|
|
||||||
def test_editor_can_edit_in_editors_mode(datalayer, client, map, post_data):
|
def test_editor_can_edit_in_editors_mode(datalayer, client, map, post_data):
|
||||||
client.login(username=map.owner.username, password="123123")
|
client.login(username=map.owner.username, password="123123")
|
||||||
datalayer.edit_status = DataLayer.EDITORS
|
datalayer.edit_status = DataLayer.COLLABORATORS
|
||||||
datalayer.save()
|
datalayer.save()
|
||||||
url = reverse("datalayer_update", args=(map.pk, datalayer.pk))
|
url = reverse("datalayer_update", args=(map.pk, datalayer.pk))
|
||||||
name = "new name"
|
name = "new name"
|
||||||
|
|
|
@ -43,13 +43,31 @@ def test_editors_cannot_edit_if_status_owner(map, user):
|
||||||
assert not map.can_edit(user)
|
assert not map.can_edit(user)
|
||||||
|
|
||||||
|
|
||||||
def test_editors_can_edit_if_status_editors(map, user):
|
def test_editors_can_edit_if_status_collaborators(map, user):
|
||||||
map.edit_status = map.EDITORS
|
map.edit_status = map.COLLABORATORS
|
||||||
map.editors.add(user)
|
map.editors.add(user)
|
||||||
map.save()
|
map.save()
|
||||||
assert map.can_edit(user)
|
assert map.can_edit(user)
|
||||||
|
|
||||||
|
|
||||||
|
def test_team_members_cannot_edit_if_status_owner(map, user, team):
|
||||||
|
user.teams.add(team)
|
||||||
|
user.save()
|
||||||
|
map.edit_status = map.OWNER
|
||||||
|
map.team = team
|
||||||
|
map.save()
|
||||||
|
assert not map.can_edit(user)
|
||||||
|
|
||||||
|
|
||||||
|
def test_team_members_can_edit_if_status_collaborators(map, user, team):
|
||||||
|
user.teams.add(team)
|
||||||
|
user.save()
|
||||||
|
map.edit_status = map.COLLABORATORS
|
||||||
|
map.team = team
|
||||||
|
map.save()
|
||||||
|
assert map.can_edit(user)
|
||||||
|
|
||||||
|
|
||||||
def test_logged_in_user_should_be_allowed_for_anonymous_map_with_anonymous_edit_status(
|
def test_logged_in_user_should_be_allowed_for_anonymous_map_with_anonymous_edit_status(
|
||||||
map, user, rf
|
map, user, rf
|
||||||
): # noqa
|
): # noqa
|
||||||
|
@ -87,6 +105,14 @@ def test_clone_should_keep_editors(map, user):
|
||||||
assert user in clone.editors.all()
|
assert user in clone.editors.all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_clone_should_keep_team(map, user, team):
|
||||||
|
map.team = team
|
||||||
|
map.save()
|
||||||
|
clone = map.clone()
|
||||||
|
assert map.pk != clone.pk
|
||||||
|
assert clone.team == team
|
||||||
|
|
||||||
|
|
||||||
def test_clone_should_update_owner_if_passed(map, user):
|
def test_clone_should_update_owner_if_passed(map, user):
|
||||||
clone = map.clone(owner=user)
|
clone = map.clone(owner=user)
|
||||||
assert map.pk != clone.pk
|
assert map.pk != clone.pk
|
||||||
|
@ -119,9 +145,9 @@ def test_publicmanager_should_get_only_public_maps(map, user, licence):
|
||||||
def test_can_change_default_edit_status(user, settings):
|
def test_can_change_default_edit_status(user, settings):
|
||||||
map = MapFactory(owner=user)
|
map = MapFactory(owner=user)
|
||||||
assert map.edit_status == Map.OWNER
|
assert map.edit_status == Map.OWNER
|
||||||
settings.UMAP_DEFAULT_EDIT_STATUS = Map.EDITORS
|
settings.UMAP_DEFAULT_EDIT_STATUS = Map.COLLABORATORS
|
||||||
map = MapFactory(owner=user)
|
map = MapFactory(owner=user)
|
||||||
assert map.edit_status == Map.EDITORS
|
assert map.edit_status == Map.COLLABORATORS
|
||||||
|
|
||||||
|
|
||||||
def test_can_change_default_share_status(user, settings):
|
def test_can_change_default_share_status(user, settings):
|
||||||
|
|
|
@ -210,7 +210,7 @@ def test_user_not_allowed_should_not_clone_map(client, map, user, settings):
|
||||||
|
|
||||||
def test_clone_should_set_cloner_as_owner(client, map, user):
|
def test_clone_should_set_cloner_as_owner(client, map, user):
|
||||||
url = reverse("map_clone", kwargs={"map_id": map.pk})
|
url = reverse("map_clone", kwargs={"map_id": map.pk})
|
||||||
map.edit_status = map.EDITORS
|
map.edit_status = map.COLLABORATORS
|
||||||
map.editors.add(user)
|
map.editors.add(user)
|
||||||
map.save()
|
map.save()
|
||||||
client.login(username=user.username, password="123123")
|
client.login(username=user.username, password="123123")
|
||||||
|
@ -330,7 +330,7 @@ def test_only_owner_can_delete(client, map, user):
|
||||||
|
|
||||||
def test_map_editors_do_not_see_owner_change_input(client, map, user):
|
def test_map_editors_do_not_see_owner_change_input(client, map, user):
|
||||||
map.editors.add(user)
|
map.editors.add(user)
|
||||||
map.edit_status = map.EDITORS
|
map.edit_status = map.COLLABORATORS
|
||||||
map.save()
|
map.save()
|
||||||
url = reverse("map_update_permissions", kwargs={"map_id": map.pk})
|
url = reverse("map_update_permissions", kwargs={"map_id": map.pk})
|
||||||
client.login(username=user.username, password="123123")
|
client.login(username=user.username, password="123123")
|
||||||
|
|
131
umap/tests/test_team_views.py
Normal file
131
umap/tests/test_team_views.py
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
import pytest
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from umap.models import Team
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_see_team_maps(client, map, team):
|
||||||
|
map.team = team
|
||||||
|
map.save()
|
||||||
|
url = reverse("team_maps", args=(team.pk,))
|
||||||
|
response = client.get(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert map.name in response.content.decode()
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_can_see_their_teams(client, team, user):
|
||||||
|
user.teams.add(team)
|
||||||
|
user.save()
|
||||||
|
url = reverse("user_teams")
|
||||||
|
client.login(username=user.username, password="123123")
|
||||||
|
response = client.get(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert team.name in response.content.decode()
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_create_a_team(client, user):
|
||||||
|
assert not Team.objects.count()
|
||||||
|
url = reverse("team_new")
|
||||||
|
client.login(username=user.username, password="123123")
|
||||||
|
response = client.post(url, {"name": "my new team", "members": [user.pk]})
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response["Location"] == "/en/me/teams"
|
||||||
|
assert Team.objects.count() == 1
|
||||||
|
team = Team.objects.first()
|
||||||
|
assert team.name == "my new team"
|
||||||
|
assert team in user.teams.all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_edit_a_team_name(client, user, team):
|
||||||
|
user.teams.add(team)
|
||||||
|
user.save()
|
||||||
|
assert Team.objects.count() == 1
|
||||||
|
url = reverse("team_update", args=(team.pk,))
|
||||||
|
client.login(username=user.username, password="123123")
|
||||||
|
response = client.post(url, {"name": "my new team", "members": [user.pk]})
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response["Location"] == "/en/me/teams"
|
||||||
|
assert Team.objects.count() == 1
|
||||||
|
modified = Team.objects.first()
|
||||||
|
assert modified.name == "my new team"
|
||||||
|
assert modified in user.teams.all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_add_user_to_team(client, user, user2, team):
|
||||||
|
user.teams.add(team)
|
||||||
|
user.save()
|
||||||
|
assert Team.objects.count() == 1
|
||||||
|
url = reverse("team_update", args=(team.pk,))
|
||||||
|
client.login(username=user.username, password="123123")
|
||||||
|
response = client.post(url, {"name": team.name, "members": [user.pk, user2.pk]})
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response["Location"] == "/en/me/teams"
|
||||||
|
assert Team.objects.count() == 1
|
||||||
|
modified = Team.objects.first()
|
||||||
|
assert user in modified.users.all()
|
||||||
|
assert user2 in modified.users.all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_remove_user_from_team(client, user, user2, team):
|
||||||
|
user.teams.add(team)
|
||||||
|
user.save()
|
||||||
|
user2.teams.add(team)
|
||||||
|
user2.save()
|
||||||
|
assert Team.objects.count() == 1
|
||||||
|
url = reverse("team_update", args=(team.pk,))
|
||||||
|
client.login(username=user.username, password="123123")
|
||||||
|
response = client.post(url, {"name": team.name, "members": [user.pk]})
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response["Location"] == "/en/me/teams"
|
||||||
|
assert Team.objects.count() == 1
|
||||||
|
modified = Team.objects.first()
|
||||||
|
assert user in modified.users.all()
|
||||||
|
assert user2 not in modified.users.all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_cannot_edit_a_team_if_not_member(client, user, user2, team):
|
||||||
|
user.teams.add(team)
|
||||||
|
user.save()
|
||||||
|
assert Team.objects.count() == 1
|
||||||
|
url = reverse("team_update", args=(team.pk,))
|
||||||
|
client.login(username=user2.username, password="456456")
|
||||||
|
response = client.post(url, {"name": "my new team", "members": [user.pk]})
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_delete_a_team(client, user, team):
|
||||||
|
user.teams.add(team)
|
||||||
|
user.save()
|
||||||
|
assert Team.objects.count() == 1
|
||||||
|
url = reverse("team_delete", args=(team.pk,))
|
||||||
|
client.login(username=user.username, password="123123")
|
||||||
|
response = client.post(url)
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response["Location"] == "/en/me/teams"
|
||||||
|
assert Team.objects.count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_cannot_delete_a_team_if_not_member(client, user, user2, team):
|
||||||
|
user.teams.add(team)
|
||||||
|
user.save()
|
||||||
|
assert Team.objects.count() == 1
|
||||||
|
url = reverse("team_delete", args=(team.pk,))
|
||||||
|
client.login(username=user2.username, password="456456")
|
||||||
|
response = client.post(url)
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert Team.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_cannot_delete_a_team_if_more_than_one_member(client, user, user2, team):
|
||||||
|
user.teams.add(team)
|
||||||
|
user.save()
|
||||||
|
user2.teams.add(team)
|
||||||
|
user2.save()
|
||||||
|
assert Team.objects.count() == 1
|
||||||
|
url = reverse("team_delete", args=(team.pk,))
|
||||||
|
client.login(username=user.username, password="123123")
|
||||||
|
response = client.post(url)
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert Team.objects.count() == 1
|
|
@ -288,6 +288,20 @@ def test_user_dashboard_display_user_maps(client, map):
|
||||||
assert "Owner only" in body
|
assert "Owner only" in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_user_dashboard_display_user_team_maps(client, map, team, user):
|
||||||
|
user.teams.add(team)
|
||||||
|
user.save()
|
||||||
|
map.team = team
|
||||||
|
map.save()
|
||||||
|
client.login(username=user.username, password="123123")
|
||||||
|
response = client.get(reverse("user_dashboard"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
body = response.content.decode()
|
||||||
|
assert map.name in body
|
||||||
|
assert map.get_absolute_url() in body
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_user_dashboard_display_user_maps_distinct(client, map):
|
def test_user_dashboard_display_user_maps_distinct(client, map):
|
||||||
# cf https://github.com/umap-project/umap/issues/1325
|
# cf https://github.com/umap-project/umap/issues/1325
|
||||||
|
@ -474,7 +488,7 @@ def test_websocket_token_returns_a_valid_token_when_authorized(client, user, map
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_websocket_token_is_generated_for_editors(client, user, user2, map):
|
def test_websocket_token_is_generated_for_editors(client, user, user2, map):
|
||||||
map.edit_status = Map.EDITORS
|
map.edit_status = Map.COLLABORATORS
|
||||||
map.editors.add(user2)
|
map.editors.add(user2)
|
||||||
map.save()
|
map.save()
|
||||||
|
|
||||||
|
|
36
umap/urls.py
36
umap/urls.py
|
@ -16,6 +16,7 @@ from .decorators import (
|
||||||
can_edit_map,
|
can_edit_map,
|
||||||
can_view_map,
|
can_view_map,
|
||||||
login_required_if_not_anonymous_allowed,
|
login_required_if_not_anonymous_allowed,
|
||||||
|
team_members_only,
|
||||||
)
|
)
|
||||||
from .utils import decorated_patterns
|
from .utils import decorated_patterns
|
||||||
|
|
||||||
|
@ -24,6 +25,10 @@ admin.autodiscover()
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
re_path(r"^admin/", admin.site.urls),
|
re_path(r"^admin/", admin.site.urls),
|
||||||
re_path("", include("social_django.urls", namespace="social")),
|
re_path("", include("social_django.urls", namespace="social")),
|
||||||
|
re_path(
|
||||||
|
r"^agnocomplete/",
|
||||||
|
include(("agnocomplete.urls", "agnocomplete"), namespace="agnocomplete"),
|
||||||
|
),
|
||||||
re_path(r"^m/(?P<pk>\d+)/$", views.MapShortUrl.as_view(), name="map_short_url"),
|
re_path(r"^m/(?P<pk>\d+)/$", views.MapShortUrl.as_view(), name="map_short_url"),
|
||||||
re_path(r"^ajax-proxy/$", cache_page(180)(views.ajax_proxy), name="ajax-proxy"),
|
re_path(r"^ajax-proxy/$", cache_page(180)(views.ajax_proxy), name="ajax-proxy"),
|
||||||
re_path(
|
re_path(
|
||||||
|
@ -39,7 +44,6 @@ urlpatterns = [
|
||||||
name="password_change_done",
|
name="password_change_done",
|
||||||
),
|
),
|
||||||
re_path(r"^i18n/", include("django.conf.urls.i18n")),
|
re_path(r"^i18n/", include("django.conf.urls.i18n")),
|
||||||
re_path(r"^agnocomplete/", include("agnocomplete.urls")),
|
|
||||||
re_path(r"^map/oembed/", views.MapOEmbed.as_view(), name="map_oembed"),
|
re_path(r"^map/oembed/", views.MapOEmbed.as_view(), name="map_oembed"),
|
||||||
re_path(
|
re_path(
|
||||||
r"^map/(?P<map_id>\d+)/download/",
|
r"^map/(?P<map_id>\d+)/download/",
|
||||||
|
@ -96,12 +100,12 @@ i18n_urls += decorated_patterns(
|
||||||
)
|
)
|
||||||
i18n_urls += decorated_patterns(
|
i18n_urls += decorated_patterns(
|
||||||
[ensure_csrf_cookie],
|
[ensure_csrf_cookie],
|
||||||
re_path(r"^map/$", views.MapPreview.as_view(), name="map_preview"),
|
path("map/", views.MapPreview.as_view(), name="map_preview"),
|
||||||
re_path(r"^map/new/$", views.MapNew.as_view(), name="map_new"),
|
path("map/new/", views.MapNew.as_view(), name="map_new"),
|
||||||
)
|
)
|
||||||
i18n_urls += decorated_patterns(
|
i18n_urls += decorated_patterns(
|
||||||
[login_required_if_not_anonymous_allowed, never_cache],
|
[login_required_if_not_anonymous_allowed, never_cache],
|
||||||
re_path(r"^map/create/$", views.MapCreate.as_view(), name="map_create"),
|
path("map/create/", views.MapCreate.as_view(), name="map_create"),
|
||||||
)
|
)
|
||||||
i18n_urls += decorated_patterns(
|
i18n_urls += decorated_patterns(
|
||||||
[login_required],
|
[login_required],
|
||||||
|
@ -110,9 +114,16 @@ i18n_urls += decorated_patterns(
|
||||||
views.ToggleMapStarStatus.as_view(),
|
views.ToggleMapStarStatus.as_view(),
|
||||||
name="map_star",
|
name="map_star",
|
||||||
),
|
),
|
||||||
re_path(r"^me$", views.user_dashboard, name="user_dashboard"),
|
path("me", views.user_dashboard, name="user_dashboard"),
|
||||||
re_path(r"^me/profile$", views.user_profile, name="user_profile"),
|
path("me/profile", views.user_profile, name="user_profile"),
|
||||||
re_path(r"^me/download$", views.user_download, name="user_download"),
|
path("me/download", views.user_download, name="user_download"),
|
||||||
|
path("me/teams", views.UserTeams.as_view(), name="user_teams"),
|
||||||
|
path("team/create/", views.TeamNew.as_view(), name="team_new"),
|
||||||
|
)
|
||||||
|
i18n_urls += decorated_patterns(
|
||||||
|
[login_required, team_members_only],
|
||||||
|
path("team/<int:pk>/edit/", views.TeamUpdate.as_view(), name="team_update"),
|
||||||
|
path("team/<int:pk>/delete/", views.TeamDelete.as_view(), name="team_delete"),
|
||||||
)
|
)
|
||||||
map_urls = [
|
map_urls = [
|
||||||
re_path(
|
re_path(
|
||||||
|
@ -179,14 +190,13 @@ datalayer_urls = [
|
||||||
i18n_urls += decorated_patterns([can_edit_map, never_cache], *map_urls)
|
i18n_urls += decorated_patterns([can_edit_map, never_cache], *map_urls)
|
||||||
i18n_urls += decorated_patterns([never_cache], *datalayer_urls)
|
i18n_urls += decorated_patterns([never_cache], *datalayer_urls)
|
||||||
urlpatterns += i18n_patterns(
|
urlpatterns += i18n_patterns(
|
||||||
re_path(r"^$", views.home, name="home"),
|
path("", views.home, name="home"),
|
||||||
re_path(
|
path("showcase/", cache_page(24 * 60 * 60)(views.showcase), name="maps_showcase"),
|
||||||
r"^showcase/$", cache_page(24 * 60 * 60)(views.showcase), name="maps_showcase"
|
path("search/", views.search, name="search"),
|
||||||
),
|
path("about/", views.about, name="about"),
|
||||||
re_path(r"^search/$", views.search, name="search"),
|
|
||||||
re_path(r"^about/$", views.about, name="about"),
|
|
||||||
re_path(r"^user/(?P<identifier>.+)/stars/$", views.user_stars, name="user_stars"),
|
re_path(r"^user/(?P<identifier>.+)/stars/$", views.user_stars, name="user_stars"),
|
||||||
re_path(r"^user/(?P<identifier>.+)/$", views.user_maps, name="user_maps"),
|
re_path(r"^user/(?P<identifier>.+)/$", views.user_maps, name="user_maps"),
|
||||||
|
path("team/<int:pk>/", views.TeamMaps.as_view(), name="team_maps"),
|
||||||
re_path(r"", include(i18n_urls)),
|
re_path(r"", include(i18n_urls)),
|
||||||
)
|
)
|
||||||
urlpatterns += (
|
urlpatterns += (
|
||||||
|
|
105
umap/views.py
105
umap/views.py
|
@ -62,10 +62,11 @@ from .forms import (
|
||||||
FlatErrorList,
|
FlatErrorList,
|
||||||
MapSettingsForm,
|
MapSettingsForm,
|
||||||
SendLinkForm,
|
SendLinkForm,
|
||||||
|
TeamForm,
|
||||||
UpdateMapPermissionsForm,
|
UpdateMapPermissionsForm,
|
||||||
UserProfileForm,
|
UserProfileForm,
|
||||||
)
|
)
|
||||||
from .models import DataLayer, Licence, Map, Pictogram, Star, TileLayer
|
from .models import DataLayer, Licence, Map, Pictogram, Star, Team, TileLayer
|
||||||
from .utils import (
|
from .utils import (
|
||||||
ConflictError,
|
ConflictError,
|
||||||
_urls_for_js,
|
_urls_for_js,
|
||||||
|
@ -188,6 +189,70 @@ class About(Home):
|
||||||
about = About.as_view()
|
about = About.as_view()
|
||||||
|
|
||||||
|
|
||||||
|
class TeamNew(CreateView):
|
||||||
|
model = Team
|
||||||
|
fields = ["name", "description"]
|
||||||
|
success_url = reverse_lazy("user_teams")
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
response = super().form_valid(form)
|
||||||
|
self.request.user.teams.add(self.object)
|
||||||
|
self.request.user.save()
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class TeamUpdate(UpdateView):
|
||||||
|
model = Team
|
||||||
|
form_class = TeamForm
|
||||||
|
success_url = reverse_lazy("user_teams")
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
initial = super().get_initial()
|
||||||
|
initial["members"] = self.object.users.all()
|
||||||
|
return initial
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
actual = self.object.users.all()
|
||||||
|
wanted = form.cleaned_data["members"]
|
||||||
|
for user in wanted:
|
||||||
|
if user not in actual:
|
||||||
|
user.teams.add(self.object)
|
||||||
|
user.save()
|
||||||
|
for user in actual:
|
||||||
|
if user not in wanted:
|
||||||
|
user.teams.remove(self.object)
|
||||||
|
user.save()
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class TeamDelete(DeleteView):
|
||||||
|
model = Team
|
||||||
|
success_url = reverse_lazy("user_teams")
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
if self.object.users.count() > 1:
|
||||||
|
return HttpResponseBadRequest(
|
||||||
|
_("Cannot delete a team with more than one member")
|
||||||
|
)
|
||||||
|
messages.info(
|
||||||
|
self.request,
|
||||||
|
_("Team “%(name)s” has been deleted") % {"name": self.object.name},
|
||||||
|
)
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class UserTeams(DetailView):
|
||||||
|
model = User
|
||||||
|
template_name = "umap/user_teams.html"
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return self.get_queryset().get(pk=self.request.user.pk)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
kwargs.update({"teams": self.object.teams.all()})
|
||||||
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class UserProfile(UpdateView):
|
class UserProfile(UpdateView):
|
||||||
model = User
|
model = User
|
||||||
form_class = UserProfileForm
|
form_class = UserProfileForm
|
||||||
|
@ -247,6 +312,21 @@ class UserStars(UserMaps):
|
||||||
user_stars = UserStars.as_view()
|
user_stars = UserStars.as_view()
|
||||||
|
|
||||||
|
|
||||||
|
class TeamMaps(PaginatorMixin, DetailView):
|
||||||
|
model = Team
|
||||||
|
list_template_name = "umap/map_list.html"
|
||||||
|
context_object_name = "current_team"
|
||||||
|
|
||||||
|
def get_maps(self):
|
||||||
|
return Map.public.filter(team=self.object).order_by("-modified_at")
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
kwargs.update(
|
||||||
|
{"maps": self.paginate(self.get_maps(), settings.UMAP_MAPS_PER_PAGE)}
|
||||||
|
)
|
||||||
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class SearchMixin:
|
class SearchMixin:
|
||||||
def get_search_queryset(self, **kwargs):
|
def get_search_queryset(self, **kwargs):
|
||||||
q = self.request.GET.get("q")
|
q = self.request.GET.get("q")
|
||||||
|
@ -293,7 +373,12 @@ class UserDashboard(PaginatorMixin, DetailView, SearchMixin):
|
||||||
|
|
||||||
def get_maps(self):
|
def get_maps(self):
|
||||||
qs = self.get_search_queryset() or Map.objects.all()
|
qs = self.get_search_queryset() or Map.objects.all()
|
||||||
qs = qs.filter(owner=self.object).union(qs.filter(editors=self.object))
|
teams = self.object.teams.all()
|
||||||
|
qs = (
|
||||||
|
qs.filter(owner=self.object)
|
||||||
|
.union(qs.filter(editors=self.object))
|
||||||
|
.union(qs.filter(team__in=teams))
|
||||||
|
)
|
||||||
return qs.order_by("-modified_at")
|
return qs.order_by("-modified_at")
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
@ -459,14 +544,16 @@ def simple_json_response(**kwargs):
|
||||||
class SessionMixin:
|
class SessionMixin:
|
||||||
def get_user_data(self):
|
def get_user_data(self):
|
||||||
data = {}
|
data = {}
|
||||||
|
user = self.request.user
|
||||||
if hasattr(self, "object"):
|
if hasattr(self, "object"):
|
||||||
data["is_owner"] = self.object.is_owner(self.request.user, self.request)
|
data["is_owner"] = self.object.is_owner(user, self.request)
|
||||||
if self.request.user.is_anonymous:
|
if user.is_anonymous:
|
||||||
return data
|
return data
|
||||||
return {
|
return {
|
||||||
"id": self.request.user.pk,
|
"id": user.pk,
|
||||||
"name": str(self.request.user),
|
"name": str(self.request.user),
|
||||||
"url": reverse("user_dashboard"),
|
"url": reverse("user_dashboard"),
|
||||||
|
"teams": [team.get_metadata() for team in user.teams.all()],
|
||||||
**data,
|
**data,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -605,6 +692,8 @@ class PermissionsMixin:
|
||||||
{"id": editor.pk, "name": str(editor)}
|
{"id": editor.pk, "name": str(editor)}
|
||||||
for editor in self.object.editors.all()
|
for editor in self.object.editors.all()
|
||||||
]
|
]
|
||||||
|
if self.object.team:
|
||||||
|
permissions["team"] = self.object.team.get_metadata()
|
||||||
if not self.object.owner and self.object.is_anonymous_owner(self.request):
|
if not self.object.owner and self.object.is_anonymous_owner(self.request):
|
||||||
permissions["anonymous_edit_url"] = self.object.get_anonymous_edit_url()
|
permissions["anonymous_edit_url"] = self.object.get_anonymous_edit_url()
|
||||||
return permissions
|
return permissions
|
||||||
|
@ -669,6 +758,12 @@ class MapView(MapDetailMixin, PermissionsMixin, DetailView):
|
||||||
map_settings["properties"] = {}
|
map_settings["properties"] = {}
|
||||||
map_settings["properties"]["name"] = self.object.name
|
map_settings["properties"]["name"] = self.object.name
|
||||||
map_settings["properties"]["permissions"] = self.get_permissions()
|
map_settings["properties"]["permissions"] = self.get_permissions()
|
||||||
|
author = self.object.get_author()
|
||||||
|
if author:
|
||||||
|
map_settings["properties"]["author"] = {
|
||||||
|
"name": str(author),
|
||||||
|
"url": author.get_url(),
|
||||||
|
}
|
||||||
return map_settings
|
return map_settings
|
||||||
|
|
||||||
def is_starred(self):
|
def is_starred(self):
|
||||||
|
|
Loading…
Reference in a new issue