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" %}
+
+{% 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" %}
+
+{% 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 %}
-
+ {% 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" %}