redeem access permission tokens

This commit is contained in:
Laura Klünder 2017-12-10 14:13:20 +01:00
parent 003bfbe389
commit 101a4c6bf2
12 changed files with 133 additions and 19 deletions

View file

@ -8,7 +8,7 @@ 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 AccessPermissionToken, AccessRestriction
from c3nav.mapdata.models.access import AccessPermissionToken, AccessPermissionTokenItem, AccessRestriction
class UserPermissionsForm(ModelForm):
@ -99,11 +99,12 @@ class AccessPermissionForm(Form):
def get_token(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))
expire_date = self.cleaned_data['expires']
author_expire_date = self.author_access_permissions.get(restriction.pk)
if author_expire_date is not None:
expire_date = author_expire_date if expire_date is None else min(expire_date, author_expire_date)
restrictions.append(AccessPermissionTokenItem(pk=restriction.pk, expire_date=expire_date,
title=restriction.title))
return AccessPermissionToken(author=self.author,
can_grant=self.cleaned_data.get('can_grant', '0') == '1',
restrictions=tuple(restrictions))

View file

@ -11,10 +11,10 @@
{% trans 'Scan this QR code to get access permissions:' %}
</p>
<p>
<img src="/qr/access/{{ token }}">
<img src="{{ url_qr }}">
</p>
<p>
{{ absolute_url }}
{{ url_absolute }}
</p>
<p>
<a href="{% url 'control.access' %}">« {% trans 'back' %}</a>

View file

@ -6,6 +6,6 @@ urlpatterns = [
url(r'^users/$', user_list, name='control.users'),
url(r'^users/(?P<user>\d+)/$', user_detail, name='control.users.detail'),
url(r'^access/$', grant_access, name='control.access'),
url(r'^access/qr/(?P<token>[^/]+)', grant_access_qr, name='control.access.qr'),
url(r'^access/(?P<token>[^/]+)$', grant_access_qr, name='control.access.qr'),
url(r'^$', main_index, name='control.index'),
]

View file

@ -155,12 +155,14 @@ def grant_access_qr(request, token):
messages.error(request, _('You can only display your most recently created token.'))
if token is None:
redirect('control.access')
return redirect('control.access')
token.bump()
token.save()
url = reverse('site.access.redeem', kwargs={'token': str(token.id)})
return render(request, 'control/access_qr.html', {
'token': token.id,
'absolute_url': request.build_absolute_uri('/access/qr/%s' % token.id)
'url': url,
'url_qr': reverse('site.qr', kwargs={'path': url}),
'url_absolute': request.build_absolute_uri(url),
})

View file

