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 %} + +{% 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 %} +
+ + + + + + {% for field in fields %} + + {% endfor %} + + + + {% endif %} + + + + {% for registration in registrations %} + + {% if registration.member %} + + {% else %} + + {% endif %} + + {% for field in registration.registration_information %} + {% if not field.value %} + + {% elif field.field.type == 'checkbox' %} + + {% else %} + + {% endif %} + {% endfor %} + + + + + {% empty %} + + + {% for field in fields %} + + {% endfor %} + + + + + + {% endfor %} + +
{% trans "name"|capfirst %}{% trans "date"|capfirst %}{{ field.name }}{% trans "present"|capfirst %}{% trans "paid"|capfirst %} + {% if addlink != 0 %} + {% trans "add"|capfirst %}
{{ registration.member.get_full_name }}{{ registration.name }}{{ registration.date }}{{ field.value|yesno }}{{ field.value }}{{ registration.present|yesno }}{{ registration.paid|yesno }}{% trans "change" %}
{% blocktrans with verb=verb|default:'registered' %}Nobody {{ verb }} yet{% endblocktrans %}     
+
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')),