diff --git a/passbook/admin/templates/administration/base.html b/passbook/admin/templates/administration/base.html
index fadc37d70..d33970b78 100644
--- a/passbook/admin/templates/administration/base.html
+++ b/passbook/admin/templates/administration/base.html
@@ -5,35 +5,44 @@
{% block nav_secondary %}
{% endblock %}
diff --git a/passbook/admin/templates/administration/group/list.html b/passbook/admin/templates/administration/group/list.html
new file mode 100644
index 000000000..4a87521b0
--- /dev/null
+++ b/passbook/admin/templates/administration/group/list.html
@@ -0,0 +1,45 @@
+{% extends "administration/base.html" %}
+
+{% load i18n %}
+{% load utils %}
+
+{% block title %}
+{% title %}
+{% endblock %}
+
+{% block content %}
+
+
{% trans "Groups" %}
+
{% trans "Group users together and give them permissions based on the membership." %}
+
+
+ {% trans 'Create...' %}
+
+
+
+
+
+ {% trans 'Name' %} |
+ {% trans 'Parent' %} |
+ {% trans 'Members' %} |
+ |
+
+
+
+ {% for group in object_list %}
+
+ {{ group.name }} |
+ {{ group.parent }} |
+ {{ group.user_set.all|length }} |
+
+ {% trans 'Edit' %}
+ {% trans 'Delete' %}
+ |
+
+ {% endfor %}
+
+
+
+{% endblock %}
diff --git a/passbook/admin/templates/administration/groups/list.html b/passbook/admin/templates/administration/groups/list.html
deleted file mode 100644
index b8df11dc8..000000000
--- a/passbook/admin/templates/administration/groups/list.html
+++ /dev/null
@@ -1,83 +0,0 @@
-{% extends "administration/base.html" %}
-
-{% load i18n %}
-{% load static %}
-{% load utils %}
-
-{% block head %}
-{{ block.super }}
-
-{% endblock %}
-
-{% block scripts %}
-{{ block.super }}
-
-
-{% endblock %}
-
-{% block title %}
-{% title %}
-{% endblock %}
-
-{% block content %}
-
-
-
{% trans "Invitations" %}
-
- {% trans 'Create...' %}
-
-
-
-
-
- {% trans 'Expiry' %} |
- {% trans 'Link' %} |
- |
-
-
-
- {% for invitation in object_list %}
-
- {{ invitation.expires|default:"Never" }} |
-
- {{ invitation.link }}
- |
-
- {%
- trans 'Delete' %}
- |
-
- {% endfor %}
-
-
-
-{% endblock %}
diff --git a/passbook/admin/urls.py b/passbook/admin/urls.py
index e2d3a6236..deb4472bd 100644
--- a/passbook/admin/urls.py
+++ b/passbook/admin/urls.py
@@ -58,6 +58,11 @@ urlpatterns = [
users.UserDeleteView.as_view(), name='user-delete'),
path('users//reset/',
users.UserPasswordResetView.as_view(), name='user-password-reset'),
+ # Groups
+ path('group/', groups.GroupListView.as_view(), name='group'),
+ path('group/create/', groups.GroupCreateView.as_view(), name='group-create'),
+ path('group//update/', groups.GroupUpdateView.as_view(), name='group-update'),
+ path('group//delete/', groups.GroupDeleteView.as_view(), name='group-delete'),
# Audit Log
path('audit/', audit.AuditEntryListView.as_view(), name='audit-log'),
# Groups
diff --git a/passbook/admin/views/groups.py b/passbook/admin/views/groups.py
index 629ed4aa0..1e120669d 100644
--- a/passbook/admin/views/groups.py
+++ b/passbook/admin/views/groups.py
@@ -1,12 +1,57 @@
"""passbook Group administration"""
-from django.views.generic import ListView
+from django.contrib import messages
+from django.contrib.messages.views import SuccessMessageMixin
+from django.urls import reverse_lazy
+from django.utils.translation import ugettext as _
+from django.views.generic import CreateView, DeleteView, ListView, UpdateView
from passbook.admin.mixins import AdminRequiredMixin
+from passbook.core.forms.groups import GroupForm
from passbook.core.models import Group
class GroupListView(AdminRequiredMixin, ListView):
- """Show list of all invitations"""
+ """Show list of all groups"""
model = Group
- template_name = 'administration/groups/list.html'
+ ordering = 'name'
+ template_name = 'administration/group/list.html'
+
+
+class GroupCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
+ """Create new Group"""
+
+ form_class = GroupForm
+
+ template_name = 'generic/create.html'
+ success_url = reverse_lazy('passbook_admin:groups')
+ success_message = _('Successfully created Group')
+
+ def get_context_data(self, **kwargs):
+ kwargs['type'] = 'Group'
+ return super().get_context_data(**kwargs)
+
+
+class GroupUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
+ """Update group"""
+
+ model = Group
+ form_class = GroupForm
+
+ template_name = 'generic/update.html'
+ success_url = reverse_lazy('passbook_admin:groups')
+ success_message = _('Successfully updated Group')
+
+
+class GroupDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
+ """Delete group"""
+
+ model = Group
+
+ template_name = 'generic/delete.html'
+ success_url = reverse_lazy('passbook_admin:groups')
+ success_message = _('Successfully deleted Group')
+
+ def delete(self, request, *args, **kwargs):
+ messages.success(self.request, self.success_message)
+ return super().delete(request, *args, **kwargs)
diff --git a/passbook/core/forms/groups.py b/passbook/core/forms/groups.py
new file mode 100644
index 000000000..76b10b667
--- /dev/null
+++ b/passbook/core/forms/groups.py
@@ -0,0 +1,30 @@
+"""passbook Core Group forms"""
+from django import forms
+
+from passbook.core.models import Group, User
+
+
+class GroupForm(forms.ModelForm):
+ """Group Form"""
+
+ members = forms.ModelMultipleChoiceField(User.objects.all(), required=False)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ if self.instance.pk:
+ self.initial['members'] = self.instance.user_set.values_list('pk', flat=True)
+
+ def save(self, *args, **kwargs):
+ instance = super().save(*args, **kwargs)
+ if instance.pk:
+ instance.user_set.clear()
+ instance.user_set.add(*self.cleaned_data['users'])
+ return instance
+
+ class Meta:
+
+ model = Group
+ fields = ['name', 'parent', 'members', 'tags']
+ widgets = {
+ 'name': forms.TextInput(),
+ }
diff --git a/passbook/core/migrations/0017_auto_20190308_1417.py b/passbook/core/migrations/0017_auto_20190308_1417.py
new file mode 100644
index 000000000..d32d6fa48
--- /dev/null
+++ b/passbook/core/migrations/0017_auto_20190308_1417.py
@@ -0,0 +1,25 @@
+# Generated by Django 2.1.7 on 2019-03-08 14:17
+
+import django.contrib.postgres.fields.hstore
+from django.contrib.postgres.operations import HStoreExtension
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('passbook_core', '0016_auto_20190227_1355'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='group',
+ name='extra_data',
+ ),
+ HStoreExtension(),
+ migrations.AddField(
+ model_name='group',
+ name='tags',
+ field=django.contrib.postgres.fields.hstore.HStoreField(default=dict),
+ ),
+ ]
diff --git a/passbook/core/models.py b/passbook/core/models.py
index 2f6d526d8..25608b67a 100644
--- a/passbook/core/models.py
+++ b/passbook/core/models.py
@@ -8,7 +8,7 @@ from typing import Tuple, Union
from uuid import uuid4
from django.contrib.auth.models import AbstractUser
-from django.contrib.postgres.fields import ArrayField
+from django.contrib.postgres.fields import ArrayField, HStoreField
from django.db import models
from django.urls import reverse_lazy
from django.utils.timezone import now
@@ -31,7 +31,7 @@ class Group(UUIDModel):
name = models.CharField(_('name'), max_length=80)
parent = models.ForeignKey('Group', blank=True, null=True,
on_delete=models.SET_NULL, related_name='children')
- extra_data = models.TextField(blank=True)
+ tags = HStoreField(default=dict)
def __str__(self):
return "Group %s" % self.name
diff --git a/passbook/core/settings.py b/passbook/core/settings.py
index 580c54f6f..495dd9282 100644
--- a/passbook/core/settings.py
+++ b/passbook/core/settings.py
@@ -60,6 +60,7 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
+ 'django.contrib.postgres',
'rest_framework',
'drf_yasg',
'raven.contrib.django.raven_compat',