@ -1,6 +1,8 @@
import pickle
import uuid
from collections import namedtuple
from datetime import timedelta
from typing import Iterable
from django.conf import settings
from django.core.cache import cache
@ -35,6 +37,9 @@ def default_valid_until():
return timezone.now()+timedelta(seconds=20)
AccessPermissionTokenItem = namedtuple('AccessPermissionTokenItem', ('pk', 'expire_date', 'title'))
class AccessPermissionToken(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
@ -52,11 +57,11 @@ class AccessPermissionToken(models.Model):
data = models.BinaryField()
@property
def restrictions(self):
def restrictions(self) -> Iterable[AccessPermissionTokenItem]:
return pickle.loads(self.data)
@restrictions.setter
def restrictions(self, value):
def restrictions(self, value: Iterable[AccessPermissionTokenItem]):
self.data = pickle.dumps(value)
def redeem(self, user=None):
@ -68,13 +73,13 @@ class AccessPermissionToken(models.Model):
self.redeemed = True
if user:
for pk, expire_date in self.restrictions:
for restriction in self.restrictions:
obj, created = AccessPermission.objects.get_or_create(
user=user,
access_restriction_id=pk
access_restriction_id=restriction.pk
)
obj.author_id = self.author_id
obj.expire_date = expire_date
obj.expire_date = restriction.expire_date
obj.can_grant = self.can_grant
obj.save()
self.redeemed_by = user

View file

@ -686,6 +686,22 @@ ul.messages li.alert-danger {
background-color:#FFEEEE;
}
#messages {
z-index: 5;
position: absolute;
top: -20px;
width: 100vw;
padding: 10px;
}
#messages ul.messages {
margin: auto;
max-width: 700px;
}
#messages ul.messages .close {
float: right;
color: inherit;
}
.search-form input {
max-width: 400px;
vertical-align: top;
@ -712,6 +728,14 @@ main.control form tr > * {
margin: 0;
}
main .narrow {
max-width: 400px;
margin: auto;
}
main .narrow p, main .narrow form, main .narrow button {
margin-bottom: 1.0rem;
}
.user-permissions-form label {
font-weight: 400;
width: auto;

View file

@ -41,6 +41,15 @@ c3nav = {
}
c3nav.continue_init();
});
$('#messages').find('ul.messages li').each(function() {
$(this).prepend(
$('<a href="#" class="close"><i class="material-icons">close</i></a>').click(function(e) {
e.preventDefault();
$(this).parent().remove();
})
);
})
},
continue_init: function() {
c3nav.init_map();

View file

@ -3,8 +3,11 @@
{% block content %}
<main class="account">
<div class="narrow">
<h2>{{ title }}</h2>
{% include 'site/fragment_messages.html' %}
{% if back_url %}
<p><a href="{{ back_url }}">« {% trans 'back' %}</a></p>
{% endif %}
@ -17,5 +20,6 @@
<a href="{{ bottom_link_url }}?{{ request.GET.urlencode }}">{{ bottom_link_text }}</a>
{% endif %}
</form>
</div>
</main>
{% endblock %}

View file

@ -0,0 +1,24 @@
{% extends 'site/base.html' %}
{% load i18n %}
{% block content %}
<main>
<div class="narrow">
<h2>{{ title }}</h2>
<form method="post">
{% include 'site/fragment_messages.html' %}
{% for text in texts %}
<p>{{ text }}</p>
{% endfor %}
{% csrf_token %}
<button>{% if button_text %}{{ button_text }}{% else %}{{ title }}{% endif %}</button>
<p>
<a href="{% url 'site.index' %}">« {% trans 'back' %}</a>
</p>
</form>
</div>
</main>
{% endblock %}

View file

@ -5,6 +5,7 @@
{% block content %}
<main class="map" data-state="{{ state }}"{% if embed %} data-embed{% endif %}>
<section id="messages">{% include 'site/fragment_messages.html' %}</section>
<section id="attributions">
<a href="{% url 'editor.index' %}" target="_blank">{% trans 'Editor' %}</a> //
<a href="/api/" target="_blank">{% trans 'API' %}</a> //

View file

@ -1,7 +1,7 @@
from django.conf.urls import url
from c3nav.site.views import (account_view, change_password_view, login_view, logout_view, map_index, qr_code,
register_view)
from c3nav.site.views import (access_redeem_view, account_view, change_password_view, login_view, logout_view,
map_index, qr_code, register_view)
slug = r'(?P<slug>[a-z0-9-_.:]+)'
slug2 = r'(?P<slug2>[a-z0-9-_.:]+)'
@ -21,4 +21,5 @@ urlpatterns = [
url(r'^register$', register_view, name='site.register'),
url(r'^account/$', account_view, name='site.account'),
url(r'^account/change_password$', change_password_view, name='site.account.change_password'),
url(r'^access/(?P<token>[^/]+)$', access_redeem_view, name='site.access.redeem'),
]

View file

@ -7,16 +7,21 @@ from django.contrib import messages
from django.contrib.auth import login, logout
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm, UserCreationForm
from django.contrib.auth.views import redirect_to_login
from django.core.serializers.json import DjangoJSONEncoder
from django.db import transaction
from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from django.views.decorators.cache import cache_control, never_cache
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.http import etag
from c3nav.mapdata.models import Location, Source
from c3nav.mapdata.models.access import AccessPermissionToken
from c3nav.mapdata.models.locations import LocationRedirect, SpecificLocation
from c3nav.mapdata.utils.locations import get_location_by_slug_for_request, levels_by_short_label_for_request
from c3nav.mapdata.utils.user import get_user_data
@ -206,3 +211,41 @@ def change_password_view(request):
@login_required(login_url='site.login')
def account_view(request):
return render(request, 'site/account.html', {})
@never_cache
@login_required(login_url='site.login')
def access_redeem_view(request, token):
with transaction.atomic():
try:
token = AccessPermissionToken.objects.select_for_update().get(id=token, redeemed=False,
valid_until__gte=timezone.now())
except AccessPermissionToken.DoesNotExist:
messages.error(request, _('This token does not exist or was already redeemed.'))
return redirect('site.index')
num_restrictions = len(token.restrictions)
if request.method == 'POST':
token.redeemed = True
token.save()
if not request.user.is_authenticated:
messages.info(request, _('You need to log in to unlock areas.'))
request.session['redeem_token_on_login'] = token.id
return redirect_to_login(request.get_full_path(), 'site.login')
token.redeemed_by = request.user
token.save()
messages.success(request, ungettext_lazy('Area successfully unlocked.',
'Areas successfully unlocked.', num_restrictions))
return redirect('site.index')
return render(request, 'site/confirm.html', {
'title': ungettext_lazy('Unlock area', 'Unlock areas', num_restrictions),
'texts': (ungettext_lazy('You have been invited to unlock the following area:',
'You have been invited to unlock the following areas:',
num_restrictions),
', '.join(str(restriction.title) for restriction in token.restrictions)),
})