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 %} -
-

- {% blocktrans %}Browse {{ current_group }}'s maps{% endblocktrans %} -

-
-
-
- {% if maps %} - {% include "umap/map_list.html" %} - {% else %} -
- {% blocktrans %}{{ current_group }} has no public maps.{% endblocktrans %} -
- {% endif %} -
-
-{% endblock maincontent %} diff --git a/umap/templates/auth/group_form.html b/umap/templates/auth/group_form.html deleted file mode 100644 index 45af6d13..00000000 --- a/umap/templates/auth/group_form.html +++ /dev/null @@ -1,58 +0,0 @@ -{% extends "umap/content.html" %} - -{% load i18n %} - -{% block maincontent %} - {% include "umap/dashboard_menu.html" with selected="groups" %} -
-
- {% if form.non_field_errors %} - - {% endif %} -
- {% csrf_token %} - {{ form }} - -
- {% if group.user_set.count == 1 %} - {% trans "Delete this team" %} - {% endif %} -
-
- -{% endblock maincontent %} diff --git a/umap/templates/umap/dashboard_menu.html b/umap/templates/umap/dashboard_menu.html index 519abe8f..ac07420f 100644 --- a/umap/templates/umap/dashboard_menu.html +++ b/umap/templates/umap/dashboard_menu.html @@ -9,7 +9,7 @@ {% endif %} {% trans "My profile" %} - {% trans "My teams" %} + {% trans "My teams" %} diff --git a/umap/templates/auth/group_confirm_delete.html b/umap/templates/umap/team_confirm_delete.html similarity index 85% rename from umap/templates/auth/group_confirm_delete.html rename to umap/templates/umap/team_confirm_delete.html index 31283229..deceb4b1 100644 --- a/umap/templates/auth/group_confirm_delete.html +++ b/umap/templates/umap/team_confirm_delete.html @@ -3,7 +3,7 @@ {% load i18n %} {% block maincontent %} - {% include "umap/dashboard_menu.html" with selected="groups" %} + {% include "umap/dashboard_menu.html" with selected="teams" %}
diff --git a/umap/templates/umap/team_detail.html b/umap/templates/umap/team_detail.html new file mode 100644 index 00000000..e5112376 --- /dev/null +++ b/umap/templates/umap/team_detail.html @@ -0,0 +1,30 @@ +{% extends "umap/content.html" %} + +{% load i18n %} + +{% block maincontent %} +
+
+
+

+ {% blocktrans %}Browse {{ current_team }}'s maps{% endblocktrans %} +

+ {% if current_team.description %} +

{{ current_team.description }}

+ {% endif %} + {% if current_team.logo_url %} +

+ {% endif %} +
+
+
+ {% if maps %} + {% include "umap/map_list.html" %} + {% else %} +
+ {% blocktrans %}{{ current_team }} has no public maps.{% endblocktrans %} +
+ {% endif %} +
+
+{% endblock maincontent %} diff --git a/umap/templates/umap/team_form.html b/umap/templates/umap/team_form.html new file mode 100644 index 00000000..a8501ac4 --- /dev/null +++ b/umap/templates/umap/team_form.html @@ -0,0 +1,60 @@ +{% extends "umap/content.html" %} + +{% load i18n %} + +{% block maincontent %} + {% include "umap/dashboard_menu.html" with selected="teams" %} +
+
+ {% if form.non_field_errors %} +
    + {% for error in form.non_field_errors %} +
  • + {{ error }} +
  • + {% endfor %} +
