Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
thalia
concrexit
Commits
c78e77db
Verified
Commit
c78e77db
authored
May 27, 2018
by
Sébastiaan Versteeg
Browse files
Add docs to events package
parent
0d0affd0
Changes
17
Hide whitespace changes
Inline
Side-by-side
website/events/admin.py
View file @
c78e77db
# -*- 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
)
website/events/admin_views.py
View file @
c78e77db
...
...
@@ -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
:
...
...
website/events/api/permissions.py
View file @
c78e77db
...
...
@@ -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'
],
}
website/events/api/serializers.py
View file @
c78e77db
...
...
@@ -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
:
...
...
website/events/api/urls.py
View file @
c78e77db
"""Defines the API routes of the events package"""
from
rest_framework
import
routers
from
events.api
import
viewsets
...
...
website/events/api/viewsets.py
View file @
c78e77db
"""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
]
...
...
website/events/apps.py
View file @
c78e77db
"""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'
)
website/events/decorators.py
View file @
c78e77db
"""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
...
...
website/events/emails.py
View file @
c78e77db
"""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
...
...
website/events/exceptions.py
View file @
c78e77db
class
RegistrationError
(
Exception
):
"""Custom error for problems during registration"""
pass
website/events/feeds.py
View file @
c78e77db
"""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
...
...
website/events/forms.py
View file @
c78e77db
...
...
@@ -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
)
...
...
website/events/models.py
View file @
c78e77db
"""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
event
s
"""
"""
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
registration
s
"""
"""
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
):
"""
F
ield description to ask for when registering"""
"""
Describes a f
ield description to ask for when registering"""
BOOLEAN_FIELD
=
'boolean'
INTEGER_FIELD
=
'integer'
TEXT_FIELD
=
'text'
...
...
website/events/services.py
View file @
c78e77db
...
...
@@ -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
,
...
...
website/events/sitemaps.py
View file @
c78e77db
"""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
)
...
...
website/events/urls.py
View file @
c78e77db
"""
Events URL Configuration
"""
"""Routes defined by the events package"""
from
django.conf.urls
import
url
from
events
import
admin_views
...
...
website/events/views.py
View file @
c78e77db
"""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
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment