diff --git a/umap/decorators.py b/umap/decorators.py index fbe70429..f0187cbc 100644 --- a/umap/decorators.py +++ b/umap/decorators.py @@ -1,6 +1,7 @@ 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 @@ -60,3 +61,14 @@ def can_view_map(view_func): return view_func(request, *args, **kwargs) return wrapper + + +def group_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(): + return HttpResponseForbidden() + return view_func(request, *args, **kwargs) + + return wrapper diff --git a/umap/forms.py b/umap/forms.py index c7813a61..d90d7068 100644 --- a/umap/forms.py +++ b/umap/forms.py @@ -1,6 +1,7 @@ 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 @@ -110,3 +111,13 @@ class UserProfileForm(forms.ModelForm): class Meta: model = User fields = ("username", "first_name", "last_name") + + +class GroupForm(forms.ModelForm): + class Meta: + model = Group + fields = ["name", "members"] + + members = forms.ModelMultipleChoiceField( + queryset=User.objects.all(), widget=forms.CheckboxSelectMultiple + ) diff --git a/umap/templates/auth/group_confirm_delete.html b/umap/templates/auth/group_confirm_delete.html new file mode 100644 index 00000000..31283229 --- /dev/null +++ b/umap/templates/auth/group_confirm_delete.html @@ -0,0 +1,19 @@ +{% extends "umap/content.html" %} + +{% load i18n %} + +{% block maincontent %} + {% include "umap/dashboard_menu.html" with selected="groups" %} +
+
+
+ {% csrf_token %} +

+ Are you sure you want to delete "{{ object }}"? +

+ {{ form }} + +
+
+
+{% endblock maincontent %} diff --git a/umap/templates/auth/group_detail.html b/umap/templates/auth/group_detail.html new file mode 100644 index 00000000..bd044b00 --- /dev/null +++ b/umap/templates/auth/group_detail.html @@ -0,0 +1,22 @@ +{% 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 new file mode 100644 index 00000000..3709a8c8 --- /dev/null +++ b/umap/templates/auth/group_form.html @@ -0,0 +1,28 @@ +{% 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 group" %} + {% endif %} +
+
+{% endblock maincontent %} diff --git a/umap/templates/auth/user_form.html b/umap/templates/auth/user_form.html index bbcc5f7e..534d0f80 100644 --- a/umap/templates/auth/user_form.html +++ b/umap/templates/auth/user_form.html @@ -3,12 +3,7 @@ {% load i18n %} {% block maincontent %} -
-

- {% trans "My Maps" %} - {% trans "My Profile" %} -

-
+ {% include "umap/dashboard_menu.html" with selected="profile" %}
{% if form.non_field_errors %} diff --git a/umap/templates/umap/dashboard_menu.html b/umap/templates/umap/dashboard_menu.html new file mode 100644 index 00000000..519abe8f --- /dev/null +++ b/umap/templates/umap/dashboard_menu.html @@ -0,0 +1,15 @@ +{% load i18n %} + + diff --git a/umap/templates/umap/user_dashboard.html b/umap/templates/umap/user_dashboard.html index 9459ee66..48cbf408 100644 --- a/umap/templates/umap/user_dashboard.html +++ b/umap/templates/umap/user_dashboard.html @@ -7,13 +7,7 @@ {% endblock head_title %} {% block maincontent %} {% trans "Search my maps" as placeholder %} - + {% include "umap/dashboard_menu.html" with selected="maps" %}
diff --git a/umap/templates/umap/user_groups.html b/umap/templates/umap/user_groups.html new file mode 100644 index 00000000..6a0e5668 --- /dev/null +++ b/umap/templates/umap/user_groups.html @@ -0,0 +1,19 @@ +{% extends "umap/content.html" %} + +{% load i18n %} + +{% block maincontent %} + {% include "umap/dashboard_menu.html" with selected="groups" %} +
+
+ + {% trans "New team" %} +
+
+{% endblock %} diff --git a/umap/urls.py b/umap/urls.py index 2a39fe6b..02accb2d 100644 --- a/umap/urls.py +++ b/umap/urls.py @@ -15,6 +15,7 @@ from . import views from .decorators import ( can_edit_map, can_view_map, + group_members_only, login_required_if_not_anonymous_allowed, ) from .utils import decorated_patterns @@ -96,8 +97,8 @@ i18n_urls += decorated_patterns( ) i18n_urls += decorated_patterns( [ensure_csrf_cookie], - re_path(r"^map/$", views.MapPreview.as_view(), name="map_preview"), - re_path(r"^map/new/$", views.MapNew.as_view(), name="map_new"), + path("map/", views.MapPreview.as_view(), name="map_preview"), + path("map/new/", views.MapNew.as_view(), name="map_new"), ) i18n_urls += decorated_patterns( [login_required_if_not_anonymous_allowed, never_cache], @@ -110,9 +111,16 @@ i18n_urls += decorated_patterns( views.ToggleMapStarStatus.as_view(), name="map_star", ), - re_path(r"^me$", views.user_dashboard, name="user_dashboard"), - re_path(r"^me/profile$", views.user_profile, name="user_profile"), - re_path(r"^me/download$", views.user_download, name="user_download"), + 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"), +) +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"), ) map_urls = [ re_path( diff --git a/umap/views.py b/umap/views.py index b55c9020..0d305b90 100644 --- a/umap/views.py +++ b/umap/views.py @@ -61,6 +61,7 @@ from .forms import ( DataLayerForm, DataLayerPermissionsForm, FlatErrorList, + GroupForm, MapSettingsForm, SendLinkForm, UpdateMapPermissionsForm, @@ -189,6 +190,63 @@ class About(Home): about = About.as_view() +class GroupNew(CreateView): + model = Group + fields = ["name"] + success_url = reverse_lazy("user_groups") + + def form_valid(self, form): + response = super().form_valid(form) + self.request.user.groups.add(self.object) + self.request.user.save() + return response + + +class GroupUpdate(UpdateView): + model = Group + form_class = GroupForm + success_url = reverse_lazy("user_groups") + + def get_initial(self): + initial = super().get_initial() + initial["members"] = self.object.user_set.all() + return initial + + def form_valid(self, form): + for user in form.cleaned_data["members"]: + user.groups.add(self.object) + user.save() + return super().form_valid(form) + + +class GroupDelete(DeleteView): + model = Group + success_url = reverse_lazy("user_groups") + + def form_valid(self, form): + if self.object.user_set.count() > 1: + return HttpResponseBadRequest( + _("Cannot delete a group with more than one member") + ) + messages.info( + self.request, + _("Group ā€œ%(name)sā€ has been deleted") % {"name": self.object.name}, + ) + return super().form_valid(form) + + +class UserGroups(DetailView): + model = User + template_name = "umap/user_groups.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()}) + return super().get_context_data(**kwargs) + + class UserProfile(UpdateView): model = User form_class = UserProfileForm