add space access and ui for it in the control panel

This commit is contained in:
Laura Klünder 2018-12-07 23:43:40 +01:00
parent 1893b3fd40
commit 5e134e5b41
6 changed files with 219 additions and 11 deletions

View file

@ -9,13 +9,14 @@ from itertools import chain
import pytz
from django.contrib.auth.models import User
from django.db.models import Prefetch
from django.forms import ChoiceField, Form, ModelForm
from django.forms import ChoiceField, Form, ModelForm, Select
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.control.models import UserPermissions, UserSpaceAccess
from c3nav.mapdata.forms import I18nModelFormMixin
from c3nav.mapdata.models import Space
from c3nav.mapdata.models.access import (AccessPermission, AccessPermissionToken, AccessPermissionTokenItem,
AccessRestriction, AccessRestrictionGroup)
from c3nav.site.models import Announcement
@ -239,6 +240,19 @@ class AccessPermissionForm(Form):
return form.get_token(unique_key=unique_key)
class UserSpaceAccessForm(ModelForm):
class Meta:
model = UserSpaceAccess
fields = ('space', 'can_edit')
def __init__(self, *args, request=None, **kwargs):
super().__init__(*args, **kwargs)
self.fields['space'].label_from_instance = lambda obj: obj.title
self.fields['space'].queryset = Space.qs_for_request(request)
choices = [('0', _('no'))] * 6 + [('1', _('yes'))] + [('0', _('no'))] * 3
self.fields['can_edit'].widget = Select(choices=choices)
class SignedPermissionDataError(Exception):
pass

View file

@ -0,0 +1,40 @@
# Generated by Django 2.1.1 on 2018-12-07 22:07
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('mapdata', '0001_squashed_2018'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('control', '0005_editor_mapdata_permissions'),
]
operations = [
migrations.CreateModel(
name='UserSpaceAccess',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('can_edit', models.BooleanField(default=False, verbose_name='can edit')),
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='spaceaccesses', to='mapdata.Space')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='spaceaccesses', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'user space access',
'verbose_name_plural': 'user space accesses',
'default_related_name': 'spaceaccesses',
},
),
migrations.AddField(
model_name='userpermissions',
name='grant_space_access',
field=models.BooleanField(default=False, verbose_name='can grant space access'),
),
migrations.AlterUniqueTogether(
name='userspaceaccess',
unique_together={('user', 'space')},
),
]

View file