+ {% endif %} + + {% csrf_token %} + {{ form }} + + + {% if team.users.count == 1 %} + {% trans "Delete this team" %} + {% endif %} +
+
+ +{% endblock maincontent %} diff --git a/umap/templates/umap/user_groups.html b/umap/templates/umap/user_groups.html deleted file mode 100644 index 1273d9ec..00000000 --- a/umap/templates/umap/user_groups.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "umap/content.html" %} - -{% load i18n %} - -{% block maincontent %} - {% include "umap/dashboard_menu.html" with selected="groups" %} -
-
- - {% trans "New team" %} -
-
-{% endblock maincontent %} diff --git a/umap/templates/umap/user_teams.html b/umap/templates/umap/user_teams.html new file mode 100644 index 00000000..d84296c3 --- /dev/null +++ b/umap/templates/umap/user_teams.html @@ -0,0 +1,19 @@ +{% extends "umap/content.html" %} + +{% load i18n %} + +{% block maincontent %} + {% include "umap/dashboard_menu.html" with selected="teams" %} +
+
+ + {% trans "New team" %} +
+
+{% endblock maincontent %} diff --git a/umap/tests/base.py b/umap/tests/base.py index 4d0ce144..ea5107a8 100644 --- a/umap/tests/base.py +++ b/umap/tests/base.py @@ -3,12 +3,11 @@ import json import factory from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.core.files.base import ContentFile from django.urls import reverse from umap.forms import DEFAULT_CENTER -from umap.models import DataLayer, Licence, Map, TileLayer +from umap.models import DataLayer, Licence, Map, TileLayer, Team User = get_user_model() @@ -59,11 +58,11 @@ class UserFactory(factory.django.DjangoModelFactory): model = User -class GroupFactory(factory.django.DjangoModelFactory): - name = "Awesome Group" +class TeamFactory(factory.django.DjangoModelFactory): + name = "Awesome Team" class Meta: - model = Group + model = Team class MapFactory(factory.django.DjangoModelFactory): diff --git a/umap/tests/conftest.py b/umap/tests/conftest.py index 3bee02e7..2f12753e 100644 --- a/umap/tests/conftest.py +++ b/umap/tests/conftest.py @@ -9,7 +9,7 @@ from umap.models import Map from .base import ( DataLayerFactory, - GroupFactory, + TeamFactory, LicenceFactory, MapFactory, TileLayerFactory, @@ -31,8 +31,8 @@ def pytest_runtest_teardown(): @pytest.fixture -def group(): - return GroupFactory() +def team(): + return TeamFactory() @pytest.fixture diff --git a/umap/tests/integration/test_caption.py b/umap/tests/integration/test_caption.py index e5f2b7eb..f9228fe5 100644 --- a/umap/tests/integration/test_caption.py +++ b/umap/tests/integration/test_caption.py @@ -36,12 +36,12 @@ def test_caption_should_display_owner_as_author(live_server, page, map): expect(panel.get_by_text("By Gabriel")).to_be_visible() -def test_caption_should_display_group_as_author(live_server, page, map, group): +def test_caption_should_display_team_as_author(live_server, page, map, team): map.settings["properties"]["onLoadPanel"] = "caption" - map.group = group + 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 Group")).to_be_visible() + expect(panel.get_by_text("By Awesome Team")).to_be_visible() diff --git a/umap/tests/integration/test_owned_map.py b/umap/tests/integration/test_owned_map.py index 19d02804..77518d69 100644 --- a/umap/tests/integration/test_owned_map.py +++ b/umap/tests/integration/test_owned_map.py @@ -243,17 +243,17 @@ def test_can_delete_datalayer(live_server, map, login, datalayer): expect(layers).to_have_count(0) -def test_can_set_group(map, live_server, login, group): - map.owner.groups.add(group) +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=group]").select_option(str(group.pk)) + 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.group == group + assert modified.team == team diff --git a/umap/tests/integration/test_group.py b/umap/tests/integration/test_team.py similarity index 58% rename from umap/tests/integration/test_group.py rename to umap/tests/integration/test_team.py index bd9a4746..0a3beea4 100644 --- a/umap/tests/integration/test_group.py +++ b/umap/tests/integration/test_team.py @@ -1,15 +1,16 @@ import re import pytest -from django.contrib.auth.models import Group + +from umap.models import Team pytestmark = pytest.mark.django_db -def test_can_add_user_to_group(live_server, map, user, group, login): - map.owner.groups.add(group) +def test_can_add_user_to_team(live_server, map, user, team, login): + map.owner.teams.add(team) map.owner.save() - assert Group.objects.count() == 1 + assert Team.objects.count() == 1 page = login(map.owner) with page.expect_navigation(): page.get_by_role("link", name="My teams").click() @@ -20,19 +21,19 @@ def test_can_add_user_to_group(live_server, map, user, group, login): page.get_by_placeholder("Add user").press_sequentially("joe") page.get_by_text("Joe").click() page.get_by_role("button", name="Save").click() - assert Group.objects.count() == 1 - modified = Group.objects.first() - assert user in modified.user_set.all() + assert Team.objects.count() == 1 + modified = Team.objects.first() + assert user in modified.users.all() -def test_can_remove_user_from_group(live_server, map, user, user2, group, login): - map.owner.groups.add(group) +def test_can_remove_user_from_team(live_server, map, user, user2, team, login): + map.owner.teams.add(team) map.owner.save() - user.groups.add(group) + user.teams.add(team) user.save() - user2.groups.add(group) + user2.teams.add(team) user2.save() - assert Group.objects.count() == 1 + assert Team.objects.count() == 1 page = login(map.owner) with page.expect_navigation(): page.get_by_role("link", name="My teams").click() @@ -40,7 +41,7 @@ def test_can_remove_user_from_group(live_server, map, user, user2, group, login) 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 Group.objects.count() == 1 - modified = Group.objects.first() - assert user in modified.user_set.all() - assert user2 not in modified.user_set.all() + assert Team.objects.count() == 1 + modified = Team.objects.first() + assert user in modified.users.all() + assert user2 not in modified.users.all() diff --git a/umap/tests/test_datalayer.py b/umap/tests/test_datalayer.py index c60ab3c0..99b0f5bb 100644 --- a/umap/tests/test_datalayer.py +++ b/umap/tests/test_datalayer.py @@ -121,11 +121,11 @@ def test_editor_can_edit_in_collaborators_mode(datalayer, user): assert datalayer.can_edit(user) -def test_group_members_can_edit_in_collaborators_mode(datalayer, user, group): - user.groups.add(group) +def test_team_members_can_edit_in_collaborators_mode(datalayer, user, team): + user.teams.add(team) user.save() map = datalayer.map - map.group = group + map.team = team map.save() datalayer.edit_status = DataLayer.COLLABORATORS datalayer.save() @@ -181,15 +181,15 @@ def test_editors_cannot_edit_in_inherit_mode_and_map_in_owner_mode(datalayer, us assert not datalayer.can_edit(user) -def test_group_members_cannot_edit_in_inherit_mode_and_map_in_owner_mode( - datalayer, user, group +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.groups.add(group) - group.save() + user.teams.add(team) + team.save() map = datalayer.map - map.group = group + map.team = team map.edit_status = Map.OWNER map.save() assert not datalayer.can_edit(user) diff --git a/umap/tests/test_group_views.py b/umap/tests/test_group_views.py deleted file mode 100644 index 0135c1bd..00000000 --- a/umap/tests/test_group_views.py +++ /dev/null @@ -1,130 +0,0 @@ -import pytest -from django.contrib.auth.models import Group -from django.urls import reverse - -pytestmark = pytest.mark.django_db - - -def test_can_see_group_maps(client, map, group): - map.group = group - map.save() - url = reverse("group_maps", args=(group.pk,)) - response = client.get(url) - assert response.status_code == 200 - assert map.name in response.content.decode() - - -def test_user_can_see_their_groups(client, group, user): - user.groups.add(group) - user.save() - url = reverse("user_groups") - client.login(username=user.username, password="123123") - response = client.get(url) - assert response.status_code == 200 - assert group.name in response.content.decode() - - -def test_can_create_a_group(client, user): - assert not Group.objects.count() - url = reverse("group_new") - client.login(username=user.username, password="123123") - response = client.post(url, {"name": "my new group", "members": [user.pk]}) - assert response.status_code == 302 - assert response["Location"] == "/en/me/groups" - assert Group.objects.count() == 1 - group = Group.objects.first() - assert group.name == "my new group" - assert group in user.groups.all() - - -def test_can_edit_a_group_name(client, user, group): - user.groups.add(group) - user.save() - assert Group.objects.count() == 1 - url = reverse("group_update", args=(group.pk,)) - client.login(username=user.username, password="123123") - response = client.post(url, {"name": "my new group", "members": [user.pk]}) - assert response.status_code == 302 - assert response["Location"] == "/en/me/groups" - assert Group.objects.count() == 1 - modified = Group.objects.first() - assert modified.name == "my new group" - assert modified in user.groups.all() - - -def test_can_add_user_to_group(client, user, user2, group): - user.groups.add(group) - user.save() - assert Group.objects.count() == 1 - url = reverse("group_update", args=(group.pk,)) - client.login(username=user.username, password="123123") - response = client.post(url, {"name": group.name, "members": [user.pk, user2.pk]}) - assert response.status_code == 302 - assert response["Location"] == "/en/me/groups" - assert Group.objects.count() == 1 - modified = Group.objects.first() - assert user in modified.user_set.all() - assert user2 in modified.user_set.all() - - -def test_can_remove_user_from_group(client, user, user2, group): - user.groups.add(group) - user.save() - user2.groups.add(group) - user2.save() - assert Group.objects.count() == 1 - url = reverse("group_update", args=(group.pk,)) - client.login(username=user.username, password="123123") - response = client.post(url, {"name": group.name, "members": [user.pk]}) - assert response.status_code == 302 - assert response["Location"] == "/en/me/groups" - assert Group.objects.count() == 1 - modified = Group.objects.first() - assert user in modified.user_set.all() - assert user2 not in modified.user_set.all() - - -def test_cannot_edit_a_group_if_not_member(client, user, user2, group): - user.groups.add(group) - user.save() - assert Group.objects.count() == 1 - url = reverse("group_update", args=(group.pk,)) - client.login(username=user2.username, password="123123") - response = client.post(url, {"name": "my new group", "members": [user.pk]}) - assert response.status_code == 403 - - -def test_can_delete_a_group(client, user, group): - user.groups.add(group) - user.save() - assert Group.objects.count() == 1 - url = reverse("group_delete", args=(group.pk,)) - client.login(username=user.username, password="123123") - response = client.post(url) - assert response.status_code == 302 - assert response["Location"] == "/en/me/groups" - assert Group.objects.count() == 0 - - -def test_cannot_delete_a_group_if_not_member(client, user, user2, group): - user.groups.add(group) - user.save() - assert Group.objects.count() == 1 - url = reverse("group_delete", args=(group.pk,)) - client.login(username=user2.username, password="123123") - response = client.post(url) - assert response.status_code == 403 - assert Group.objects.count() == 1 - - -def test_cannot_delete_a_group_if_more_than_one_member(client, user, user2, group): - user.groups.add(group) - user.save() - user2.groups.add(group) - user2.save() - assert Group.objects.count() == 1 - url = reverse("group_delete", args=(group.pk,)) - client.login(username=user.username, password="123123") - response = client.post(url) - assert response.status_code == 400 - assert Group.objects.count() == 1 diff --git a/umap/tests/test_map.py b/umap/tests/test_map.py index a0eb83e0..ff134e4c 100644 --- a/umap/tests/test_map.py +++ b/umap/tests/test_map.py @@ -50,20 +50,20 @@ def test_editors_can_edit_if_status_collaborators(map, user): assert map.can_edit(user) -def test_group_members_cannot_edit_if_status_owner(map, user, group): - user.groups.add(group) +def test_team_members_cannot_edit_if_status_owner(map, user, team): + user.teams.add(team) user.save() map.edit_status = map.OWNER - map.group = group + map.team = team map.save() assert not map.can_edit(user) -def test_group_members_can_edit_if_status_collaborators(map, user, group): - user.groups.add(group) +def test_team_members_can_edit_if_status_collaborators(map, user, team): + user.teams.add(team) user.save() map.edit_status = map.COLLABORATORS - map.group = group + map.team = team map.save() assert map.can_edit(user) @@ -105,12 +105,12 @@ def test_clone_should_keep_editors(map, user): assert user in clone.editors.all() -def test_clone_should_keep_group(map, user, group): - map.group = group +def test_clone_should_keep_team(map, user, team): + map.team = team map.save() clone = map.clone() assert map.pk != clone.pk - assert clone.group == group + assert clone.team == team def test_clone_should_update_owner_if_passed(map, user): diff --git a/umap/tests/test_team_views.py b/umap/tests/test_team_views.py new file mode 100644 index 00000000..7bce42b0 --- /dev/null +++ b/umap/tests/test_team_views.py @@ -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 diff --git a/umap/tests/test_views.py b/umap/tests/test_views.py index 1a0564a4..c6a6dbc3 100644 --- a/umap/tests/test_views.py +++ b/umap/tests/test_views.py @@ -289,10 +289,10 @@ def test_user_dashboard_display_user_maps(client, map): @pytest.mark.django_db -def test_user_dashboard_display_user_group_maps(client, map, group, user): - user.groups.add(group) +def test_user_dashboard_display_user_team_maps(client, map, team, user): + user.teams.add(team) user.save() - map.group = group + map.team = team map.save() client.login(username=user.username, password="123123") response = client.get(reverse("user_dashboard")) diff --git a/umap/urls.py b/umap/urls.py index ca75a7ab..f2d3821f 100644 --- a/umap/urls.py +++ b/umap/urls.py @@ -15,7 +15,7 @@ from . import views from .decorators import ( can_edit_map, can_view_map, - group_members_only, + team_members_only, login_required_if_not_anonymous_allowed, ) from .utils import decorated_patterns @@ -117,13 +117,13 @@ i18n_urls += decorated_patterns( path("me", views.user_dashboard, name="user_dashboard"), path("me/profile", views.user_profile, name="user_profile"), path("me/download", views.user_download, name="user_download"), - path("me/groups", views.UserGroups.as_view(), name="user_groups"), - path("group/create/", views.GroupNew.as_view(), name="group_new"), + 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, group_members_only], - path("group//edit/", views.GroupUpdate.as_view(), name="group_update"), - path("group//delete/", views.GroupDelete.as_view(), name="group_delete"), + [login_required, team_members_only], + path("team//edit/", views.TeamUpdate.as_view(), name="team_update"), + path("team//delete/", views.TeamDelete.as_view(), name="team_delete"), ) map_urls = [ re_path( @@ -196,7 +196,7 @@ urlpatterns += i18n_patterns( path("about/", views.about, name="about"), re_path(r"^user/(?P.+)/stars/$", views.user_stars, name="user_stars"), re_path(r"^user/(?P.+)/$", views.user_maps, name="user_maps"), - path("group//", views.group_maps, name="group_maps"), + path("team//", views.TeamMaps.as_view(), name="team_maps"), re_path(r"", include(i18n_urls)), ) urlpatterns += ( diff --git a/umap/views.py b/umap/views.py index 6eebf52e..21dc174a 100644 --- a/umap/views.py +++ b/umap/views.py @@ -18,7 +18,6 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.auth import logout as do_logout -from django.contrib.auth.models import Group from django.contrib.gis.measure import D from django.contrib.postgres.search import SearchQuery, SearchVector from django.contrib.staticfiles.storage import staticfiles_storage @@ -61,13 +60,13 @@ from .forms import ( DataLayerForm, DataLayerPermissionsForm, FlatErrorList, - GroupForm, + TeamForm, MapSettingsForm, SendLinkForm, UpdateMapPermissionsForm, UserProfileForm, ) -from .models import DataLayer, Licence, Map, Pictogram, Star, TileLayer +from .models import DataLayer, Licence, Map, Pictogram, Star, Team, TileLayer from .utils import ( ConflictError, _urls_for_js, @@ -190,67 +189,67 @@ class About(Home): about = About.as_view() -class GroupNew(CreateView): - model = Group - fields = ["name"] - success_url = reverse_lazy("user_groups") +class TeamNew(CreateView): + model = Team + fields = ["name", "description", "logo_url"] + success_url = reverse_lazy("user_teams") def form_valid(self, form): response = super().form_valid(form) - self.request.user.groups.add(self.object) + self.request.user.teams.add(self.object) self.request.user.save() return response -class GroupUpdate(UpdateView): - model = Group - form_class = GroupForm - success_url = reverse_lazy("user_groups") +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.user_set.all() + initial["members"] = self.object.users.all() return initial def form_valid(self, form): - actual = self.object.user_set.all() + actual = self.object.users.all() wanted = form.cleaned_data["members"] for user in wanted: if user not in actual: - user.groups.add(self.object) + user.teams.add(self.object) user.save() for user in actual: if user not in wanted: - user.groups.remove(self.object) + user.teams.remove(self.object) user.save() return super().form_valid(form) -class GroupDelete(DeleteView): - model = Group - success_url = reverse_lazy("user_groups") +class TeamDelete(DeleteView): + model = Team + success_url = reverse_lazy("user_teams") def form_valid(self, form): - if self.object.user_set.count() > 1: + if self.object.users.count() > 1: return HttpResponseBadRequest( - _("Cannot delete a group with more than one member") + _("Cannot delete a team with more than one member") ) messages.info( self.request, - _("Group ā€œ%(name)sā€ has been deleted") % {"name": self.object.name}, + _("Team ā€œ%(name)sā€ has been deleted") % {"name": self.object.name}, ) return super().form_valid(form) -class UserGroups(DetailView): +class UserTeams(DetailView): model = User - template_name = "umap/user_groups.html" + 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({"groups": self.object.groups.all()}) + kwargs.update({"teams": self.object.teams.all()}) return super().get_context_data(**kwargs) @@ -313,13 +312,13 @@ class UserStars(UserMaps): user_stars = UserStars.as_view() -class GroupMaps(PaginatorMixin, DetailView): - model = Group +class TeamMaps(PaginatorMixin, DetailView): + model = Team list_template_name = "umap/map_list.html" - context_object_name = "current_group" + context_object_name = "current_team" def get_maps(self): - return Map.public.filter(group=self.object).order_by("-modified_at") + return Map.public.filter(team=self.object).order_by("-modified_at") def get_context_data(self, **kwargs): kwargs.update( @@ -328,9 +327,6 @@ class GroupMaps(PaginatorMixin, DetailView): return super().get_context_data(**kwargs) -group_maps = GroupMaps.as_view() - - class SearchMixin: def get_search_queryset(self, **kwargs): q = self.request.GET.get("q") @@ -377,11 +373,11 @@ class UserDashboard(PaginatorMixin, DetailView, SearchMixin): def get_maps(self): qs = self.get_search_queryset() or Map.objects.all() - groups = self.object.groups.all() + teams = self.object.teams.all() qs = ( qs.filter(owner=self.object) .union(qs.filter(editors=self.object)) - .union(qs.filter(group__in=groups)) + .union(qs.filter(team__in=teams)) ) return qs.order_by("-modified_at") @@ -557,7 +553,7 @@ class SessionMixin: "id": user.pk, "name": str(self.request.user), "url": reverse("user_dashboard"), - "groups": [group.get_metadata() for group in user.groups.all()], + "teams": [team.get_metadata() for team in user.teams.all()], **data, } @@ -696,8 +692,8 @@ class PermissionsMixin: {"id": editor.pk, "name": str(editor)} for editor in self.object.editors.all() ] - if self.object.group: - permissions["group"] = self.object.group.get_metadata() + if self.object.team: + permissions["team"] = self.object.team.get_metadata() if not self.object.owner and self.object.is_anonymous_owner(self.request): permissions["anonymous_edit_url"] = self.object.get_anonymous_edit_url() return permissions