diff --git a/requirements.txt b/requirements.txt
index e5e0266ab3808de02bad6b210a0f2fc4072b810a..875459cd7fc6fcb4152c0a7469fd56f66c3e4e86 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,3 +6,4 @@ django-sendfile==0.3.10
django-template-check # This should be in dev-requirements somehow
bleach==1.4.3
django-tinymce==2.3.0
+pytz
diff --git a/tox.ini b/tox.ini
index e9f8d36df2f3801d6f5467fda550f7c4040bc88e..a0b3e80b6cfe7c095756604265c6e151d5bd6b26 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,7 +12,7 @@ commands =
deps = -r{toxinidir}/requirements.txt
[flake8]
-exclude = */migrations/*, */urls.py, */.ropeproject/*
+exclude = */migrations/*, */urls.py, .ropeproject
[testenv:flake8]
deps= flake8==3.0.2
diff --git a/website/events/__init__.py b/website/events/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/website/events/admin.py b/website/events/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..26a21ac119ec4f9ed310c08a06da6bb0edf4db16
--- /dev/null
+++ b/website/events/admin.py
@@ -0,0 +1,144 @@
+# -*- coding: utf-8 -*-
+from django import forms
+from django.core.urlresolvers import reverse
+from django.contrib import admin
+from django.http import HttpResponseRedirect
+from django.utils import timezone
+from django.utils.http import is_safe_url
+from django.utils.html import format_html
+from django.utils.translation import ugettext_lazy as _
+
+from committees.models import Committee
+from members.models import Member
+from . import models
+
+
+def _do_next(request, response):
+ if 'next' in request.GET and is_safe_url(request.GET['next']):
+ return HttpResponseRedirect(request.GET['next'])
+ else:
+ return response
+
+
+class DoNextModelAdmin(admin.ModelAdmin):
+
+ def response_add(self, request, obj):
+ res = super().response_add(request, obj)
+ return _do_next(request, res)
+
+ def response_change(self, request, obj):
+ res = super().response_change(request, obj)
+ return _do_next(request, res)
+
+
+class RegistrationInformationFieldForm(forms.ModelForm):
+
+ order = forms.IntegerField(label=_('order'), initial=0)
+
+ class Meta:
+ fields = '__all__'
+ model = models.RegistrationInformationField
+
+
+class RegistrationInformationFieldInline(admin.StackedInline):
+ form = RegistrationInformationFieldForm
+ extra = 0
+ model = models.RegistrationInformationField
+ ordering = ('_order',)
+
+ radio_fields = {'type': admin.VERTICAL}
+
+
+@admin.register(models.Event)
+class EventAdmin(DoNextModelAdmin):
+ inlines = (RegistrationInformationFieldInline,)
+ list_display = ('overview_link', 'start', 'registration_start',
+ 'num_participants', 'organiser', 'published', 'edit_link')
+ list_display_links = ('edit_link',)
+ list_filter = ('start', 'published')
+ actions = ('make_published', 'make_unpublished')
+ date_hierarchy = 'start'
+ search_fields = ('title', 'description')
+ prepopulated_fields = {'map_location': ('location',)}
+
+ def overview_link(self, obj):
+ return format_html('{title}',
+ link=reverse('events:admin-details',
+ kwargs={'event_id': obj.pk}),
+ title=obj.title)
+
+ def edit_link(self, obj):
+ return _('Edit')
+ edit_link.short_description = ''
+
+ def num_participants(self, obj):
+ """Pretty-print the number of participants"""
+ num = (obj.registration_set
+ .filter(date_cancelled__lt=timezone.now()).count())
+ if not obj.max_participants:
+ return '{}/∞'.format(num)
+ return '{}/{}'.format(num, obj.max_participants)
+ num_participants.short_description = _('Number of participants')
+
+ def make_published(self, request, queryset):
+ queryset.update(published=True)
+ make_published.short_description = _('Publish selected events')
+
+ def make_unpublished(self, request, queryset):
+ queryset.update(published=False)
+ make_unpublished.short_description = _('Unpublish selected events')
+
+ def save_formset(self, request, form, formset, change):
+ """Save formsets with their order"""
+ formset.save()
+
+ form.instance.set_registrationinformationfield_order([
+ f.instance.pk
+ for f in sorted(formset.forms,
+ key=lambda x: (x.cleaned_data['order'],
+ x.instance.pk))
+ ])
+ form.instance.save()
+
+ def formfield_for_dbfield(self, db_field, request, **kwargs):
+ field = super().formfield_for_dbfield(db_field, request, **kwargs)
+ if db_field.name == 'organiser':
+ # Disable add/change/delete buttons
+ field.widget.can_add_related = False
+ field.widget.can_change_related = False
+ field.widget.can_delete_related = False
+ return field
+
+ def formfield_for_foreignkey(self, db_field, request, **kwargs):
+ if db_field.name == 'organiser':
+ # Use custom queryset for organiser field
+ try:
+ if not request.user.is_superuser:
+ member = request.user.member
+ kwargs['queryset'] = Committee.active_committees.filter(
+ members=member)
+ except Member.DoesNotExist:
+ pass
+ return super().formfield_for_foreignkey(db_field, request, **kwargs)
+
+
+@admin.register(models.Registration)
+class RegistrationAdmin(DoNextModelAdmin):
+ """Custom admin for registrations"""
+
+ def formfield_for_dbfield(self, db_field, request, **kwargs):
+ field = super().formfield_for_dbfield(db_field, request, **kwargs)
+ if db_field.name in ('event', 'member'):
+ # Disable add/change/delete buttons
+ field.widget.can_add_related = False
+ field.widget.can_change_related = False
+ field.widget.can_delete_related = False
+ return field
+
+ def formfield_for_foreignkey(self, db_field, request, **kwargs):
+ 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']))
+ return super().formfield_for_foreignkey(db_field, request, **kwargs)
diff --git a/website/events/apps.py b/website/events/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..3854644330068da846bc752b7298469f5417d87d
--- /dev/null
+++ b/website/events/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class EventsConfig(AppConfig):
+ name = 'events'
diff --git a/website/events/migrations/0001_initial.py b/website/events/migrations/0001_initial.py
new file mode 100644
index 0000000000000000000000000000000000000000..7d4c9b7b3e9ddaa572a45d4e63a7723e03e0a961
--- /dev/null
+++ b/website/events/migrations/0001_initial.py
@@ -0,0 +1,133 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10 on 2016-08-13 13:33
+from __future__ import unicode_literals
+
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('members', '0004_auto_20160805_1435'),
+ ('committees', '0004_auto_20160727_2253'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='BooleanRegistrationInformation',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('changed', models.DateTimeField(auto_now=True, verbose_name='last changed')),
+ ('value', models.BooleanField()),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='Event',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=100, verbose_name='title')),
+ ('description', models.TextField(verbose_name='description')),
+ ('start', models.DateTimeField(verbose_name='start time')),
+ ('end', models.DateTimeField(verbose_name='end time')),
+ ('registration_start', models.DateTimeField(blank=True, null=True, verbose_name='registration start')),
+ ('registration_end', models.DateTimeField(blank=True, null=True, verbose_name='registration end')),
+ ('location', models.CharField(max_length=255, verbose_name='location')),
+ ('map_location', models.CharField(help_text='Location of Huygens: Heyendaalseweg 135, Nijmegen. Not shown as text!!', max_length=255, verbose_name='location for minimap')),
+ ('price', models.DecimalField(decimal_places=2, default=0, max_digits=5, validators=[django.core.validators.MinValueValidator(0)], verbose_name='price')),
+ ('cost', models.DecimalField(decimal_places=2, default=0, help_text='Actual cost of event.', max_digits=5, validators=[django.core.validators.MinValueValidator(0)], verbose_name='cost')),
+ ('max_participants', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='maximum number of participants')),
+ ('registration_required', models.BooleanField(default=False, verbose_name='registration required')),
+ ('no_registration_message', models.CharField(blank=True, help_text='Default: No registration required', max_length=200, null=True, verbose_name='message when there is no registration')),
+ ('published', models.BooleanField(default=False, verbose_name='published')),
+ ('organiser', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='committees.Committee')),
+ ],
+ options={
+ 'ordering': ('-start',),
+ },
+ ),
+ migrations.CreateModel(
+ name='IntegerRegistrationInformation',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('changed', models.DateTimeField(auto_now=True, verbose_name='last changed')),
+ ('value', models.IntegerField()),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='Registration',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(blank=True, help_text='Use this for non-members', max_length=50, null=True, verbose_name='name')),
+ ('date', models.DateTimeField(auto_now_add=True, verbose_name='registration date')),
+ ('date_cancelled', models.DateTimeField(blank=True, null=True, verbose_name='cancellation date')),
+ ('present', models.BooleanField(default=False, verbose_name='Present')),
+ ('paid', models.BooleanField(default=False, verbose_name='Paid')),
+ ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Event')),
+ ('member', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='members.Member')),
+ ],
+ options={
+ 'ordering': ('date',),
+ },
+ ),
+ migrations.CreateModel(
+ name='RegistrationInformationField',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('type', models.CharField(choices=[('checkbox', 'checkbox'), ('charfield', 'text field'), ('intfield', 'integer field')], max_length=10, verbose_name='field type')),
+ ('name', models.CharField(max_length=100, verbose_name='field name')),
+ ('description', models.TextField(blank=True, null=True, verbose_name='description')),
+ ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Event')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='TextRegistrationInformation',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('changed', models.DateTimeField(auto_now=True, verbose_name='last changed')),
+ ('value', models.TextField()),
+ ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.RegistrationInformationField')),
+ ('registration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Registration')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.AddField(
+ model_name='integerregistrationinformation',
+ name='field',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.RegistrationInformationField'),
+ ),
+ migrations.AddField(
+ model_name='integerregistrationinformation',
+ name='registration',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Registration'),
+ ),
+ migrations.AddField(
+ model_name='booleanregistrationinformation',
+ name='field',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.RegistrationInformationField'),
+ ),
+ migrations.AddField(
+ model_name='booleanregistrationinformation',
+ name='registration',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Registration'),
+ ),
+ migrations.AlterOrderWithRespectTo(
+ name='registrationinformationfield',
+ order_with_respect_to='event',
+ ),
+ migrations.AlterUniqueTogether(
+ name='registration',
+ unique_together=set([('member', 'event', 'name', 'date_cancelled')]),
+ ),
+ ]
diff --git a/website/events/migrations/0002_auto_20160813_1620.py b/website/events/migrations/0002_auto_20160813_1620.py
new file mode 100644
index 0000000000000000000000000000000000000000..5326b4d482957608ff188929f55af3af28a81e2e
--- /dev/null
+++ b/website/events/migrations/0002_auto_20160813_1620.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10 on 2016-08-13 14:20
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('events', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='event',
+ name='organiser',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='committees.Committee'),
+ ),
+ ]
diff --git a/website/events/migrations/0003_remove_event_registration_required.py b/website/events/migrations/0003_remove_event_registration_required.py
new file mode 100644
index 0000000000000000000000000000000000000000..c3375e218fdf00ffafe121ecdd891d2471925c37
--- /dev/null
+++ b/website/events/migrations/0003_remove_event_registration_required.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10 on 2016-08-13 14:21
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('events', '0002_auto_20160813_1620'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='event',
+ name='registration_required',
+ ),
+ ]
diff --git a/website/events/migrations/__init__.py b/website/events/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/website/events/models.py b/website/events/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..38fa5504f050391180c3eb122e5be1ae65ed5e3f
--- /dev/null
+++ b/website/events/models.py
@@ -0,0 +1,263 @@
+from django.core import validators
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.db.models import Q
+from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _
+
+
+class Event(models.Model):
+ """Represents events"""
+
+ DEFAULT_NO_REGISTRATION_MESSAGE = _('No registration required')
+
+ title = models.CharField(_("title"), max_length=100)
+
+ description = models.TextField(_("description"))
+
+ start = models.DateTimeField(_("start time"))
+
+ end = models.DateTimeField(_("end time"))
+
+ organiser = models.ForeignKey(
+ 'committees.Committee',
+ models.SET_NULL,
+ null=True,
+ )
+
+ registration_start = models.DateTimeField(
+ _("registration start"),
+ null=True,
+ blank=True,
+ )
+
+ registration_end = models.DateTimeField(
+ _("registration end"),
+ null=True,
+ blank=True
+ )
+
+ location = models.CharField(_("location"), max_length=255)
+
+ map_location = models.CharField(
+ _("location for minimap"),
+ max_length=255,
+ help_text=_('Location of Huygens: Heyendaalseweg 135, Nijmegen. '
+ 'Not shown as text!!'),
+ )
+
+ price = models.DecimalField(
+ _("price"),
+ max_digits=5,
+ decimal_places=2,
+ default=0,
+ validators=[validators.MinValueValidator(0)],
+ )
+
+ cost = models.DecimalField(
+ _("cost"),
+ max_digits=5,
+ decimal_places=2,
+ default=0,
+ help_text=_("Actual cost of event."),
+ validators=[validators.MinValueValidator(0)],
+ )
+
+ max_participants = models.PositiveSmallIntegerField(
+ _('maximum number of participants'),
+ blank=True,
+ null=True,
+ )
+
+ no_registration_message = models.CharField(
+ _('message when there is no registration'),
+ max_length=200,
+ blank=True,
+ null=True,
+ help_text=(_("Default: {}").format(DEFAULT_NO_REGISTRATION_MESSAGE)),
+ )
+
+ published = models.BooleanField(_("published"), default=False)
+
+ def registration_required(self):
+ return bool(self.registration_start) or bool(self.registration_end)
+
+ def clean(self):
+ super().clean()
+ errors = {}
+ if self.end < self.start:
+ errors.update({
+ 'end': _("Can't have an event travel back in time")})
+ if self.registration_required():
+ if self.no_registration_message:
+ errors.update(
+ {'no_registration_message': _(
+ "Doesn't make sense to have this if you require "
+ "registrations.")})
+ if not self.registration_start:
+ errors.update(
+ {'registration_start': _(
+ "If registration is required, you need a start of "
+ "registration")})
+ if not self.registration_end:
+ errors.update(
+ {'registration_end': _(
+ "If registration is required, you need an end of "
+ "registration")})
+ if self.registration_start and self.registration_end and (
+ self.registration_start >= self.registration_end):
+ message = _('Registration start should be before '
+ 'registration end')
+ errors.update({
+ 'registration_start': message,
+ 'registration_end': message})
+ if errors:
+ raise ValidationError(errors)
+
+ def get_absolute_url(self):
+ return ''
+
+ def __str__(self):
+ return '{}: {}'.format(
+ self.title,
+ timezone.localtime(self.start).strftime('%Y-%m-%d %H:%M'))
+
+ class Meta:
+ ordering = ('-start',)
+
+
+class RegistrationInformationField(models.Model):
+ """Field description to ask for when registering"""
+ FIELD_TYPES = (('checkbox', _('checkbox')),
+ ('charfield', _('text field')),
+ ('intfield', _('integer field')))
+
+ event = models.ForeignKey(Event, models.CASCADE)
+
+ type = models.CharField(
+ _('field type'),
+ choices=FIELD_TYPES,
+ max_length=10,
+ )
+
+ name = models.CharField(
+ _('field name'),
+ max_length=100,
+ )
+
+ description = models.TextField(
+ _('description'),
+ null=True,
+ blank=True,
+ )
+
+ def get_value_for(self, registration):
+ if self.type == 'charfield':
+ value_set = self.textregistrationinformation_set
+ elif self.type == 'checkbox':
+ value_set = self.booleanregistrationinformation_set
+ elif value_set == 'intfield':
+ value_set = self.integerregistrationinformation_set
+ try:
+ return value_set.get(registration=registration).value
+ except (TextRegistrationInformation.DoesNotExist,
+ BooleanRegistrationInformation.DoesNotExist,
+ IntegerRegistrationInformation.DoesNotExist):
+ return None
+
+ class Meta:
+ order_with_respect_to = 'event'
+
+
+class Registration(models.Model):
+ """Event registrations"""
+
+ event = models.ForeignKey(Event, models.CASCADE)
+
+ member = models.ForeignKey(
+ 'members.Member', models.CASCADE,
+ blank=True,
+ null=True,
+ limit_choices_to=(Q(user__membership__until__isnull=True) |
+ Q(user__membership__until__gt=timezone.now()))
+ )
+
+ name = models.CharField(
+ _('name'),
+ max_length=50,
+ help_text=_('Use this for non-members'),
+ null=True,
+ blank=True
+ )
+
+ date = models.DateTimeField(_('registration date'), auto_now_add=True)
+ date_cancelled = models.DateTimeField(_('cancellation date'),
+ null=True,
+ blank=True)
+
+ present = models.BooleanField(
+ _('Present'),
+ default=False,
+ )
+ paid = models.BooleanField(
+ _('Paid'),
+ default=False,
+ )
+
+ def registration_information(self):
+ fields = self.event.registrationinformationfield_set.all()
+ return [{'field': field, 'value': field.get_value_for(self)}
+ for field in fields]
+
+ def clean(self):
+ if ((self.member is None and not self.name) or
+ (self.member and self.name)):
+ raise ValidationError({
+ 'member': _('Either specify a member or a name'),
+ 'name': _('Either specify a member or a name'),
+ })
+
+ def validate_unique(self, exclude=None):
+ super().validate_unique(exclude)
+
+ def __str__(self):
+ if self.member:
+ return '{}: {}'.format(self.member.get_full_name(), self.event)
+ else:
+ return '{}: {}'.format(self.name, self.event)
+
+ class Meta:
+ ordering = ('date',)
+ unique_together = (('member', 'event', 'name', 'date_cancelled'),)
+
+
+class AbstractRegistrationInformation(models.Model):
+ """Abstract to contain common things for registration information"""
+
+ registration = models.ForeignKey(Registration, models.CASCADE)
+ field = models.ForeignKey(RegistrationInformationField, models.CASCADE)
+ changed = models.DateTimeField(_('last changed'), auto_now=True)
+
+ def __str__(self):
+ return '{} - {}: {}'.format(self.registration, self.field, self.value)
+
+ class Meta:
+ abstract = True
+
+
+class BooleanRegistrationInformation(AbstractRegistrationInformation):
+ """Checkbox information filled in by members when registring"""
+
+ value = models.BooleanField()
+
+
+class TextRegistrationInformation(AbstractRegistrationInformation):
+ """Checkbox information filled in by members when registring"""
+
+ value = models.TextField()
+
+
+class IntegerRegistrationInformation(AbstractRegistrationInformation):
+ """Checkbox information filled in by members when registring"""
+
+ value = models.IntegerField()
diff --git a/website/events/templates/events/admin/details.html b/website/events/templates/events/admin/details.html
new file mode 100644
index 0000000000000000000000000000000000000000..9fc02cb9f2cabe966f78d87e0f180076077903c1
--- /dev/null
+++ b/website/events/templates/events/admin/details.html
@@ -0,0 +1,68 @@
+{% extends 'admin/index.html' %}
+{% load i18n admin_urls static admin_modify %}
+
+{% block title %}{{ event.title }}{{ block.super }}{% endblock %}
+
+{% block breadcrumbs %}
+
+{% endblock %}
+
+{% block content_title %}{% blocktrans with event=event.title %}Event overview: {{ event }}{% endblocktrans %}
{% endblock %}
+{% block content %}
+
+
+ {% with event.registrationinformationfield_set.all as fields %}
+
{% trans "registrations"|capfirst %}
+ {% include 'events/admin/registrations_table.html' with registrations=registrations %}
+
+ {% if waiting %}
+ {% trans "waiting"|capfirst %}
+ {% include 'events/admin/registrations_table.html' with registrations=waiting verb='queued' addlink=False %}
+
+ {% endif %}
+ {% trans "cancellations"|capfirst %}
+ {% include 'events/admin/registrations_table.html' with registrations=cancellations verb='cancelled' addlink=False %}
+ {% endwith %}
+
+
+{% endblock %}
+
+{% block sidebar %}
+
+
+
+
+ - {% trans "title"|capfirst %}
+ - {{ event.title }}
+ - {% trans "date"|capfirst %}
+ - {{ event.start }}
— {{ event.end }}
+ - {% trans "organiser"|capfirst %}
+ - {{ event.organiser }}
+ - {% trans "registration period"|capfirst %}
+ - {{ event.registration_start }}
— {{ event.registration_end }}
+ - {% trans "location"|capfirst %}
+ - {{ event.location }} ({{ event.map_location }})
+ - {% trans "price"|capfirst %}
+ - {{ event.price }}
+ - {% trans "cost"|capfirst %}
+ - {{ event.cost }}
+ - {% trans "registration required"|capfirst %}
+ - {{ event.registration_required|yesno }}
+ {% if not event.registration_required %}
+ - {% trans "registration message"|capfirst %}
+ - {{ event.registration_message|default:event.DEFAULT_NO_REGISTRATION_MESSAGE }}
+ {% endif %}
+
+
+
+{% endblock %}
diff --git a/website/events/templates/events/admin/registrations_table.html b/website/events/templates/events/admin/registrations_table.html
new file mode 100644
index 0000000000000000000000000000000000000000..7dced333090b123cf65d88c89ab3092d0a5f2d50
--- /dev/null
+++ b/website/events/templates/events/admin/registrations_table.html
@@ -0,0 +1,55 @@
+{% load i18n %}
+
+
+
+
+ {% trans "name"|capfirst %} |
+ {% trans "date"|capfirst %} |
+ {% for field in fields %}
+ {{ field.name }} |
+ {% endfor %}
+ {% trans "present"|capfirst %} |
+ {% trans "paid"|capfirst %} |
+
+ {% if addlink != 0 %}
+ {% trans "add"|capfirst %} |
+ {% endif %}
+
+
+
+ {% for registration in registrations %}
+
+ {% if registration.member %}
+ {{ registration.member.get_full_name }} |
+ {% else %}
+ {{ registration.name }} |
+ {% endif %}
+ {{ registration.date }} |
+ {% for field in registration.registration_information %}
+ {% if not field.value %}
+ |
+ {% elif field.field.type == 'checkbox' %}
+ {{ field.value|yesno }} |
+ {% else %}
+ {{ field.value }} |
+ {% endif %}
+ {% endfor %}
+ {{ registration.present|yesno }} |
+ {{ registration.paid|yesno }} |
+ {% trans "change" %} |
+
+ {% empty %}
+
+ {% blocktrans with verb=verb|default:'registered' %}Nobody {{ verb }} yet{% endblocktrans %} |
+ {% for field in fields %}
+ |
+ {% endfor %}
+ |
+ |
+ |
+ |
+
+ {% endfor %}
+
+
+
diff --git a/website/events/tests.py b/website/events/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..1b81c5b8f9340fcd0e21ba3351e303ab16284cdb
--- /dev/null
+++ b/website/events/tests.py
@@ -0,0 +1,38 @@
+import datetime
+
+from django.core.exceptions import ValidationError
+from django.test import TestCase
+from django.utils import timezone
+
+from members.models import Member
+
+from events.models import Event, Registration
+
+
+class RegistrationTest(TestCase):
+ """Tests event registrations"""
+
+ fixtures = ['members.json']
+
+ def setUp(self):
+ self.event = Event.objects.create(
+ title='testevent',
+ description='desc',
+ start=timezone.now(),
+ end=(timezone.now() + datetime.timedelta(hours=1)),
+ location='test location',
+ map_location='test map location',
+ price=0.00,
+ cost=0.00)
+ self.member = Member.objects.all()[0]
+
+ def test_registration_either_name_or_member(self):
+ r1 = Registration.objects.create(event=self.event, member=self.member)
+ r1.clean()
+ r2 = Registration.objects.create(event=self.event, name='test name')
+ r2.clean()
+ with self.assertRaises(ValidationError):
+ r3 = Registration.objects.create(event=self.event,
+ name='test name',
+ member=self.member)
+ r3.clean()
diff --git a/website/events/urls.py b/website/events/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..efbda5164727aa0413dbc6090c79524361ea3204
--- /dev/null
+++ b/website/events/urls.py
@@ -0,0 +1,11 @@
+"""
+Events URL Configuration
+"""
+
+from django.conf.urls import url
+
+from . import views
+
+urlpatterns = [
+ url(r'admin/(?P\d+)/$', views.admin_details, name='admin-details'),
+]
diff --git a/website/events/views.py b/website/events/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..95e8205fd56015a56ce7bf58540ff33d846ba3e2
--- /dev/null
+++ b/website/events/views.py
@@ -0,0 +1,20 @@
+from django.shortcuts import render, get_object_or_404
+from django.contrib.admin.views.decorators import staff_member_required
+from django.contrib.auth.decorators import permission_required
+
+from .models import Event
+
+
+@staff_member_required
+@permission_required('events.change_event')
+def admin_details(request, event_id):
+ event = get_object_or_404(Event, pk=event_id)
+ n = event.max_participants
+ registrations = list(event.registration_set.filter(date_cancelled=None))
+ cancellations = event.registration_set.exclude(date_cancelled=None)
+ return render(request, 'events/admin/details.html', {
+ 'event': event,
+ 'registrations': registrations[:n],
+ 'waiting': registrations[n:] if n else [],
+ 'cancellations': cancellations,
+ })
diff --git a/website/members/models.py b/website/members/models.py
index 6baf37c0914d2bfaf3f52975c5781c2995cbae00..d501a5bac956c5ea0aa41ef06b7661d2758d4528 100644
--- a/website/members/models.py
+++ b/website/members/models.py
@@ -252,9 +252,12 @@ class Member(models.Model):
return "'{}' {}".format(self.nickname,
self.user.last_name)
else:
- return self.user.get_full_name()
+ return self.get_full_name()
display_name.short_description = _('Display name')
+ def get_full_name(self):
+ return self.user.get_full_name()
+
def __str__(self):
return self.display_name()
diff --git a/website/thaliawebsite/settings/settings.py b/website/thaliawebsite/settings/settings.py
index 94e47e1fc6da206fd4f4f1ff1f407f7eda7cd092..80870942d1a52273ea2e74be95703eb5dca25eb2 100644
--- a/website/thaliawebsite/settings/settings.py
+++ b/website/thaliawebsite/settings/settings.py
@@ -42,6 +42,7 @@ INSTALLED_APPS = [
# Dependencies
'static_precompiler',
'tinymce',
+ 'django_template_check', # This is only necessary in development
# Our apps
'thaliawebsite', # include for admin settings
'members',
@@ -51,9 +52,9 @@ INSTALLED_APPS = [
'utils',
'mailinglists',
'merchandise',
- 'django_template_check', # This is only necessary in development
'thabloid',
'partners',
+ 'events',
]
MIDDLEWARE = [
diff --git a/website/thaliawebsite/urls.py b/website/thaliawebsite/urls.py
index e7c7408287a2e760a53a3a12ec0cb3ebcfb48e8c..45915f1e49c8cf2a127b5740231768556babccc8 100644
--- a/website/thaliawebsite/urls.py
+++ b/website/thaliawebsite/urls.py
@@ -30,6 +30,7 @@ urlpatterns = [
url(r'^mailinglists/', include('mailinglists.urls', namespace='mailinglists')),
url(r'^members/', include('members.urls', namespace='members')),
url(r'^nyi$', TemplateView.as_view(template_name='status/nyi.html'), name='#'),
+ url(r'^events/', include('events.urls', namespace='events')),
url(r'^association/', include([
url(r'^committees/', include('committees.urls', namespace='committees')),
url(r'^merchandise/', include('merchandise.urls', namespace='merchandise')),