add space access and ui for it in the control panel
This commit is contained in:
parent
1893b3fd40
commit
5e134e5b41
6 changed files with 219 additions and 11 deletions
|
@ -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
|
||||
|
||||
|
|
40
src/c3nav/control/migrations/0006_user_space_access.py
Normal file
40
src/c3nav/control/migrations/0006_user_space_access.py
Normal 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')},
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue