diff options
-rw-r--r-- | postgresqleu/confreg/admin.py | 2 | ||||
-rw-r--r-- | postgresqleu/confreg/backendforms.py | 28 | ||||
-rw-r--r-- | postgresqleu/confreg/backendviews.py | 61 | ||||
-rw-r--r-- | postgresqleu/confreg/migrations/0023_accesstokens.py | 29 | ||||
-rw-r--r-- | postgresqleu/confreg/models.py | 26 | ||||
-rw-r--r-- | postgresqleu/confsponsor/util.py | 7 | ||||
-rw-r--r-- | postgresqleu/urls.py | 3 | ||||
-rw-r--r-- | template/confreg/admin_dashboard_single.html | 1 |
8 files changed, 154 insertions, 3 deletions
diff --git a/postgresqleu/confreg/admin.py b/postgresqleu/confreg/admin.py index 39a4e09e..ce725d3a 100644 --- a/postgresqleu/confreg/admin.py +++ b/postgresqleu/confreg/admin.py @@ -18,6 +18,7 @@ from models import ConferenceFeedbackQuestion, Speaker_Photo from models import PrepaidVoucher, PrepaidBatch, BulkPayment, DiscountCode from models import PendingAdditionalOrder from models import VolunteerSlot +from models import AccessToken from selectable.forms.widgets import AutoCompleteSelectWidget, AutoCompleteSelectMultipleWidget from postgresqleu.accountinfo.lookups import UserLookup @@ -618,3 +619,4 @@ admin.site.register(BulkPayment, BulkPaymentAdmin) admin.site.register(AttendeeMail, AttendeeMailAdmin) admin.site.register(PendingAdditionalOrder, PendingAdditionalOrderAdmin) admin.site.register(VolunteerSlot, VolunteerSlotAdmin) +admin.site.register(AccessToken) diff --git a/postgresqleu/confreg/backendforms.py b/postgresqleu/confreg/backendforms.py index 87fc78f9..b893c4d6 100644 --- a/postgresqleu/confreg/backendforms.py +++ b/postgresqleu/confreg/backendforms.py @@ -4,6 +4,7 @@ from django.db.models import Q import django.forms import django.forms.widgets from django.forms.widgets import TextInput +from django.utils.safestring import mark_safe import datetime from psycopg2.extras import DateTimeTZRange @@ -12,6 +13,7 @@ from selectable.forms.widgets import AutoCompleteSelectWidget, AutoCompleteSelec from postgresqleu.util.admin import SelectableWidgetAdminFormMixin from postgresqleu.util.forms import ConcurrentProtectedModelForm +from postgresqleu.util.random import generate_random_token from postgresqleu.accountinfo.lookups import UserLookup from postgresqleu.confreg.lookups import RegistrationLookup @@ -21,7 +23,7 @@ from postgresqleu.confreg.models import RegistrationClass, RegistrationType, Reg from postgresqleu.confreg.models import ConferenceAdditionalOption, ConferenceFeedbackQuestion from postgresqleu.confreg.models import ConferenceSession, Track, Room from postgresqleu.confreg.models import ConferenceSessionScheduleSlot, VolunteerSlot -from postgresqleu.confreg.models import DiscountCode +from postgresqleu.confreg.models import DiscountCode, AccessToken, AccessTokenPermissions from postgresqleu.confreg.models import valid_status_transitions, get_status_string @@ -543,6 +545,30 @@ class BackendDiscountCodeForm(BackendForm): self.update_protected_fields() +class BackendAccessTokenForm(BackendForm): + list_fields = ['token', 'description', 'permissions', ] + readonly_fields = ['token', ] + + class Meta: + model = AccessToken + fields = ['token', 'description', 'permissions', ] + + def _transformed_accesstoken_permissions(self): + for k,v in AccessTokenPermissions: + baseurl = '/events/admin/{0}/tokendata/{1}/{2}'.format(self.conference.urlname, self.instance.token, k) + yield k, mark_safe('{0} (<a href="{1}.csv">csv</a>, <a href="{1}.tsv">tsv</a>)'.format(v, baseurl)) + + def fix_fields(self): + self.fields['permissions'].widget = django.forms.CheckboxSelectMultiple( + choices=self._transformed_accesstoken_permissions(), + ) + + @classmethod + def get_initial(self): + return { + 'token': generate_random_token() + } + # # Form to pick a conference to copy from # diff --git a/postgresqleu/confreg/backendviews.py b/postgresqleu/confreg/backendviews.py index a4219a8b..a0c1a511 100644 --- a/postgresqleu/confreg/backendviews.py +++ b/postgresqleu/confreg/backendviews.py @@ -2,7 +2,7 @@ from django.shortcuts import render, get_object_or_404 from django.db import transaction from django import forms from django.core import urlresolvers -from django.http import HttpResponseRedirect, Http404 +from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.contrib.admin.utils import NestedObjects from django.contrib import messages from django.contrib.auth.decorators import login_required @@ -10,14 +10,17 @@ from django.conf import settings import urllib import datetime +import csv from postgresqleu.util.middleware import RedirectException -from postgresqleu.util.db import exec_to_dict, exec_no_result +from postgresqleu.util.db import exec_to_list, exec_to_dict, exec_no_result from models import Conference, ConferenceRegistration from models import RegistrationType, RegistrationClass +from models import AccessToken from postgresqleu.invoices.models import Invoice +from postgresqleu.confsponsor.util import get_sponsor_dashboard_data from backendforms import BackendCopySelectConferenceForm from backendforms import BackendConferenceForm, BackendRegistrationForm @@ -26,6 +29,7 @@ from backendforms import BackendRegistrationDayForm, BackendAdditionalOptionForm from backendforms import BackendTrackForm, BackendRoomForm, BackendConferenceSessionForm from backendforms import BackendConferenceSessionSlotForm, BackendVolunteerSlotForm from backendforms import BackendFeedbackQuestionForm, BackendDiscountCodeForm +from backendforms import BackendAccessTokenForm def get_authenticated_conference(request, urlname): if not request.user.is_authenticated: @@ -375,6 +379,12 @@ def edit_discountcodes(request, urlname, rest): BackendDiscountCodeForm, rest) +def edit_accesstokens(request, urlname, rest): + return backend_list_editor(request, + urlname, + BackendAccessTokenForm, + rest) + ### # Non-simple-editor views @@ -420,3 +430,50 @@ FROM confreg_conferenceregistration WHERE conference_id=%(confid)s""", { 'confid': conference.id, })[0], }) + + + +def _reencode_row(r): + def _reencode_value(v): + if isinstance(v, unicode): + return v.encode('utf-8') + return v + return [_reencode_value(x) for x in r] + +def tokendata(request, urlname, token, datatype, dataformat): + conference = get_object_or_404(Conference, urlname=urlname) + if not AccessToken.objects.filter(conference=conference, token=token, permissions__contains=[datatype,]).exists(): + raise Http404() + + if dataformat.lower() == 'csv': + delimiter = "," + elif dataformat.lower() == 'tsv': + delimiter = "\t" + else: + raise Http404() + + response = HttpResponse(content_type='text/plain; charset=utf-8') + writer = csv.writer(response, delimiter=delimiter) + writer.writerow(["File loaded", datetime.datetime.now()]) + + if datatype == 'regtypes': + writer.writerow(['Type', 'Confirmed', 'Unconfirmed']) + for r in exec_to_list("SELECT regtype, count(payconfirmedat) AS confirmed, count(r.id) FILTER (WHERE payconfirmedat IS NULL) AS unconfirmed FROM confreg_conferenceregistration r RIGHT JOIN confreg_registrationtype rt ON rt.id=r.regtype_id WHERE rt.conference_id=%(confid)s GROUP BY rt.id ORDER BY rt.sortkey", { 'confid': conference.id, }): + writer.writerow(_reencode_row(r)) + elif datatype == 'discounts': + writer.writerow(['Code', 'Max uses', 'Confirmed', 'Unconfirmed']) + for r in exec_to_list("SELECT code, maxuses, count(payconfirmedat) AS confirmed, count(r.id) FILTER (WHERE payconfirmedat IS NULL) AS unconfirmed FROM confreg_conferenceregistration r RIGHT JOIN confreg_discountcode dc ON dc.code=r.vouchercode WHERE dc.conference_id=%(confid)s AND (r.conference_id=%(confid)s OR r.conference_id IS NULL) GROUP BY dc.id ORDER BY code", {'confid': conference.id, }): + writer.writerow(_reencode_row(r)) + elif datatype == 'vouchers': + writer.writerow(["Code", "Buyer", "Used", "Unused"]) + for r in exec_to_list("SELECT b.buyername, count(v.user_id) AS used, count(*) FILTER (WHERE v.user_id IS NULL) AS unused FROM confreg_prepaidbatch b INNER JOIN confreg_prepaidvoucher v ON v.batch_id=b.id WHERE b.conference_id=%(confid)s GROUP BY b.id ORDER BY buyername", {'confid': conference.id, }): + writer.writerow(_reencode_row(r)) + elif datatype == 'sponsors': + (headers, data) = get_sponsor_dashboard_data(conference) + writer.writerow(headers) + for r in data: + writer.writerow(_reencode_row(r)) + else: + raise Http404() + + return response diff --git a/postgresqleu/confreg/migrations/0023_accesstokens.py b/postgresqleu/confreg/migrations/0023_accesstokens.py new file mode 100644 index 00000000..0167ffd0 --- /dev/null +++ b/postgresqleu/confreg/migrations/0023_accesstokens.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import postgresqleu.util.forms + + +class Migration(migrations.Migration): + + dependencies = [ + ('confreg', '0022_ask_more_fields'), + ] + + operations = [ + migrations.CreateModel( + name='AccessToken', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('token', models.CharField(max_length=200)), + ('description', models.TextField()), + ('permissions', postgresqleu.util.forms.ChoiceArrayField(base_field=models.CharField(max_length=32, choices=[(b'regtypes', b'Registration types and counters'), (b'discounts', b'Discount codes'), (b'vouchers', b'Voucher codes'), (b'sponsors', b'Sponsors and counts')]), size=None)), + ('conference', models.ForeignKey(to='confreg.Conference')), + ], + ), + migrations.AlterUniqueTogether( + name='accesstoken', + unique_together=set([('conference', 'token')]), + ), + ] diff --git a/postgresqleu/confreg/models.py b/postgresqleu/confreg/models.py index 98515084..2303aa11 100644 --- a/postgresqleu/confreg/models.py +++ b/postgresqleu/confreg/models.py @@ -11,6 +11,7 @@ from django.utils.dateformat import DateFormat from django.contrib.postgres.fields import DateTimeRangeField from postgresqleu.util.validators import validate_lowercase +from postgresqleu.util.forms import ChoiceArrayField from postgresqleu.confreg.dbimage import SpeakerImageStorage @@ -903,3 +904,28 @@ class AggregatedDietary(models.Model): class Meta: unique_together = ( ('conference', 'dietary'), ) + + +AccessTokenPermissions = ( + ('regtypes', 'Registration types and counters'), + ('discounts', 'Discount codes'), + ('vouchers', 'Voucher codes'), + ('sponsors', 'Sponsors and counts'), +) + +class AccessToken(models.Model): + conference = models.ForeignKey(Conference, null=False, blank=False) + token = models.CharField(max_length=200, null=False, blank=False) + description = models.TextField(null=False, blank=False) + permissions = ChoiceArrayField( + models.CharField(max_length=32, blank=False, null=False, choices=AccessTokenPermissions) + ) + + class Meta: + unique_together = ( ('conference', 'token'), ) + + def __unicode__(self): + return self.token + + def _display_permissions(self): + return ", ".join(self.permissions) diff --git a/postgresqleu/confsponsor/util.py b/postgresqleu/confsponsor/util.py new file mode 100644 index 00000000..5a81ef54 --- /dev/null +++ b/postgresqleu/confsponsor/util.py @@ -0,0 +1,7 @@ +from postgresqleu.util.db import exec_to_list + +def get_sponsor_dashboard_data(conference): + return ( + ["Level", "Confirmed", "Unconfirmed"], + exec_to_list("SELECT l.levelname, count(s.id) FILTER (WHERE confirmed) AS confirmed, count(s.id) FILTER (WHERE NOT confirmed) AS unconfirmed FROM confsponsor_sponsorshiplevel l LEFT JOIN confsponsor_sponsor s ON s.level_id=l.id WHERE l.conference_id=%(confid)s GROUP BY l.id ORDER BY levelcost", {'confid': conference.id, }) + ) diff --git a/postgresqleu/urls.py b/postgresqleu/urls.py index 1a824426..ab3d44ac 100644 --- a/postgresqleu/urls.py +++ b/postgresqleu/urls.py @@ -150,9 +150,12 @@ urlpatterns = [ url(r'^events/admin/(\w+)/volunteerslots/(.*/)?$', postgresqleu.confreg.backendviews.edit_volunteerslots), url(r'^events/admin/(\w+)/feedbackquestions/(.*/)?$', postgresqleu.confreg.backendviews.edit_feedbackquestions), url(r'^events/admin/(\w+)/discountcodes/(.*/)?$', postgresqleu.confreg.backendviews.edit_discountcodes), + url(r'^events/admin/(\w+)/accesstokens/(.*/)?$', postgresqleu.confreg.backendviews.edit_accesstokens), url(r'^events/admin/(\w+)/pendinginvoices/$', postgresqleu.confreg.backendviews.pendinginvoices), url(r'^events/admin/(\w+)/purgedata/$', postgresqleu.confreg.backendviews.purge_personal_data), + url(r'^events/admin/(\w+)/tokendata/([a-z0-9]{64})/(\w+)\.(tsv|csv)$', postgresqleu.confreg.backendviews.tokendata), + url(r'^events/sponsor/', include('postgresqleu.confsponsor.urls')), # "Homepage" for events diff --git a/template/confreg/admin_dashboard_single.html b/template/confreg/admin_dashboard_single.html index e712f585..df307c77 100644 --- a/template/confreg/admin_dashboard_single.html +++ b/template/confreg/admin_dashboard_single.html @@ -74,6 +74,7 @@ <h2>Metadata</h2> <div class="row"> <div class="col-md-3 col-sm-6 col-xs-12 buttonrow"><a class="btn btn-default btn-block" href="/events/admin/{{c.urlname}}/edit/">Conference entry</a></div> + <div class="col-md-3 col-sm-6 col-xs-12 buttonrow"><a class="btn btn-default btn-block" href="/events/admin/{{c.urlname}}/accesstokens/">Access tokens</a></div> <div class="col-md-3 col-sm-6 col-xs-12 buttonrow"><a class="btn btn-default btn-block" href="/events/admin/{{c.urlname}}/feedbackquestions/">Feedback questions</a></div> </div> <div class="row"> |