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

Add docs to events package

parent 0d0affd0
# -*- coding: utf-8 -*-
"""Registers admin interfaces for the events module"""
from django.contrib import admin
from django.http import HttpResponseRedirect
from django.template.defaultfilters import date as _date
......@@ -17,6 +18,7 @@ from . import forms, models
def _do_next(request, response):
"""See DoNextModelAdmin"""
if 'next' in request.GET and is_safe_url(request.GET['next']):
return HttpResponseRedirect(request.GET['next'])
else:
......@@ -40,6 +42,7 @@ class DoNextModelAdmin(TranslatedModelAdmin):
class RegistrationInformationFieldInline(admin.StackedInline):
"""The inline for registration information fields in the Event admin"""
form = forms.RegistrationInformationFieldForm
extra = 0
model = models.RegistrationInformationField
......@@ -56,6 +59,7 @@ class RegistrationInformationFieldInline(admin.StackedInline):
class PizzaEventInline(admin.StackedInline):
"""The inline for pizza events in the Event admin"""
model = PizzaEvent
extra = 0
max_num = 1
......@@ -63,6 +67,7 @@ class PizzaEventInline(admin.StackedInline):
@admin.register(models.Event)
class EventAdmin(DoNextModelAdmin):
"""Manage the events"""
inlines = (RegistrationInformationFieldInline, PizzaEventInline,)
fields = ('title', 'description', 'start', 'end', 'organiser', 'category',
'registration_start', 'registration_end', 'cancel_deadline',
......@@ -85,6 +90,7 @@ class EventAdmin(DoNextModelAdmin):
title=obj.title)
def has_change_permission(self, request, event=None):
"""Only allow access to the change form if the user is an organiser"""
if (event is not None and
not services.is_organiser(request.member, event)):
return False
......@@ -118,10 +124,12 @@ class EventAdmin(DoNextModelAdmin):
num_participants.short_description = _('Number of participants')
def make_published(self, request, queryset):
"""Action to change the status of the event"""
self._change_published(request, queryset, True)
make_published.short_description = _('Publish selected events')
def make_unpublished(self, request, queryset):
"""Action to change the status of the event"""
self._change_published(request, queryset, False)
make_unpublished.short_description = _('Unpublish selected events')
......@@ -149,6 +157,7 @@ class EventAdmin(DoNextModelAdmin):
form.instance.save()
def formfield_for_dbfield(self, db_field, request, **kwargs):
"""Customise formfield for organiser"""
field = super().formfield_for_dbfield(db_field, request, **kwargs)
if db_field.name == 'organiser':
# Disable add/change/delete buttons
......@@ -158,6 +167,7 @@ class EventAdmin(DoNextModelAdmin):
return field
def formfield_for_foreignkey(self, db_field, request, **kwargs):
"""Customise the organiser formfield, limit the options"""
if db_field.name == 'organiser':
# Use custom queryset for organiser field
# Only get the current active committees the user is a member of
......@@ -187,6 +197,7 @@ class RegistrationAdmin(DoNextModelAdmin):
"""Custom admin for registrations"""
def formfield_for_dbfield(self, db_field, request, **kwargs):
"""Customise the formfields of event and member"""
field = super().formfield_for_dbfield(db_field, request, **kwargs)
if db_field.name in ('event', 'member'):
# Disable add/change/delete buttons
......@@ -196,11 +207,13 @@ class RegistrationAdmin(DoNextModelAdmin):
return field
def formfield_for_foreignkey(self, db_field, request, **kwargs):
"""Customise the formfields of event and member"""
if db_field.name == 'event':
# allow to restrict event
if request.GET.get('event_pk'):
kwargs['queryset'] = models.Event.objects.filter(
pk=int(request.GET['event_pk']))
elif db_field.name == 'member':
# Filter the queryset to current members only
kwargs['queryset'] = Member.current_members.all()
return super().formfield_for_foreignkey(db_field, request, **kwargs)
......@@ -19,6 +19,12 @@ from .models import Event, Registration
@permission_required('events.change_event')
@organiser_only
def details(request, event_id):
"""
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
"""
event = get_object_or_404(Event, pk=event_id)
return render(request, 'events/admin/details.html', {
......@@ -31,6 +37,13 @@ def details(request, event_id):
@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
}
......@@ -57,6 +70,12 @@ def change_registration(request, event_id, action=None):
@staff_member_required
@permission_required('events.change_event')
def export(request, event_id):
"""
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
"""
event = get_object_or_404(Event, pk=event_id)
extra_fields = event.registrationinformationfield_set.all()
registrations = event.registration_set.all()
......@@ -140,6 +159,13 @@ def export(request, event_id):
@staff_member_required
@permission_required('events.change_event')
def export_email(request, event_id):
"""
Renders a page that outputs all email addresses of registered members
for an event
:param request: the request object
:param event_id: the primary key of the event
:return: HttpResponse 200 with the HTML of the page
"""
event = get_object_or_404(Event, pk=event_id)
registrations = event.registration_set.filter(
date_cancelled=None)
......@@ -155,6 +181,12 @@ def export_email(request, event_id):
@permission_required('events.change_event')
@organiser_only
def all_present(request, event_id):
"""
Mark all registrations of an event as present
: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)
if event.max_participants is None:
......
......@@ -2,6 +2,7 @@ from rest_framework import permissions
class UnpublishedEventPermissions(permissions.DjangoModelPermissions):
"""Custom permission for the unpublished events route"""
perms_map = {
'GET': ['%(app_label)s.add_%(model_name)s'],
}
......@@ -16,6 +16,9 @@ from thaliawebsite.templatetags.bleach_tags import bleach
class CalenderJSSerializer(serializers.ModelSerializer):
"""
Serializer using the right format for CalendarJS
"""
class Meta:
fields = (
'start', 'end', 'all_day', 'is_birthday',
......@@ -86,6 +89,9 @@ class EventCalenderJSSerializer(CalenderJSSerializer):
class UnpublishedEventSerializer(CalenderJSSerializer):
"""
See CalenderJSSerializer, customised colors
"""
class Meta(CalenderJSSerializer.Meta):
model = Event
......@@ -101,6 +107,9 @@ class UnpublishedEventSerializer(CalenderJSSerializer):
class EventRetrieveSerializer(serializers.ModelSerializer):
"""
Serializer for events
"""
class Meta:
model = Event
fields = ('pk', 'title', 'description', 'start', 'end', 'organiser',
......@@ -177,6 +186,7 @@ class EventRetrieveSerializer(serializers.ModelSerializer):
class EventListSerializer(serializers.ModelSerializer):
"""Custom list serializer for events"""
class Meta:
model = Event
fields = ('pk', 'title', 'description', 'start', 'end',
......@@ -202,6 +212,7 @@ class EventListSerializer(serializers.ModelSerializer):
class RegistrationListSerializer(serializers.ModelSerializer):
"""Custom registration list serializer"""
class Meta:
model = Registration
fields = ('pk', 'member', 'name', 'photo', 'avatar', 'registered_on',
......@@ -258,6 +269,7 @@ class RegistrationListSerializer(serializers.ModelSerializer):
class RegistrationSerializer(serializers.ModelSerializer):
"""Registration serializer"""
information_fields = None
class Meta:
......
"""Defines the API routes of the events package"""
from rest_framework import routers
from events.api import viewsets
......
"""Defines the viewsets of the events package"""
from datetime import datetime
from django.utils import timezone
......@@ -28,12 +29,14 @@ from events.models import Event, Registration
def _extract_date(param):
"""Extract the date from an arbitrary string"""
if param is None:
return None
return timezone.make_aware(datetime.strptime(param, '%Y-%m-%d'))
def _extract_date_range(request):
"""Extract a date range from an arbitrary string"""
try:
start = _extract_date(request.query_params['start'])
end = _extract_date(request.query_params['end'])
......@@ -43,6 +46,10 @@ def _extract_date_range(request):
class EventViewset(viewsets.ReadOnlyModelViewSet):
"""
Defines the viewset for events, requires an authenticated user
and enables ordering on the event start/end.
"""
queryset = Event.objects.filter(published=True)
permission_classes = [IsAuthenticated]
filter_backends = (filters.OrderingFilter,)
......@@ -84,6 +91,13 @@ class EventViewset(viewsets.ReadOnlyModelViewSet):
@detail_route(methods=['get', 'post'])
def registrations(self, request, pk):
"""
Defines a custom route for the event's registrations,
can filter on registration status if the user is an organiser
:param request: the request object
:param pk: the primary key of the event
:return: the registrations of the event
"""
event = get_object_or_404(Event, pk=pk)
if request.method.lower() == 'post':
......@@ -124,6 +138,12 @@ class EventViewset(viewsets.ReadOnlyModelViewSet):
@list_route(permission_classes=(IsAuthenticatedOrReadOnly,))
def calendarjs(self, request):
"""
Defines a custom route that outputs the correctly formatted
events information for CalendarJS, published events only
:param request: the request object
:return: response containing the data
"""
end, start = _extract_date_range(request)
queryset = Event.objects.filter(
......@@ -138,6 +158,12 @@ class EventViewset(viewsets.ReadOnlyModelViewSet):
@list_route(permission_classes=(IsAdminUser, UnpublishedEventPermissions,))
def unpublished(self, request):
"""
Defines a custom route that outputs the correctly formatted
events information for CalendarJS, unpublished events only
:param request: the request object
:return: response containing the data
"""
end, start = _extract_date_range(request)
queryset = Event.objects.filter(
......@@ -153,6 +179,10 @@ class EventViewset(viewsets.ReadOnlyModelViewSet):
class RegistrationViewSet(GenericViewSet, RetrieveModelMixin,
UpdateModelMixin):
"""
Defines the viewset for registrations, requires an authenticated user.
Has custom update and destroy methods that use the services.
"""
queryset = Registration.objects.all()
serializer_class = RegistrationSerializer
permission_classes = [IsAuthenticated]
......
"""Configuration for the events package"""
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class EventsConfig(AppConfig):
"""AppConfig for the events package"""
name = 'events'
verbose_name = _('Events')
"""The decorators defined by the events package"""
from django.core.exceptions import PermissionDenied
from events import services
......@@ -5,10 +6,17 @@ from events.models import Event
def organiser_only(view_function):
"""See OrganiserOnly"""
return OrganiserOnly(view_function)
class OrganiserOnly(object):
"""
Decorator that denies access to the page if:
1. There is no `event_id` in the request
2. The specified event does not exist
3. The user is no organiser of the specified event
"""
def __init__(self, view_function):
self.view_function = view_function
......
"""The emails defined by the events package"""
from django.core.mail import EmailMessage
from django.template.loader import get_template
from django.utils import translation
......@@ -9,6 +10,12 @@ from thaliawebsite.templatetags import baseurl
def notify_first_waiting(request, event):
"""
Send an email to the first person on the waiting list
when someone cancels their registration
:param request: the request object
:param event: the event
"""
if (event.max_participants is not None and
Registration.objects
.filter(event=event, date_cancelled=None)
......@@ -45,6 +52,12 @@ def notify_first_waiting(request, event):
def notify_organiser(event, registration):
"""
Send an email to the organiser of the event if
someone cancels their registration
:param event: the event
:param registration: the registration that was cancelled
"""
if event.organiser is None or event.organiser.contact_mailinglist is None:
return
......
class RegistrationError(Exception):
"""Custom error for problems during registration"""
pass
"""The feeds defined by the events package"""
from django.contrib.sites.models import Site
from django.urls import reverse
from django.utils.translation import ugettext as _
......@@ -8,6 +9,7 @@ from events.models import Event
class EventFeed(ICalFeed):
"""Output an iCal feed containing all published events"""
def __init__(self, lang='en'):
super().__init__()
self.lang = lang
......
......@@ -5,6 +5,10 @@ from .models import RegistrationInformationField, Event
class RegistrationInformationFieldForm(forms.ModelForm):
"""
Custom form for the registration information fields
that adds an order field
"""
order = forms.IntegerField(label=_('order'), initial=0)
def __init__(self, *args, **kwargs):
......@@ -26,6 +30,7 @@ class RegistrationInformationFieldForm(forms.ModelForm):
class FieldsForm(forms.Form):
"""Form that outputs the correct widgets for the information fields"""
def __init__(self, *args, **kwargs):
self.information_fields = kwargs.pop('fields')
super(FieldsForm, self).__init__(*args, **kwargs)
......
"""The models defined by the events package"""
from django.conf import settings
from django.core import validators
from django.core.exceptions import ValidationError, ObjectDoesNotExist
......@@ -13,7 +14,7 @@ from utils.translation import ModelTranslateMeta, MultilingualField
class Event(models.Model, metaclass=ModelTranslateMeta):
"""Represents events"""
"""Describes an event"""
EVENT_CATEGORIES = (
('drinks', _('Drinks')),
......@@ -281,12 +282,13 @@ class Event(models.Model, metaclass=ModelTranslateMeta):
def registration_member_choices_limit():
"""Defines queryset filters to only include current members"""
return (Q(membership__until__isnull=True) |
Q(membership__until__gt=timezone.now().date()))
class Registration(models.Model):
"""Event registrations"""
"""Describes a registration for an Event"""
PAYMENT_CARD = 'card_payment'
PAYMENT_CASH = 'cash_payment'
......@@ -404,7 +406,7 @@ class Registration(models.Model):
class RegistrationInformationField(models.Model, metaclass=ModelTranslateMeta):
"""Field description to ask for when registering"""
"""Describes a field description to ask for when registering"""
BOOLEAN_FIELD = 'boolean'
INTEGER_FIELD = 'integer'
TEXT_FIELD = 'text'
......
......@@ -9,6 +9,12 @@ from events.models import Registration, RegistrationInformationField
def is_user_registered(member, event):
"""
Returns if the user is registered for the specified event
:param member: the user
:param event: the event
:return: None if registration is not required or no member else True/False
"""
if not event.registration_required or not member.is_authenticated:
return None
......@@ -18,6 +24,12 @@ def is_user_registered(member, event):
def event_permissions(member, event):
"""
Returns a dictionary with the available event permissions of the user
:param member: the user
:param event: the event
:return: the permission dictionary
"""
perms = {
"create_registration": False,
"cancel_registration": False,
......@@ -64,6 +76,12 @@ def is_organiser(member, event):
def create_registration(member, event):
"""
Creates a new user registration for an event
:param member: the user
:param event: the event
:return: returns the registration if successful
"""
if event_permissions(member, event)["create_registration"]:
registration = None
try:
......@@ -97,6 +115,12 @@ def create_registration(member, event):
def cancel_registration(request, member, event):
"""
Cancel a user registration for an event
:param request: the request object
:param member: the user
:param event: the event
"""
registration = None
try:
registration = Registration.objects.get(
......@@ -126,6 +150,12 @@ def cancel_registration(request, member, event):
def update_registration(member, event, field_values):
"""
Updates a user registration of an event
:param member: the user
:param event: the event
:param field_values: values for the information fields
"""
registration = None
try:
registration = Registration.objects.get(
......@@ -159,7 +189,12 @@ def update_registration(member, event, field_values):
def registration_fields(member, event):
registration = None
"""
Returns information about the registration fields of a registration
:param member: the user
:param event: the event
:return: the fields
"""
try:
registration = Registration.objects.get(
event=event,
......
"""The sitemaps defined by the events package"""
from django.contrib import sitemaps
from django.urls import reverse
......@@ -5,6 +6,7 @@ from . import models
class StaticViewSitemap(sitemaps.Sitemap):
"""Sitemap of the static event pages"""
changefreq = 'daily'
def items(self):
......@@ -15,6 +17,7 @@ class StaticViewSitemap(sitemaps.Sitemap):
class EventSitemap(sitemaps.Sitemap):
"""Sitemap of the event detail pages"""
def items(self):
return models.Event.objects.filter(published=True)
......
"""
Events URL Configuration
"""
"""Routes defined by the events package"""
from django.conf.urls import url
from events import admin_views
......
"""Views provided by the events package"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect
......@@ -14,6 +15,9 @@ from .models import Event, Registration
class EventIndex(TemplateView):
"""
Renders the events calendar overview
"""
template_name = 'events/index.html'
def get_context_data(self, **kwargs):
......@@ -29,6 +33,9 @@ class EventIndex(TemplateView):
class EventDetail(DetailView):
"""
Renders a single event detail page
"""
model = Event
queryset = Event.objects.filter(published=True)
template_name = 'events/event.html'
......@@ -61,6 +68,10 @@ class EventDetail(DetailView):
@method_decorator(login_required, name='dispatch')
class EventRegisterView(View):
"""
Defines a view that allows the user to register for an event using a POST
request. The user should be authenticated.
"""
def get(self, request, *args, **kwargs):
return redirect('events:event', pk=kwargs['pk'])
......@@ -81,6 +92,10 @@ class EventRegisterView(View):
@method_decorator(login_required, name='dispatch')
class EventCancelView(View):
"""
Defines a view that allows the user to cancel their event registration
using a POSt request. The user should be authenticated.
"""
def get(self, request, *args, **kwargs):
return redirect('events:event', pk=kwargs['pk'])
......@@ -98,6 +113,10 @@ class EventCancelView(View):
@method_decorator(login_required, name='dispatch')
class RegistrationView(FormView):
"""
Renders a form that allows the user to change the details of their
registration. The user should be authenticated.
"""
form_class = FieldsForm
template_name = 'events/registration.html'
event = None
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment