Verified Commit dba85fa5 authored by Sébastiaan Versteeg's avatar Sébastiaan Versteeg
Browse files

Refactor admin urls for event admin

parent 4594727b
......@@ -5,7 +5,7 @@ from django.core.exceptions import DisallowedRedirect
from django.db.models import Max, Min
from django.http import HttpResponseRedirect
from django.template.defaultfilters import date as _date
from django.urls import reverse
from django.urls import reverse, path
from django.utils import timezone
from django.utils.datetime_safe import date
from django.utils.html import format_html
......@@ -19,6 +19,7 @@ from pizzas.models import PizzaEvent
from utils.snippets import datetime_to_lectureyear
from utils.translation import TranslatedModelAdmin
from . import forms, models
import events.admin_views as admin_views
def _do_next(request, response):
......@@ -122,8 +123,8 @@ class EventAdmin(DoNextModelAdmin):
def overview_link(self, obj):
return format_html('<a href="{link}">{title}</a>',
link=reverse('events:admin-details',
kwargs={'event_id': obj.pk}),
link=reverse('admin:events_event_details',
kwargs={'pk': obj.pk}),
title=obj.title)
def has_change_permission(self, request, event=None):
......@@ -243,6 +244,28 @@ class EventAdmin(DoNextModelAdmin):
if self.has_change_permission(request, obj) or obj is None:
yield inline.get_formset(request, obj), inline
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('<int:pk>/details/',
self.admin_site.admin_view(
admin_views.EventAdminDetails.as_view()),
name='events_event_details'),
path('<int:pk>/export/',
self.admin_site.admin_view(
admin_views.EventRegistrationsExport.as_view()),
name='events_event_export'),
path('<int:pk>/export-email/',
self.admin_site.admin_view(
admin_views.EventRegistrationEmailsExport.as_view()),
name='events_event_export_email'),
path('<int:pk>/all-present/',
self.admin_site.admin_view(
admin_views.EventRegistrationsMarkPresent.as_view()),
name='events_event_all_present'),
]
return custom_urls + urls
@admin.register(models.Registration)
class RegistrationAdmin(DoNextModelAdmin):
......
import csv
import json
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import permission_required
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.text import slugify
from django.utils.translation import pgettext_lazy
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.http import require_http_methods
from django.views import View
from django.views.generic import DetailView, TemplateView
from events.decorators import organiser_only
from .models import Event, Registration
@staff_member_required
@permission_required('events.change_event')
@organiser_only
def details(request, event_id):
@method_decorator([staff_member_required, ], name='dispatch')
@method_decorator(organiser_only, name='dispatch')
class EventAdminDetails(DetailView):
"""
Renders an overview of registration for the specified event
:param request: the request object
:param event_id: the primary key of the event
:return: HttpResponse 200 with the page HTML
Renders an overview of registrations for the specified event
"""
event = get_object_or_404(Event, pk=event_id)
return render(request, 'events/admin/details.html', {
'event': event,
})
template_name = 'events/admin/details.html'
model = Event
queryset = Event.objects.filter(published=True)
context_object_name = 'event'
@staff_member_required
@permission_required('events.change_event')
@organiser_only
@require_http_methods(["POST"])
def change_registration(request, event_id, action=None):
"""
JSON call to change the status of a registration
:param request: the request object
:param event_id: the primary key of the event
:param action: specifies what should be changed
:return: JsonResponse with a success status
"""
data = {
'success': True
}
try:
id = request.POST.get("id", -1)
obj = Registration.objects.get(event=event_id, pk=id)
if action == 'present':
checked = json.loads(request.POST.get("checked"))
if checked is not None:
obj.present = checked
obj.save()
elif action == 'payment':
value = request.POST.get("value")
if value is not None:
obj.payment = value
obj.save()
except Registration.DoesNotExist:
data['success'] = False
return JsonResponse(data)
@staff_member_required
@permission_required('events.change_event')
def export(request, event_id):
@method_decorator([staff_member_required, ], name='dispatch')
@method_decorator(organiser_only, name='dispatch')
class EventRegistrationsExport(View):
"""
Export the registration of a specified event
:param request: the request object
:param event_id: the primary key of the event
:return: A CSV containing all registrations for the event
View to export registrations
"""
event = get_object_or_404(Event, pk=event_id)
extra_fields = event.registrationinformationfield_set.all()
registrations = event.registration_set.all()
header_fields = (
[_('Name'), _('Email'), _('Paid'), _('Present'),
_('Status'), _('Phone number')] +
[field.name for field in extra_fields] +
[_('Date'), _('Date cancelled')])
rows = []
if event.price == 0:
header_fields.remove(_('Paid'))
for i, registration in enumerate(registrations):
if registration.member:
name = registration.member.get_full_name()
else:
name = registration.name
status = pgettext_lazy('registration status',
'registered').capitalize()
cancelled = None
if registration.date_cancelled:
if registration.is_late_cancellation():
status = pgettext_lazy('registration status',
'late cancellation').capitalize()
else:
status = pgettext_lazy('registration status',
'cancelled').capitalize()
cancelled = timezone.localtime(registration.date_cancelled)
elif registration.queue_position:
status = pgettext_lazy('registration status', 'waiting')
data = {
_('Name'): name,
_('Date'): timezone.localtime(registration.date),
_('Present'): _('Yes') if registration.present else '',
_('Phone number'): (registration.member.profile.phone_number
if registration.member
else ''),
_('Email'): (registration.member.email
if registration.member
else ''),
_('Status'): status,
_('Date cancelled'): cancelled,
}
if event.price > 0:
if registration.payment == registration.PAYMENT_CASH:
data[_('Paid')] = _('Cash')
elif registration.payment == registration.PAYMENT_CARD:
data[_('Paid')] = _('Pin')
template_name = 'events/admin/details.html'
def get(self, request, pk):
"""
Export the registration of a specified event
:param request: the request object
:param pk: the primary key of the event
:return: A CSV containing all registrations for the event
"""
event = get_object_or_404(Event, pk=pk)
extra_fields = event.registrationinformationfield_set.all()
registrations = event.registration_set.all()
header_fields = (
[_('Name'), _('Email'), _('Paid'), _('Present'),
_('Status'), _('Phone number')] +
[field.name for field in extra_fields] +
[_('Date'), _('Date cancelled')])
rows = []
if event.price == 0:
header_fields.remove(_('Paid'))
for i, registration in enumerate(registrations):
if registration.member:
name = registration.member.get_full_name()
else:
data[_('Paid')] = _('No')
data.update({field['field'].name: field['value'] for field in
registration.information_fields})
rows.append(data)
response = HttpResponse(content_type='text/csv')
writer = csv.DictWriter(response, header_fields)
writer.writeheader()
rows = sorted(rows,
key=lambda row:
(row[_('Status')] == pgettext_lazy(
'registration status',
'late cancellation').capitalize(),
row[_('Date')]),
reverse=True,
)
for row in rows:
writer.writerow(row)
response['Content-Disposition'] = (
'attachment; filename="{}.csv"'.format(slugify(event.title)))
return response
@staff_member_required
@permission_required('events.change_event')
def export_email(request, event_id):
name = registration.name
status = pgettext_lazy('registration status',
'registered').capitalize()
cancelled = None
if registration.date_cancelled:
if registration.is_late_cancellation():
status = pgettext_lazy('registration status',
'late cancellation').capitalize()
else:
status = pgettext_lazy('registration status',
'cancelled').capitalize()
cancelled = timezone.localtime(registration.date_cancelled)
elif registration.queue_position:
status = pgettext_lazy('registration status', 'waiting')
data = {
_('Name'): name,
_('Date'): timezone.localtime(registration.date),
_('Present'): _('Yes') if registration.present else '',
_('Phone number'): (registration.member.profile.phone_number
if registration.member
else ''),
_('Email'): (registration.member.email
if registration.member
else ''),
_('Status'): status,
_('Date cancelled'): cancelled,
}
if event.price > 0:
if registration.payment == registration.PAYMENT_CASH:
data[_('Paid')] = _('Cash')
elif registration.payment == registration.PAYMENT_CARD:
data[_('Paid')] = _('Pin')
else:
data[_('Paid')] = _('No')
data.update({field['field'].name: field['value'] for field in
registration.information_fields})
rows.append(data)
response = HttpResponse(content_type='text/csv')
writer = csv.DictWriter(response, header_fields)
writer.writeheader()
rows = sorted(rows,
key=lambda row:
(row[_('Status')] == pgettext_lazy(
'registration status',
'late cancellation').capitalize(),
row[_('Date')]),
reverse=True,
)
for row in rows:
writer.writerow(row)
response['Content-Disposition'] = (
'attachment; filename="{}.csv"'.format(slugify(event.title)))
return response
@method_decorator([staff_member_required, ], name='dispatch')
@method_decorator(organiser_only, name='dispatch')
class EventRegistrationEmailsExport(TemplateView):
"""
Renders a page that outputs all email addresses of registered members
for an event
"""
template_name = 'events/admin/email_export.html'
:param request: the request object
:param event_id: the primary key of the event
:return: HttpResponse 200 with the HTML of the page
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
event = get_object_or_404(Event, pk=kwargs['pk'])
registrations = event.registration_set.filter(
date_cancelled=None)
registrations = registrations[:event.max_participants]
addresses = [r.member.email for r in registrations if r.member]
no_addresses = [r.name for r in registrations if not r.member]
context['event'] = event
context['addresses'] = addresses
context['no_addresses'] = no_addresses
return context
@method_decorator([staff_member_required, ], name='dispatch')
@method_decorator(organiser_only, name='dispatch')
class EventRegistrationsMarkPresent(View):
"""
event = get_object_or_404(Event, pk=event_id)
registrations = event.registration_set.filter(
date_cancelled=None)
registrations = registrations[:event.max_participants]
addresses = [r.member.email for r in registrations if r.member]
no_addresses = [r.name for r in registrations if not r.member]
return render(request, 'events/admin/email_export.html',
{'event': event, 'addresses': addresses,
'no_addresses': no_addresses})
@staff_member_required
@permission_required('events.change_event')
@organiser_only
def all_present(request, event_id):
Renders a page that outputs all email addresses of registered members
for an event
"""
Mark all registrations of an event as present
template_name = 'events/admin/email_export.html'
:param request: the request object
:param event_id: the primary key of the event
:return: HttpResponse 302 to the event admin page
"""
event = get_object_or_404(Event, pk=event_id)
def get(self, request, pk):
"""
Mark all registrations of an event as present
if event.max_participants is None:
registrations_query = event.registration_set.filter(
date_cancelled=None)
else:
registrations_query = (event.registration_set
.filter(date_cancelled=None)
.order_by('date')[:event.max_participants])
:param request: the request object
:param pk: the primary key of the event
:return: HttpResponse 302 to the event admin page
"""
event = get_object_or_404(Event, pk=pk)
if event.max_participants is None:
registrations_query = event.registration_set.filter(
date_cancelled=None)
else:
registrations_query = (event.registration_set
.filter(date_cancelled=None)
.order_by('date')[:event.max_participants])
event.registration_set.filter(pk__in=registrations_query).update(
present=True, payment=Registration.PAYMENT_CASH)
event.registration_set.filter(pk__in=registrations_query).update(
present=True, payment=Registration.PAYMENT_CASH)
return HttpResponseRedirect('/events/admin/{}'.format(event_id))
return HttpResponseRedirect(reverse('admin:events_event_details',
args=[str(event.pk)]))
......@@ -100,8 +100,8 @@ class UnpublishedEventSerializer(CalenderJSSerializer):
return "black"
def _url(self, instance):
return reverse('events:admin-details', kwargs={
'event_id': instance.id})
return reverse('admin:events_events_details', kwargs={
'pk': instance.id})
class EventRetrieveSerializer(serializers.ModelSerializer):
......@@ -222,7 +222,8 @@ class RegistrationListSerializer(serializers.ModelSerializer):
class Meta:
model = Registration
fields = ('pk', 'member', 'name', 'photo', 'avatar', 'registered_on',
'is_late_cancellation', 'is_cancelled', 'queue_position')
'is_late_cancellation', 'is_cancelled', 'queue_position',
'payment', 'present')
name = serializers.SerializerMethodField('_name')
photo = serializers.SerializerMethodField('_photo')
......@@ -282,7 +283,8 @@ class RegistrationSerializer(serializers.ModelSerializer):
model = Registration
fields = ('pk', 'member', 'name', 'photo', 'avatar', 'registered_on',
'is_late_cancellation', 'is_cancelled',
'queue_position', 'fields')
'queue_position', 'fields',
'payment', 'present')
name = serializers.SerializerMethodField('_name')
photo = serializers.SerializerMethodField('_photo')
......@@ -340,6 +342,7 @@ class RegistrationSerializer(serializers.ModelSerializer):
try:
if instance:
self.information_fields = services.registration_fields(
kwargs['context']['request'],
instance.member, instance.event)
except RegistrationError:
pass
......
......@@ -193,6 +193,7 @@ class RegistrationViewSet(GenericViewSet, RetrieveModelMixin,
registration.event,
serializer.field_values())
serializer.information_fields = services.registration_fields(
serializer.context['request'],
registration.member, registration.event)
def destroy(self, request, pk=None, **kwargs):
......
......@@ -13,7 +13,7 @@ def organiser_only(view_function):
class OrganiserOnly(object):
"""
Decorator that denies access to the page if:
1. There is no `event_id` in the request
1. There is no `pk` in the request
2. The specified event does not exist
3. The user is no organiser of the specified event
"""
......@@ -21,11 +21,11 @@ class OrganiserOnly(object):
self.view_function = view_function
def __call__(self, request, *args, **kwargs):
event_id = kwargs.get('event_id')
if event_id:
pk = kwargs.get('pk')
if pk:
event = None
try:
event = Event.objects.get(pk=event_id)
event = Event.objects.get(pk=pk)
except Event.DoesNotExist:
pass
......
......@@ -193,7 +193,7 @@ def update_registration(member, event, field_values):
field.set_value_for(registration, field_value)
def registration_fields(member, event):
def registration_fields(request, member, event):
"""
Returns information about the registration fields of a registration
......@@ -210,8 +210,9 @@ def registration_fields(member, event):
raise RegistrationError(
_("You are not registered for this event.")) from error
if (event_permissions(member, event)["update_registration"] and
registration):
perms = (event_permissions(member, event)["update_registration"] or
is_organiser(request.member, event))
if perms and registration:
information_fields = registration.information_fields
fields = OrderedDict()
......
.dashboard #content {
width: auto;
}
.dashboard #content .results {
overflow-y: auto;
}
.dashboard {
#content {
width: auto;
.results {
overflow-y: auto;
}
}
.dashboard .module table td a {
display: inline-block;
vertical-align: middle;
.module table td a {
display: inline-block;
vertical-align: middle;
&.member-phone, &.member-email {
width: 16px;
height: 16px;
}
&.member-phone, &.member-email {
width: 16px;
height: 16px;
}
&.member-phone {
background: url('../images/phone-square.svg') no-repeat;
padding-right: 2px;
}
&.member-phone {
background: url('../images/phone-square.svg') no-repeat;
padding-right: 2px;
}
&.member-email {
background: url('../images/envelope-square.svg') no-repeat;
&.member-email {
background: url('../images/envelope-square.svg') no-repeat;
}
}
}
......
django.jQuery(function () {
var $ = django.jQuery;
var url = $("#content-main").attr("data-url");
var payment_url = url + "payment/";
var present_url = url + "present/";
$(".present-check").change(function () {
var checkbox = $(this);
var id = checkbox.attr("data-id");
var url = checkbox.parent().parent().data("url");
var checked = checkbox.prop('checked');
post(present_url, { checked: checked, id: id }, function(result) {
if (!result.success) {
checkbox.prop('checked', !checked);
}
patch(url, { present: checked }, function(result) {
checkbox.prop('checked', result.present);
$("table").trigger("update");
}, function() {
checkbox.prop('checked', !checked);
......@@ -21,13 +15,11 @@ django.jQuery(function () {
$(".payment-radio").change(function () {
var radiobutton = $(this);
var id = radiobutton.attr("data-id");
var value = radiobutton.attr("data-value");
var url = radiobutton.parent().parent().data("url");
var value = radiobutton.data("value");
if (radiobutton.prop('checked')) {
post(payment_url, { value: value, id: id }, function(result) {
if (!result.success) {
radiobutton.prop('checked', !checked);
}
patch(url, { payment: value }, function(result) {
radiobutton.prop('checked', value === result.payment);
$("table").trigger("update");
}, function() {
radiobutton.prop('checked', !checked);
......@@ -74,30 +66,14 @@ django.jQuery(function () {
});
});
function post(url, data, success, error) {