redeem access permission tokens
This commit is contained in:
parent
003bfbe389
commit
101a4c6bf2
12 changed files with 133 additions and 19 deletions
|
@ -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))
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 %}
|
||||
|
|
24
src/c3nav/site/templates/site/confirm.html
Normal file
24
src/c3nav/site/templates/site/confirm.html
Normal 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 %}
|
|
@ -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> //
|
||||
|
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
@ -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)),
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue