Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion umap/autocomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db.models.functions import Length

from .models import Team

@register
class AutocompleteUser(AgnocompleteModel):
Expand All @@ -22,3 +22,21 @@ def build_extra_filtered_queryset(self, queryset, **kwargs):
field_name = field_name[1:]
order_by.append(Length(field_name).asc())
return queryset.order_by(*order_by)

@register
class AutocompleteTeam(AgnocompleteModel):
model = Team
fields = settings.TEAM_AUTOCOMPLETE_FIELDS

def item(self, current_item):
data = super().item(current_item)
data["url"] = current_item.get_url()
return data

def build_extra_filtered_queryset(self, queryset, **kwargs):
order_by = []
for field_name in self.fields:
if not field_name[0].isalnum():
field_name = field_name[1:]
order_by.append(Length(field_name).asc())
return queryset.order_by(*order_by)
4 changes: 2 additions & 2 deletions umap/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class SendLinkForm(forms.Form):
class UpdateMapPermissionsForm(forms.ModelForm):
class Meta:
model = Map
fields = ("edit_status", "editors", "share_status", "owner", "team")
fields = ("edit_status", "editors", "share_status", "owner", "teams")


class AnonymousMapPermissionsForm(forms.ModelForm):
Expand All @@ -56,7 +56,7 @@ class Meta:
class DataLayerPermissionsForm(forms.ModelForm):
class Meta:
model = DataLayer
fields = ("edit_status",)
fields = ("edit_status","editors", "teams")


class AnonymousDataLayerPermissionsForm(forms.ModelForm):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 5.2.1 on 2025-05-17 08:13

from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('umap', '0027_map_tags'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.AddField(
model_name='datalayer',
name='editors',
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='editors'),
),
migrations.AlterField(
model_name='datalayer',
name='edit_status',
field=models.SmallIntegerField(choices=[(0, 'Inherit'), (1, 'Everyone'), (2, 'Editors and team only'), (4, 'Assigned Users and Owner'), (3, 'Owner only')], default=0, verbose_name='edit status'),
),
]
18 changes: 18 additions & 0 deletions umap/migrations/0029_alter_datalayer_edit_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.1 on 2025-05-17 16:46

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('umap', '0028_datalayer_editors_alter_datalayer_edit_status'),
]