@ -1,9 +1,15 @@
from contextlib import contextmanager
from typing import Dict
from django.conf import settings
from django.contrib.auth.models import User
from django.core.cache import cache
from django.db import models, transaction
from django.utils.functional import lazy
from django.utils.translation import ugettext_lazy as _
from c3nav.mapdata.models import Space
class UserPermissions(models.Model):
"""
@ -21,6 +27,7 @@ class UserPermissions(models.Model):
grant_permissions = models.BooleanField(default=False, verbose_name=_('can grant control permissions'))
manage_announcements = models.BooleanField(default=False, verbose_name=_('manage announcements'))
grant_all_access = models.BooleanField(default=False, verbose_name=_('can grant access to everything'))
grant_space_access = models.BooleanField(default=False, verbose_name=_('can grant space access'))
api_secret = models.CharField(null=True, blank=True, max_length=64, verbose_name=_('API secret'))
class Meta:
@ -39,6 +46,13 @@ class UserPermissions(models.Model):
def get_cache_key(pk):
return 'control:permissions:%d' % pk
@classmethod
@contextmanager
def lock(cls, pk):
with transaction.atomic():
User.objects.filter(pk=pk).select_for_update()
yield
@classmethod
def get_for_user(cls, user, force=False) -> 'UserPermissions':
if not user.is_authenticated:
@ -53,16 +67,15 @@ class UserPermissions(models.Model):
break
if result:
return result
with transaction.atomic():
result = cls.objects.filter(user=user).select_for_update().first()
with cls.lock(user.pk):
result = cls.objects.filter(pk=user.pk).first()
if not result:
result = cls(user=user)
cache.set(cache_key, result, 900)
return result
def save(self, *args, **kwargs):
with transaction.atomic():
UserPermissions.objects.filter(user_id=self.user_id).select_for_update()
with self.lock(self.user_id):
super().save(*args, **kwargs)
cache_key = self.get_cache_key(self.pk)
cache.set(cache_key, self, 900)
@ -73,3 +86,48 @@ class UserPermissions(models.Model):
get_permissions_for_user_lazy = lazy(UserPermissions.get_for_user, UserPermissions)
class UserSpaceAccess(models.Model):
"""
User Authorities
"""
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
can_edit = models.BooleanField(_('can edit'), default=False)
class Meta:
verbose_name = _('user space access')
verbose_name_plural = _('user space accesses')
default_related_name = 'spaceaccesses'
unique_together = (('user', 'space'))
@staticmethod
def get_cache_key(pk):
return 'control:spaceaccesses:%d' % pk
@classmethod
def get_for_user(cls, user, force=False) -> Dict[int, bool]:
if not user.is_authenticated:
return {}
cache_key = cls.get_cache_key(user.pk)
result = None
if not force:
result = cache.get(cache_key, None)
for field in cls._meta.get_fields():
if not hasattr(result, field.attname):
result = None
break
if result:
return result
with UserPermissions.lock(user.pk):
result = dict(cls.objects.filter(user=user).values_list('space_id', 'can_edit'))
cache.set(cache_key, result, 900)
return result
def save(self, *args, **kwargs):
with UserPermissions.lock(self.user_id):
UserPermissions.objects.filter(user_id=self.user_id).select_for_update()
super().save(*args, **kwargs)
cache_key = self.get_cache_key(self.user_id)
cache.delete(cache_key)

View file

@ -151,4 +151,50 @@
<p><em>{% trans 'none' %}</em></p>
{% endif %}
{% endif %}
{% if space_accesses or space_accesses_form %}
<p></p>
<h4>{% trans 'Space Access' %}</h4>
{{ space_accesses_form.non_field_errors }}
{{ space_accesses_form.access_permissions.errors }}
{{ space_accesses_form.expires.errors }}
<table>
<tr>
<th>{% trans 'Space' %}</th>
<th>{% trans 'can edit' %}</th>
<th></th>
</tr>
{% for space_access in space_accesses %}
<tr>
<td>{{ space_access.space.title }}</td>
<td>
{% if space_access.can_edit %}
<strong class="green">{% trans 'Yes' %}</strong>
{% else %}
{% trans 'No' %}
{% endif %}
</td>
<td class="button-cell"><form method="post">
{% csrf_token %}<button type="submit" name="delete_space_access" value="{{ space_access.pk }}">{% trans 'Delete' %}</button>
</form></td>
</tr>
{% endfor %}
<form method="post">
{% csrf_token %}
{% if space_accesses_form %}
<tr>
<td class="input-cell">
{{ space_accesses_form.space }}
</td>
<td class="input-cell">
{{ space_accesses_form.can_edit }}
</td>
<td class="button-cell">
<button type="submit" name="submit_space_access" value="1">{% trans 'Add' %}</button>
</td>
</tr>
{% endif %}
</form>
</table>
{% endif %}
{% endblock %}

View file

@ -8,7 +8,7 @@ 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 import transaction
from django.db import IntegrityError, transaction
from django.db.models import Prefetch
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
@ -16,8 +16,8 @@ from django.utils import timezone
from django.utils.crypto import get_random_string
from django.utils.translation import ugettext_lazy as _
from c3nav.control.forms import AccessPermissionForm, AnnouncementForm, UserPermissionsForm
from c3nav.control.models import UserPermissions
from c3nav.control.forms import AccessPermissionForm, AnnouncementForm, UserPermissionsForm, UserSpaceAccessForm
from c3nav.control.models import UserPermissions, UserSpaceAccess
from c3nav.mapdata.models.access import AccessPermission, AccessPermissionToken, AccessRestriction
from c3nav.site.models import Announcement
@ -61,6 +61,7 @@ def user_detail(request, user):
qs = User.objects.select_related(
'permissions',
).prefetch_related(
Prefetch('spaceaccesses', UserSpaceAccess.objects.select_related('space')),
Prefetch('accesspermissions', AccessPermission.objects.select_related('access_restriction', 'author'))
)
user = get_object_or_404(qs, pk=user)
@ -165,7 +166,7 @@ def user_detail(request, user):
form = AccessPermissionForm(request=request)
access_permissions = {}
for permission in user.accesspermissions.select_related('access_restriction'):
for permission in user.accesspermissions.all():
access_permissions.setdefault(permission.access_restriction_id, []).append(permission)
access_permissions = tuple(
{
@ -188,6 +189,48 @@ def user_detail(request, user):
'access_permission_form': form
})
# space access
form = None
if request.user_permissions.grant_space_access:
if request.method == 'POST' and request.POST.get('submit_space_access'):
form = UserSpaceAccessForm(request=request, data=request.POST)
if form.is_valid():
instance = form.instance
instance.user = user
try:
instance.save()
except IntegrityError:
messages.error(request, _('User space access could not be granted because it already exists.'))
else:
messages.success(request, _('User space access successfully granted.'))
return redirect(request.path_info)
else:
form = UserSpaceAccessForm(request=request)
delete_space_access = request.POST.get('delete_space_access')
if delete_space_access:
with transaction.atomic():
try:
access = user.spaceaccesses.filter(pk=delete_space_access)
except AccessPermission.DoesNotExist:
messages.error(request, _('Unknown space access.'))
else:
if request.user_permissions.grant_space_access or user.pk == request.user.pk:
access.delete()
messages.success(request, _('Space access successfully deleted.'))
else:
messages.error(request, _('You cannot delete this space access.'))
return redirect(request.path_info)
space_accesses = None
if request.user_permissions.grant_space_access or user.pk == request.user.pk:
space_accesses = user.spaceaccesses.all()
ctx.update({
'space_accesses': space_accesses,
'space_accesses_form': form
})
return render(request, 'control/user.html', ctx)

View file

@ -887,7 +887,14 @@ main.control form tr > * {
padding-bottom: 4px;
text-align: right;
}
.button-cell button, .button-cell .button {
.button-cell button, .button-cell .button, .button-cell form {
margin: 0;
}
.input-cell {
padding-top: 6px;
padding-bottom: 6px;
}
.input-cell input, .input-cell select {
margin: 0;
}