diff --git a/umap/decorators.py b/umap/decorators.py index f0187cbc..42b08d5f 100644 --- a/umap/decorators.py +++ b/umap/decorators.py @@ -1,12 +1,11 @@ from functools import wraps from django.conf import settings -from django.contrib.auth.models import Group from django.http import HttpResponseForbidden from django.shortcuts import get_object_or_404 from django.urls import reverse_lazy -from .models import Map +from .models import Map, Team from .views import simple_json_response LOGIN_URL = getattr(settings, "LOGIN_URL", "login") @@ -63,11 +62,11 @@ def can_view_map(view_func): return wrapper -def group_members_only(view_func): +def team_members_only(view_func): @wraps(view_func) def wrapper(request, *args, **kwargs): - group = get_object_or_404(Group, pk=kwargs["pk"]) - if group not in request.user.groups.all(): + 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) diff --git a/umap/forms.py b/umap/forms.py index 15f39514..0ec29ccb 100644 --- a/umap/forms.py +++ b/umap/forms.py @@ -1,13 +1,12 @@ from django import forms from django.conf import settings from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.contrib.gis.geos import Point from django.forms.utils import ErrorList from django.template.defaultfilters import slugify from django.utils.translation import gettext_lazy as _ -from .models import DataLayer, Map +from .models import DataLayer, Map, Team DEFAULT_LATITUDE = ( settings.LEAFLET_LATITUDE if hasattr(settings, "LEAFLET_LATITUDE") else 51 @@ -37,7 +36,7 @@ class SendLinkForm(forms.Form): class UpdateMapPermissionsForm(forms.ModelForm): class Meta: model = Map - fields = ("edit_status", "editors", "share_status", "owner", "group") + fields = ("edit_status", "editors", "share_status", "owner", "team") class AnonymousMapPermissionsForm(forms.ModelForm): @@ -113,25 +112,25 @@ class UserProfileForm(forms.ModelForm): fields = ("username", "first_name", "last_name") -class GroupMembersField(forms.ModelMultipleChoiceField): +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 group + # already existing members of the team iterator.queryset = choices self.choices = iterator -class GroupForm(forms.ModelForm): +class TeamForm(forms.ModelForm): class Meta: - model = Group - fields = ["name", "members"] + model = Team + fields = ["name", "description", "logo_url", "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 = GroupMembersField(queryset=User.objects.all()) + members = TeamMembersField(queryset=User.objects.all()) diff --git a/umap/migrations/0022_map_group_alter_datalayer_edit_status_and_more.py b/umap/migrations/0022_add_team.py similarity index 56% rename from umap/migrations/0022_map_group_alter_datalayer_edit_status_and_more.py rename to umap/migrations/0022_add_team.py index 511a8678..8163c96b 100644 --- a/umap/migrations/0022_map_group_alter_datalayer_edit_status_and_more.py +++ b/umap/migrations/0022_add_team.py @@ -1,27 +1,62 @@ # 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 = [ - ("auth", "0012_alter_user_first_name_max_length"), - ("umap", "0021_remove_map_description"), - ] + 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"), + ), + ( + "logo_url", + models.URLField( + help_text="URL to an image.", + verbose_name="Logo URL", + blank=True, + null=True, + ), + ), + ( + "users", + models.ManyToManyField( + related_name="teams", to=settings.AUTH_USER_MODEL + ), + ), + ], + ), migrations.AddField( model_name="map", - name="group", + name="team", field=models.ForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, - to="auth.group", - verbose_name="group", + to="umap.team", + verbose_name="team", ), ), migrations.AlterField( diff --git a/umap/models.py b/umap/models.py index d4fbae30..f0c786dc 100644 --- a/umap/models.py +++ b/umap/models.py @@ -5,7 +5,7 @@ import time import uuid from django.conf import settings -from django.contrib.auth.models import Group, User +from django.contrib.auth.models import User from django.contrib.gis.db import models from django.core.files.base import File from django.core.signing import Signer @@ -36,19 +36,9 @@ def get_user_stars_url(self): return reverse("user_stars", kwargs={"identifier": identifier}) -def get_group_url(self): - return reverse("group_maps", kwargs={"pk": self.pk}) - - -def get_group_metadata(self): - return {"id": self.pk, "name": self.name, "url": self.get_url()} - - User.add_to_class("__str__", display_name) User.add_to_class("get_url", get_user_url) User.add_to_class("get_stars_url", get_user_stars_url) -Group.add_to_class("get_url", get_group_url) -Group.add_to_class("get_metadata", get_group_metadata) def get_default_share_status(): @@ -59,6 +49,32 @@ def get_default_edit_status(): 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")) + logo_url = models.URLField( + verbose_name=_("Logo URL"), + help_text=_("URL to an image."), + null=True, + blank=True, + ) + 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): name = models.CharField(max_length=200, verbose_name=_("name")) @@ -190,8 +206,8 @@ class Map(NamedModel): editors = models.ManyToManyField( settings.AUTH_USER_MODEL, blank=True, verbose_name=_("editors") ) - group = models.ForeignKey( - "auth.Group", + team = models.ForeignKey( + Team, blank=True, null=True, verbose_name=_("team"), @@ -269,7 +285,7 @@ class Map(NamedModel): return settings.SITE_URL + path def get_author(self): - return self.group or self.owner + return self.team or self.owner def is_owner(self, user=None, request=None): if user and self.owner == user: @@ -301,7 +317,7 @@ class Map(NamedModel): In owner mode: - only owner by default (OWNER) - - any editor or group member if mode is COLLABORATORS + - any editor or team member if mode is COLLABORATORS - anyone otherwise (ANONYMOUS) In anonymous owner mode: - only owner (has ownership cookie) by default (OWNER) @@ -318,7 +334,7 @@ class Map(NamedModel): elif user == self.owner: can = True elif self.edit_status == self.COLLABORATORS: - if user in self.editors.all() or self.group in user.groups.all(): + if user in self.editors.all() or self.team in user.teams.all(): can = True return can @@ -337,7 +353,7 @@ class Map(NamedModel): can = not ( self.share_status == self.PRIVATE and request.user not in self.editors.all() - and self.group not in request.user.groups.all() + and self.team not in request.user.teams.all() ) return can @@ -563,7 +579,7 @@ class DataLayer(NamedModel): elif user is not None and user == self.map.owner: can = True elif user is not None and self.edit_status == self.COLLABORATORS: - if user in self.map.editors.all() or self.map.group in user.groups.all(): + if user in self.map.editors.all() or self.map.team in user.teams.all(): can = True return can diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index 82cba270..f233ff83 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -345,7 +345,7 @@ input + .error { input[type="file"] + .error { margin-top: 0; } -input:invalid { +input[value]:invalid { border-color: red; background-color: darkred; } diff --git a/umap/static/umap/content.css b/umap/static/umap/content.css index 6a9fe92e..f723fd35 100644 --- a/umap/static/umap/content.css +++ b/umap/static/umap/content.css @@ -133,6 +133,8 @@ h2.section { text-align: center; } h2.tabs { + display: flex; + justify-content: space-around; text-transform: uppercase; color: var(--color-darkBlue); text-align: start; @@ -144,7 +146,6 @@ h2.tabs a { text-decoration-thickness: 3px; text-decoration-skip-ink: none; text-underline-offset: 7px; - margin-inline-end: 2rem; display: inline-block; } h2.tabs a:not(.selected) { diff --git a/umap/static/umap/js/modules/permissions.js b/umap/static/umap/js/modules/permissions.js index 58c230a7..2dde4002 100644 --- a/umap/static/umap/js/modules/permissions.js +++ b/umap/static/umap/js/modules/permissions.js @@ -25,7 +25,7 @@ export class MapPermissions { this.options = Object.assign( { owner: null, - group: null, + team: null, editors: [], share_status: null, edit_status: null, @@ -97,13 +97,13 @@ export class MapPermissions { 'options.owner', { handler: 'ManageOwner', label: translate("Map's owner") }, ]) - if (this.map.options.user?.groups?.length) { + if (this.map.options.user?.teams?.length) { fields.push([ - 'options.group', + 'options.team', { - handler: 'ManageGroup', + handler: 'ManageTeam', label: translate('Attach map to a team'), - groups: this.map.options.user.groups, + teams: this.map.options.user.teams, }, ]) } @@ -161,7 +161,7 @@ export class MapPermissions { formData.append('edit_status', this.options.edit_status) if (this.isOwner()) { formData.append('owner', this.options.owner?.id) - formData.append('group', this.options.group?.id || '') + formData.append('team', this.options.team?.id || '') formData.append('share_status', this.options.share_status) } const [data, response, error] = await this.map.server.post( diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 15dc3311..8eb730eb 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -1086,10 +1086,10 @@ L.FormBuilder.ManageEditors = L.FormBuilder.Element.extend({ }, }) -L.FormBuilder.ManageGroup = L.FormBuilder.IntSelect.extend({ +L.FormBuilder.ManageTeam = L.FormBuilder.IntSelect.extend({ getOptions: function () { return [[null, L._('None')]].concat( - this.options.groups.map((group) => [group.id, group.name]) + this.options.teams.map((team) => [team.id, team.name]) ) }, toHTML: function () { @@ -1097,8 +1097,8 @@ L.FormBuilder.ManageGroup = L.FormBuilder.IntSelect.extend({ }, toJS: function () { const value = this.value() - for (const group of this.options.groups) { - if (group.id === value) return group + for (const team of this.options.teams) { + if (team.id === value) return team } }, }) diff --git a/umap/templates/auth/group_detail.html b/umap/templates/auth/group_detail.html deleted file mode 100644 index bd044b00..00000000 --- a/umap/templates/auth/group_detail.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "umap/content.html" %} - -{% load i18n %} - -{% block maincontent %} -