From eb54ac7896c6b94ce64a0bb1954b12892b2ad949 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laura=20Kl=C3=BCnder?=
Date: Fri, 8 Dec 2017 21:31:53 +0100
Subject: [PATCH] manage access permissions
---
src/c3nav/control/forms.py | 134 +++++++++++++++++-
src/c3nav/control/templates/control/user.html | 25 +++-
src/c3nav/control/views.py | 24 +++-
.../0055_grant_access_permissions.py | 28 ++++
src/c3nav/mapdata/models/access.py | 6 +-
src/c3nav/site/static/site/css/c3nav.css | 2 +
6 files changed, 214 insertions(+), 5 deletions(-)
create mode 100644 src/c3nav/mapdata/migrations/0055_grant_access_permissions.py
diff --git a/src/c3nav/control/forms.py b/src/c3nav/control/forms.py
index 01c931a2..5f950aa0 100644
--- a/src/c3nav/control/forms.py
+++ b/src/c3nav/control/forms.py
@@ -1,9 +1,141 @@
-from django.forms import ModelForm
+import time
+import uuid
+from datetime import timedelta
+from itertools import chain
+
+from django.core.cache import cache
+from django.db import transaction
+from django.db.models import Q
+from django.forms import BooleanField, ChoiceField, Form, ModelForm
+from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ungettext_lazy
from c3nav.control.models import UserPermissions
+from c3nav.mapdata.models.access import AccessPermission, AccessRestriction
class UserPermissionsForm(ModelForm):
class Meta:
model = UserPermissions
exclude = ('user', )
+
+
+class AccessPermissionForm(Form):
+ def __init__(self, request, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.author = request.user
+
+ if not request.user_permissions.access_all:
+ self.author_access_permissions = {
+ pk: expire_date for pk, expire_date in self.author.accesspermissions.filter(
+ Q(can_grant=True) & (Q(expire_date__isnull=True) | Q(expire_date__lt=timezone.now()))
+ ).values_list('access_restriction_id', 'expire_date')
+ }
+ access_restrictions = AccessRestriction.objects.filter(
+ pk__in=self.author_access_permissions.keys()
+ )
+ else:
+ self.author_access_permissions = {}
+ access_restrictions = AccessRestriction.objects.all()
+
+ self.access_restrictions = {
+ access_restriction.pk: access_restriction
+ for access_restriction in access_restrictions
+ }
+
+ self.access_restriction_choices = {
+ 'all': self.access_restrictions.values(),
+ **{str(pk): (access_restriction, ) for pk, access_restriction in self.access_restrictions.items()}
+ }
+
+ choices = [('', _('choose permissions…')),
+ ('all', ungettext_lazy('everything possible (%d permission)',
+ 'everything possible (%d permissions)',
+ len(access_restrictions)) % len(access_restrictions))]
+
+ choices.append((_('Access Permissions'), tuple(
+ (str(pk), access_restriction.title)
+ for pk, access_restriction in self.access_restrictions.items()
+ )))
+
+ self.fields['access_restrictions'] = ChoiceField(label=_('Access Permission'),
+ choices=choices, required=True)
+
+ expire_choices = [
+ ('', _('never')),
+ ]
+ for minutes in range(15, 60, 15):
+ expire_choices.append(
+ (str(minutes), ungettext_lazy('in %d minute', 'in %d minutes', minutes) % minutes))
+
+ for hours in chain(range(1, 6), range(6, 24, 6)):
+ expire_choices.append(
+ (str(hours*60), ungettext_lazy('in %d hour', 'in %d hours', hours) % hours)
+ )
+ expire_choices.insert(
+ 5, (str(90), _('in 1½ hour'))
+ )
+ for days in range(1, 14):
+ expire_choices.append(
+ (str(days*24*60), ungettext_lazy('in %d day', 'in %d days', days) % days)
+ )
+
+ self.fields['expires'] = ChoiceField(label=_('expires'), required=False, initial='60',
+ choices=expire_choices)
+
+ if request.user_permissions.access_all:
+ self.fields['can_grant'] = BooleanField(label=_('can grant'), required=False)
+
+ def clean_access_restrictions(self):
+ data = self.cleaned_data['access_restrictions']
+ return self.access_restriction_choices[data]
+
+ def clean_expires(self):
+ data = self.cleaned_data['expires']
+ if data == '':
+ return None
+ return timezone.now()+timedelta(minutes=int(data))
+
+ def save(self, user):
+ self._save_code(self._create_code(), user)
+
+ def create_code(self, timeout=30):
+ code = uuid.uuid4()
+ cache.set('access:code:%s' % code, (self._create_code(), time.time()+timeout), timeout)
+
+ def save_code(self, code, user):
+ cache_key = 'access:code:%s' % code
+ with transaction.atomic():
+ AccessPermission.objects.select_for_update().first()
+ code, expires = cache.get(cache_key, (None, None))
+ if code is None or expires < time.time():
+ raise ValueError
+ self._save_code(code, user)
+ cache.delete(cache_key)
+
+ def _create_code(self):
+ restrictions = []
+ for restriction in self.cleaned_data['access_restrictions']:
+ expires = self.cleaned_data['expires']
+ author_expires = self.author_access_permissions.get(restriction.pk)
+ if author_expires is not None:
+ expires = author_expires if expires is None else min(expires, author_expires)
+ restrictions.append((restriction.pk, expires))
+ return (tuple(restrictions), self.author.pk, self.cleaned_data.get('can_grant', False))
+
+ @classmethod
+ def _save_code(cls, code, user):
+ restrictions, author_id, can_grant = code
+ print(code)
+ with transaction.atomic():
+ for pk, expire_date in restrictions:
+ obj, created = AccessPermission.objects.get_or_create(
+ user=user,
+ access_restriction_id=pk
+ )
+ obj.author_id = author_id
+ obj.expire_date = expire_date
+ obj.can_grant = can_grant
+ obj.save()
diff --git a/src/c3nav/control/templates/control/user.html b/src/c3nav/control/templates/control/user.html
index a311d055..6f9a8453 100644
--- a/src/c3nav/control/templates/control/user.html
+++ b/src/c3nav/control/templates/control/user.html
@@ -9,7 +9,7 @@
@@ -22,4 +22,27 @@
{% endfor %}
{% endif %}
+
+ {% trans 'Access Permissions' %}
+ {% if user.accesspermissions.all %}
+
+
+ {% trans 'Access Restriction' %} |
+ {% trans 'expires' %} |
+
+ {% for access_permission in user.accesspermissions.all %}
+
+ {{ access_permission.access_restriction.title }} |
+ {{ access_permission.expire_date }} |
+
+ {% endfor %}
+
+ {% else %}
+ {% trans 'none' %}
+ {% endif %}
+
{% endblock %}
diff --git a/src/c3nav/control/views.py b/src/c3nav/control/views.py
index 213fc28c..7868f857 100644
--- a/src/c3nav/control/views.py
+++ b/src/c3nav/control/views.py
@@ -5,11 +5,13 @@ from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.core.paginator import Paginator
+from django.db.models import Prefetch
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import ugettext_lazy as _
-from c3nav.control.forms import UserPermissionsForm
+from c3nav.control.forms import AccessPermissionForm, UserPermissionsForm
from c3nav.control.models import UserPermissions
+from c3nav.mapdata.models.access import AccessPermission
def control_panel_view(func):
@@ -48,7 +50,11 @@ def user_list(request):
@login_required
@control_panel_view
def user_detail(request, user):
- qs = User.objects.select_related('permissions').prefetch_related('accesspermissions')
+ qs = User.objects.select_related(
+ 'permissions',
+ ).prefetch_related(
+ Prefetch('accesspermissions', AccessPermission.objects.select_related('access_restriction'))
+ )
user = get_object_or_404(qs, pk=user)
ctx = {
@@ -79,4 +85,18 @@ def user_detail(request, user):
'user_permissions_form': form
})
+ # access permissions
+ if request.method == 'POST' and request.POST.get('submit_access_permissions'):
+ form = AccessPermissionForm(request=request, data=request.POST)
+ if form.is_valid():
+ form.save(user)
+ messages.success(request, _('Access permissions successfully updated.'))
+ return redirect(request.path_info)
+ else:
+ form = AccessPermissionForm(request=request)
+
+ ctx.update({
+ 'add_access_permission_form': form
+ })
+
return render(request, 'control/user.html', ctx)
diff --git a/src/c3nav/mapdata/migrations/0055_grant_access_permissions.py b/src/c3nav/mapdata/migrations/0055_grant_access_permissions.py
new file mode 100644
index 00000000..8bd822b3
--- /dev/null
+++ b/src/c3nav/mapdata/migrations/0055_grant_access_permissions.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.7 on 2017-12-08 18:23
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('mapdata', '0054_title_plural'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='accesspermission',
+ name='author',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='authored_access_permissions', to=settings.AUTH_USER_MODEL, verbose_name='Author'),
+ ),
+ migrations.AddField(
+ model_name='accesspermission',
+ name='can_grant',
+ field=models.BooleanField(default=False, verbose_name='can grant'),
+ ),
+ ]
diff --git a/src/c3nav/mapdata/models/access.py b/src/c3nav/mapdata/models/access.py
index c5b8b162..44d51fcc 100644
--- a/src/c3nav/mapdata/models/access.py
+++ b/src/c3nav/mapdata/models/access.py
@@ -15,7 +15,8 @@ class AccessRestriction(TitledMixin, models.Model):
"""
An access restriction
"""
- users = models.ManyToManyField(settings.AUTH_USER_MODEL, through='AccessPermission')
+ users = models.ManyToManyField(settings.AUTH_USER_MODEL, through='AccessPermission',
+ through_fields=('access_restriction', 'user'))
open = models.BooleanField(default=False, verbose_name=_('open'))
class Meta:
@@ -34,6 +35,9 @@ class AccessPermission(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
access_restriction = models.ForeignKey(AccessRestriction, on_delete=models.CASCADE)
expire_date = models.DateTimeField(null=True, verbose_name=_('expires'))
+ can_grant = models.BooleanField(default=False, verbose_name=_('can grant'))
+ author = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL,
+ related_name='authored_access_permissions', verbose_name=_('Author'))
class Meta:
verbose_name = _('Access Permission')
diff --git a/src/c3nav/site/static/site/css/c3nav.css b/src/c3nav/site/static/site/css/c3nav.css
index 4a6173b8..dcb6e66a 100644
--- a/src/c3nav/site/static/site/css/c3nav.css
+++ b/src/c3nav/site/static/site/css/c3nav.css
@@ -703,4 +703,6 @@ main.control h4 {
.user-permissions-form label {
font-weight: 400;
+ width: auto;
+ display: inline;
}