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

Refactor events app frontend logic

- Put admin views in a separate file
- Move creation/updating/cancelling of registrations to services.py
- Create classes for all frontend views so that these are class-based
- Make small changes to registration behaviour and urls (update form now loads using get)
- Make small changes to model methods: change some into properties or remove them in total and add new ones for better global usage
- Add API for registrations

Use event instead of event pk for

Add is_registration_allowed

Do not use status for is_registration_allowed

Rename is_registration_allowed, add cancellation_allowed and tests

Completely remove event 'status'

Catch DoesNotExist exceptions

Fix tests and PEP8

Rename is_registration_allowed to _is_registration_allowed

Move event attendance check to member model

Remove status property from events

Fix API fields

Add deprecated status field to events serializer

Fix event_id to pk

Fix serializers and viewsets

Change errors of update_registration

Make  a default empty hidden field

Fix fields in admin

Return latest data after update

Add tests for API

Fix all_present

Fix services create registration return
parent 7f93d3dc
# -*- coding: utf-8 -*-
from django.contrib import admin
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.template.defaultfilters import date as _date
from django.urls import reverse
from django.utils import timezone
from django.utils.html import format_html
from django.utils.http import is_safe_url
from django.utils.translation import ugettext_lazy as _
from django.template.defaultfilters import date as _date
from events import services
from members.models import Member
from utils.translation import TranslatedModelAdmin
from . import forms, models
......@@ -73,12 +73,9 @@ class EventAdmin(DoNextModelAdmin):
def has_change_permission(self, request, event=None):
try:
if (not request.user.is_superuser and event is not None and
not request.user.has_perm('events.override_organiser')):
committees = request.user.member.get_committees().filter(
Q(pk=event.organiser.pk)).count()
if committees == 0:
return False
if (event is not None and
not services.is_organiser(request.user, event)):
return False
except Member.DoesNotExist:
pass
return super().has_change_permission(request, event)
......@@ -150,7 +147,8 @@ class EventAdmin(DoNextModelAdmin):
# Use custom queryset for organiser field
# Only get the current active committees the user is a member of
try:
if not request.user.is_superuser:
if not (request.user.is_superuser or
request.user.has_perm('events.override_organiser')):
kwargs['queryset'] = request.user.member.get_committees()
except Member.DoesNotExist:
......
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.utils import timezone
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 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):
event = get_object_or_404(Event, pk=event_id)
return render(request, 'events/admin/details.html', {
'event': event,
})
@staff_member_required
@permission_required('events.change_event')
@organiser_only
@require_http_methods(["POST"])
def change_registration(request, event_id, action=None):
data = {
'success': True
}
try:
id = request.POST.get("id", -1)
checked = json.loads(request.POST.get("checked"))
obj = Registration.objects.get(event=event_id, pk=id)
if checked is not None:
if action == 'present':
obj.present = checked
elif action == 'paid':
obj.paid = checked
obj.save()
except Registration.DoesNotExist:
data['success'] = False
return JsonResponse(data)
@staff_member_required
@permission_required('events.change_event')
@organiser_only
def export(request, event_id):
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')
cancelled = None
if registration.date_cancelled:
if registration.is_late_cancellation():
status = pgettext_lazy('registration status',
'late cancellation')
else:
status = pgettext_lazy('registration status', 'cancelled')
cancelled = timezone.localtime(registration.date_cancelled)
elif registration.queue_position:
status = pgettext_lazy('registration status', 'waiting')
data = {
'name': name,
'date': timezone.localtime(registration.date
).strftime("%Y-%m-%d %H:%m"),
'present': _('Yes') if registration.present else '',
'phone number': (registration.member.phone_number
if registration.member
else ''),
'email': (registration.member.user.email
if registration.member
else ''),
'status': status,
'date cancelled': cancelled,
}
if event.price > 0:
if registration.payment == 'cash_payment':
data['paid'] = _('Cash')
elif registration.payment == 'pin_payment':
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'),
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):
event = get_object_or_404(Event, pk=event_id)
registrations = event.registration_set.filter(
date_cancelled=None).prefetch_related('member__user')
registrations = registrations[:event.max_participants]
addresses = [r.member.user.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):
event = get_object_or_404(Event, pk=event_id)
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='cash_payment')
return HttpResponseRedirect('/events/admin/{}'.format(event_id))
from html import unescape
from django.templatetags.static import static
from django.urls import reverse
from django.utils import timezone
from django.utils.html import strip_tags
from html import unescape
from rest_framework import serializers
from rest_framework.fields import empty
from events.models import Event, Registration
from events import services
from events.exceptions import RegistrationError
from events.models import Event, Registration, RegistrationInformationField
from pizzas.models import PizzaEvent
from thaliawebsite.settings import settings
......@@ -71,11 +73,11 @@ class EventCalenderJSSerializer(CalenderJSSerializer):
model = Event
def _url(self, instance):
return reverse('events:event', kwargs={'event_id': instance.id})
return reverse('events:event', kwargs={'pk': instance.id})
def _registered(self, instance):
try:
return instance.is_member_registered(self.context['user'].member)
return services.is_user_registered(instance, self.context['user'])
except AttributeError:
return None
......@@ -111,21 +113,50 @@ class EventRetrieveSerializer(serializers.ModelSerializer):
registration_allowed = serializers.SerializerMethodField(
'_registration_allowed')
has_fields = serializers.SerializerMethodField('_has_fields')
status = serializers.SerializerMethodField('_status') # DEPRECATED
REGISTRATION_NOT_NEEDED = -1
REGISTRATION_NOT_YET_OPEN = 0
REGISTRATION_OPEN = 1
REGISTRATION_OPEN_NO_CANCEL = 2
REGISTRATION_CLOSED = 3
REGISTRATION_CLOSED_CANCEL_ONLY = 4
""" DEPRECATED """
def _status(self, instance):
now = timezone.now()
if instance.registration_start or instance.registration_end:
if now <= instance.registration_start:
return self.REGISTRATION_NOT_YET_OPEN
elif (instance.registration_end <= now
< instance.cancel_deadline):
return self.REGISTRATION_CLOSED_CANCEL_ONLY
elif (instance.cancel_deadline <= now <
instance.registration_end):
return self.REGISTRATION_OPEN_NO_CANCEL
elif (now >= instance.registration_end and
now >= instance.cancel_deadline):
return self.REGISTRATION_CLOSED
else:
return self.REGISTRATION_OPEN
else:
return self.REGISTRATION_NOT_NEEDED
def _description(self, instance):
return unescape(strip_tags(instance.description))
def _num_participants(self, instance):
if (instance.max_participants and
instance.num_participants() > instance.max_participants):
instance.participants.count() > instance.max_participants):
return instance.max_participants
return instance.num_participants()
return instance.participants.count()
def _user_registration(self, instance):
try:
reg = instance.registration_set.get(
member=self.context['request'].user.member)
return RegistrationSerializer(reg, context=self.context).data
return RegistrationListSerializer(reg, context=self.context).data
except Registration.DoesNotExist:
return None
......@@ -154,8 +185,8 @@ class EventListSerializer(serializers.ModelSerializer):
def _registered(self, instance):
try:
return instance.is_member_registered(
self.context['request'].user.member)
return services.is_user_registered(
instance, self.context['request'].user)
except AttributeError:
return None
......@@ -164,7 +195,7 @@ class EventListSerializer(serializers.ModelSerializer):
return pizza_events.exists()
class RegistrationSerializer(serializers.ModelSerializer):
class RegistrationListSerializer(serializers.ModelSerializer):
class Meta:
model = Registration
fields = ('pk', 'member', 'name', 'photo', 'registered_on',
......@@ -173,43 +204,75 @@ class RegistrationSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField('_name')
photo = serializers.SerializerMethodField('_photo')
member = serializers.SerializerMethodField('_member')
registered_on = serializers.SerializerMethodField('_registered_on')
registered_on = serializers.DateTimeField(source='date')
is_cancelled = serializers.SerializerMethodField('_is_cancelled')
is_late_cancellation = serializers.SerializerMethodField(
'_is_late_cancellation')
queue_position = serializers.SerializerMethodField(
'_queue_position')
def _has_view_permission(self, instance):
# We dont have an explicit viewing permission model, so we rely on the
# 'change' permission (Django provides add/change/delete by default)
return (self.context['request'].user.has_perm('events.change_event')
or instance.member.user == self.context['request'].user)
'_queue_position', read_only=False)
def _is_late_cancellation(self, instance):
if self._has_view_permission(instance):
return instance.is_late_cancellation()
return None
return instance.is_late_cancellation()
def _queue_position(self, instance):
if self._has_view_permission(instance):
pos = instance.queue_position()
return pos if pos > 0 else None
return None
pos = instance.queue_position
return pos if pos > 0 else None
def _is_cancelled(self, instance):
return instance.date_cancelled is not None
def _registered_on(self, instance):
if self._has_view_permission(instance):
return serializers.DateTimeField().to_representation(instance.date)
def _member(self, instance):
if instance.member:
return instance.member.pk
return None
def _name(self, instance):
if instance.member:
return instance.member.display_name()
return instance.name
def _photo(self, instance):
if instance.member and instance.member.photo:
return self.context['request'].build_absolute_uri(
'%s%s' % (settings.MEDIA_URL, instance.member.photo))
else:
return self.context['request'].build_absolute_uri(
static('members/images/default-avatar.jpg'))
class RegistrationSerializer(serializers.ModelSerializer):
information_fields = None
class Meta:
model = Registration
fields = ('pk', 'member', 'name', 'photo', 'registered_on',
'is_late_cancellation', 'is_cancelled',
'queue_position')
name = serializers.SerializerMethodField('_name')
photo = serializers.SerializerMethodField('_photo')
member = serializers.SerializerMethodField('_member')
registered_on = serializers.DateTimeField(source='date', read_only=True)
is_cancelled = serializers.SerializerMethodField('_is_cancelled')
is_late_cancellation = serializers.SerializerMethodField(
'_is_late_cancellation')
queue_position = serializers.SerializerMethodField(
'_queue_position', read_only=False)
def _is_late_cancellation(self, instance):
val = instance.is_late_cancellation()
return False if val is None else val
def _is_cancelled(self, instance):
if self._has_view_permission(instance):
return instance.date_cancelled is not None
return None
return instance.date_cancelled is not None
def _queue_position(self, instance):
pos = instance.queue_position
return pos if pos > 0 else None
def _member(self, instance):
if instance.member:
return instance.member.user.pk
return instance.member.pk
return None
def _name(self, instance):
......@@ -224,3 +287,59 @@ class RegistrationSerializer(serializers.ModelSerializer):
else:
return self.context['request'].build_absolute_uri(
static('members/images/default-avatar.jpg'))
def __init__(self, instance=None, data=empty, **kwargs):
super().__init__(instance, data, **kwargs)
try:
if instance:
self.information_fields = services.registration_fields(
instance.member.user, instance.event)
except RegistrationError:
pass
def get_fields(self):
fields = super().get_fields()
if self.information_fields:
for key, field in self.information_fields.items():
key = 'fields[{}]'.format(key)
field_type = field['type']
if field_type == RegistrationInformationField.BOOLEAN_FIELD:
fields[key] = serializers.BooleanField(
required=False,
write_only=True
)
elif field_type == RegistrationInformationField.INTEGER_FIELD:
fields[key] = serializers.IntegerField(
required=field['required'],
write_only=True
)
elif field_type == RegistrationInformationField.TEXT_FIELD:
fields[key] = serializers.CharField(
required=field['required'],
write_only=True
)
fields[key].label = field['label']
fields[key].help_text = field['description']
fields[key].initial = field['value']
fields[key].default = field['value']
try:
if key in self.information_fields:
fields[key].initial = self.validated_data[key]
except AssertionError:
pass
return fields
def to_representation(self, instance):
data = super().to_representation(instance)
data['fields'] = self.information_fields
return data
def field_values(self):
return ((name[7:len(name) - 1], value)
for name, value in self.validated_data.items()
if "info_field" in name)
......@@ -4,4 +4,5 @@ from events.api import viewsets
router = routers.SimpleRouter()
router.register(r'events', viewsets.EventViewset)
router.register(r'registrations', viewsets.RegistrationViewSet)
urlpatterns = router.urls
......@@ -3,22 +3,26 @@ from datetime import datetime
from django.utils import timezone
from rest_framework import viewsets, filters
from rest_framework.decorators import list_route, detail_route
from rest_framework.exceptions import ParseError
from rest_framework.exceptions import ParseError, PermissionDenied, NotFound
from rest_framework.generics import get_object_or_404
from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin
from rest_framework.permissions import (
IsAuthenticated,
IsAdminUser,
IsAuthenticatedOrReadOnly
)
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from events import services
from events.api.permissions import UnpublishedEventPermissions
from events.api.serializers import (
EventCalenderJSSerializer,
UnpublishedEventSerializer,
EventRetrieveSerializer,
EventListSerializer,
RegistrationSerializer)
RegistrationListSerializer, RegistrationSerializer)
from events.exceptions import RegistrationError
from events.models import Event, Registration
......@@ -52,21 +56,28 @@ class EventViewset(viewsets.ReadOnlyModelViewSet):
def get_serializer_context(self):
return super().get_serializer_context()
@detail_route()
@detail_route(methods=['get', 'post'])
def registrations(self, request, pk):
event = get_object_or_404(Event, pk=pk)
if request.method.lower() == 'post':
try:
registration = services.create_registration(
request.user, event)
serializer = RegistrationSerializer(
instance=registration,
context={'request': request}
)
return Response(status=201, data=serializer.data)
except RegistrationError as e:
raise PermissionDenied(detail=e)
status = request.query_params.get('status', None)
# Make sure you can only access other registrations when you have
# the permissions to do so
if not request.user.has_perm('events.change_event'):
if not services.is_organiser(request.user, event):
status = 'registered'
elif (not request.user.is_superuser and
not request.user.has_perm('events.override_organiser')):
committees = request.user.member.get_committees().filter(
pk=event.organiser.pk).count()
if committees == 0:
status = 'registered'
queryset = Registration.objects.filter(event=pk)
if status is not None:
......@@ -80,8 +91,8 @@ class EventViewset(viewsets.ReadOnlyModelViewSet):
queryset = Registration.objects.filter(
event=pk, date_cancelled=None)[:event.max_participants]
serializer = RegistrationSerializer(queryset, many=True,
context={'request': request})
serializer = RegistrationListSerializer(queryset, many=True,
context={'request': request})
return Response(serializer.data)
......@@ -112,3 +123,49 @@ class EventViewset(viewsets.ReadOnlyModelViewSet):
serializer = UnpublishedEventSerializer(queryset, many=True,
context={'user': request.user})
return Response(serializer.data)
class RegistrationViewSet(GenericViewSet, RetrieveModelMixin,
UpdateModelMixin):
queryset = Registration.objects.all()
serializer_class = RegistrationSerializer
permission_classes = [IsAuthenticated]
def get_serializer_context(self):
context = super().get_serializer_context()
context['request'] = self.request
return context
def get_object(self):
instance = super().get_object()
if (instance.member.user.pk != self.request.user.pk and
not services.is_organiser(self.request.user,
instance.event)):
raise NotFound()
return instance
# Always set instance so that OPTIONS call will show the info fields too
def get_serializer(self, *args, **kwargs):
if len(args) == 0 and "instance" not in kwargs:
kwargs["instance"] = self.get_object()
return super().get_serializer(*args, **kwargs)
def perform_update(self, serializer):
super().perform_update(serializer)
registration = serializer.instance
services.update_registration(registration.member.user,
registration.event,
serializer.field_values())
serializer.information_fields = services.registration_fields(
registration.member.user, registration.event)
def destroy(self, request, pk=None, **kwargs):
registration = self.get_object()
try:
services.cancel_registration(request,
registration.member.user,
registration.event)