operations = [
migrations.AlterField(
model_name='datalayer',
name='edit_status',
field=models.SmallIntegerField(choices=[(0, 'Inherit'), (1, 'Everyone'), (2, 'Editors and team only'), (4, 'Selected Users, Teams and Owner'), (3, 'Owner only')], default=0, verbose_name='edit status'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 5.2.1 on 2025-05-17 21:53

import umap.models
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('umap', '0029_alter_datalayer_edit_status'),
]

operations = [
migrations.RemoveField(
model_name='map',
name='team',
),
migrations.AddField(
model_name='datalayer',
name='teams',
field=models.ManyToManyField(blank=True, to='umap.team', verbose_name='teams'),
),
migrations.AddField(
model_name='map',
name='teams',
field=models.ManyToManyField(blank=True, to='umap.team', verbose_name='teams'),
),
migrations.AlterField(
model_name='datalayer',
name='edit_status',
field=models.SmallIntegerField(choices=[(0, 'Inherit'), (1, 'Everyone'), (2, 'Editors and teams only'), (4, 'Selected Users, Teams and Owner'), (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 teams 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=[(0, 'Draft (private)'), (1, 'Everyone (public)'), (2, 'Anyone with link'), (3, 'Editors and teams only'), (9, 'Blocked'), (99, 'Deleted')], default=umap.models.get_default_share_status, verbose_name='share status'),
),
]
64 changes: 48 additions & 16 deletions umap/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ class Map(NamedModel):
)
EDIT_STATUS = (
(ANONYMOUS, _("Everyone")),
(COLLABORATORS, _("Editors and team only")),
(COLLABORATORS, _("Editors and teams only")),
(OWNER, _("Owner only")),
)
ANONYMOUS_SHARE_STATUS = (
Expand All @@ -187,7 +187,7 @@ class Map(NamedModel):
)
SHARE_STATUS = ANONYMOUS_SHARE_STATUS + (
(OPEN, _("Anyone with link")),
(PRIVATE, _("Editors and team only")),
(PRIVATE, _("Editors and teams only")),
(BLOCKED, _("Blocked")),
(DELETED, _("Deleted")),
)
Expand Down Expand Up @@ -217,13 +217,10 @@ class Map(NamedModel):
editors = models.ManyToManyField(
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,
teams = models.ManyToManyField(
Team, blank=True, verbose_name=_("teams")
)

edit_status = models.SmallIntegerField(
choices=EDIT_STATUS,
default=get_default_edit_status,
Expand Down Expand Up @@ -314,7 +311,10 @@ def get_anonymous_edit_url(self):
return settings.SITE_URL + path

def get_author(self):
return self.team or self.owner
# check if te
if not self.teams.all():
return self.owner
return self.teams.all()[0]

def is_owner(self, request=None):
if not request:
Expand Down Expand Up @@ -373,7 +373,7 @@ def can_edit(self, request=None):
elif user == self.owner:
can = True
elif self.edit_status == self.COLLABORATORS:
if user in self.editors.all() or self.team in user.teams.all():
if user in self.editors.all() or any(e in self.teams.all() for e in user.teams.all()):
can = True
return can

Expand All @@ -393,7 +393,7 @@ def can_view(self, request):
can = not (
restricted
and request.user not in self.editors.all()
and self.team not in request.user.teams.all()
and not any(e in self.teams.all() for e in request.user.teams.all())
)
return can

Expand Down Expand Up @@ -471,6 +471,7 @@ class DataLayer(NamedModel):
ANONYMOUS = 1
COLLABORATORS = 2
OWNER = 3
USERSANDTEAMS = 4
DELETED = 99
SHARE_STATUS = (
(INHERIT, _("Inherit")),
Expand All @@ -479,7 +480,8 @@ class DataLayer(NamedModel):
EDIT_STATUS = (
(INHERIT, _("Inherit")),
(ANONYMOUS, _("Everyone")),
(COLLABORATORS, _("Editors and team only")),
(COLLABORATORS, _("Editors and teams only")),
(USERSANDTEAMS, _("Selected Users, Teams and Owner")),
(OWNER, _("Owner only")),
)
ANONYMOUS_EDIT_STATUS = (
Expand All @@ -490,6 +492,12 @@ class DataLayer(NamedModel):
uuid = models.UUIDField(unique=True, primary_key=True, editable=False)
old_id = models.IntegerField(null=True, blank=True)
map = models.ForeignKey(Map, on_delete=models.CASCADE)
editors = models.ManyToManyField(
settings.AUTH_USER_MODEL, blank=True, verbose_name=_("editors")
)
teams = models.ManyToManyField(
Team, blank=True, verbose_name=_("teams")
)
description = models.TextField(blank=True, null=True, verbose_name=_("description"))
geojson = models.FileField(
upload_to=upload_to, blank=True, null=True, storage=set_storage
Expand Down Expand Up @@ -552,11 +560,31 @@ def metadata(self, request=None):
metadata["old_id"] = self.old_id
metadata["id"] = self.pk
metadata["rank"] = self.rank
metadata["permissions"] = {"edit_status": self.edit_status}
metadata["editMode"] = "advanced" if self.can_edit(request) else "disabled"
metadata["editMode"] = "simple" if self.can_edit(request) else "disabled"
metadata["permissions"] = self.buildPermissions()
metadata["_referenceVersion"] = self.reference_version
return metadata

def buildPermissions(self):
permissions = {"edit_status": self.edit_status}
permissions["editors"] = [
{
"id": user.id,
"name": user.get_username(),
"url": user.get_url(),
}
for user in self.editors.all()
]
permissions["teams"] = [
{
"id": team.id,
"name": str(team),
"url": team.get_url(),
}
for team in self.teams.all()
]
return permissions

def clone(self, map_inst=None):
new = self.__class__.objects.get(pk=self.pk)
new._state.adding = True
Expand All @@ -582,7 +610,7 @@ def get_version(self, ref):

def get_version_path(self, ref):
return self.geojson.storage.get_version_path(ref, self)

def can_edit(self, request=None):
"""
Define if a user can edit or not the instance, according to his account
Expand All @@ -602,8 +630,12 @@ def can_edit(self, request=None):
elif user.is_authenticated and user == self.map.owner:
can = True
elif user.is_authenticated and self.edit_status == self.COLLABORATORS:
if user in self.map.editors.all() or self.map.team in user.teams.all():
if user in self.map.editors.all() or any(e in self.map.teams.all() for e in user.teams.all()):
can = True
elif user.is_authenticated and self.edit_status == self.USERSANDTEAMS:
if user in self.editors.all() or any(e in self.map.teams.all() for e in user.teams.all()):
can = True

return can

def move_to_trash(self):
Expand Down
2 changes: 2 additions & 0 deletions umap/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@
USER_AUTOCOMPLETE_FIELDS = ["^username"]
USER_URL_FIELD = "username"

TEAM_AUTOCOMPLETE_FIELDS = ['^name']

# =============================================================================
# Miscellaneous project settings
# =============================================================================
Expand Down
22 changes: 7 additions & 15 deletions umap/static/umap/js/modules/autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,23 +208,11 @@ export class BaseAutocomplete {
}

getLeft(el) {
let tmp = el.offsetLeft
el = el.offsetParent
while (el) {
tmp += el.offsetLeft
el = el.offsetParent
}
return tmp
return el.getBoundingClientRect().left
}

getTop(el) {
let tmp = el.offsetTop
el = el.offsetParent
while (el) {
tmp += el.offsetTop
el = el.offsetParent
}
return tmp
return el.getBoundingClientRect().top
}
}

Expand Down Expand Up @@ -277,7 +265,11 @@ export class BaseAjax extends BaseAutocomplete {

class BaseServerAjax extends BaseAjax {
setUrl() {
this.url = '/agnocomplete/AutocompleteUser/?q={q}'
if (this.options?.className === 'edit-teams') {
this.url = '/agnocomplete/AutocompleteTeam/?q={q}';
} else {
this.url = '/agnocomplete/AutocompleteUser/?q={q}';
}
}

initRequest() {
Expand Down
44 changes: 33 additions & 11 deletions umap/static/umap/js/modules/form/fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -1353,22 +1353,44 @@ Fields.ManageEditors = class extends BaseElement {
}
}
}
Fields.ManageTeams = class extends BaseElement {
build() {
super.build()
const options = {
className: 'edit-teams',
on_select: L.bind(this.onSelect, this),
on_unselect: L.bind(this.onUnselect, this),
placeholder: translate("Type team name"),
}
this.autocomplete = new AjaxAutocompleteMultiple(this.container, options)
this._values = this.toHTML() || []
if (this._values) {
for (let i = 0; i < this._values.length; i++)
this.autocomplete.displaySelected({
item: { value: this._values[i].id, label: this._values[i].name },
})
}
}

Fields.ManageTeam = class extends Fields.IntSelect {
getOptions() {
return [[null, translate('None')]].concat(
this.properties.teams.map((team) => [team.id, team.name])
)
value() {
return this._values
}

toHTML() {
return this.get()?.id
onSelect(choice) {
this._values.push({
id: choice.item.value,
name: choice.item.label,
url: choice.item.url,
})
this.set()
}

toJS() {
const value = this.value()
for (const team of this.properties.teams) {
if (team.id === value) return team
onUnselect(choice) {
const index = this._values.findIndex((item) => item.id === choice.item.value)
if (index !== -1) {
this._values.splice(index, 1)
this.set()
}
}
}

Loading