Commit 5f24f571 authored by Joost Rijneveld's avatar Joost Rijneveld
Browse files

Merge branch 'events' into 'master'

Events datamodel en grootste deel admininterface

Working on #4 

Implements the checked functionality:
* [x] Events aanmaken
  * [x] Extra informatievelden
  * [x] ORM Aanmeldingen
  * [x] Organisatorveld
* [ ] Aanmeldingen weergeven
  * [x] Met de hand aanmelden
    * [x] leden
    * [x] niet-leden
  * [ ] Aanwezigheidregistratie
  * [ ] Export aanmeldingen 
     * [ ] met informatie
* [ ] Events weergeven
  * [ ] Agendaoverzicht
  * [ ] iCal feed
  * [ ] Evenement view
  * [ ] Deelnemers laten zien iff ingelogd
  * [ ] Aanmelden
     * [ ] Alleen huidige leden laten aanmelden
  * [ ] Afmelden 
     * [ ] Afmelden na de deadline: thalia/website#390
* [ ] Squash migrations

See merge request !21
parents 5a96665c 344c520b
......@@ -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
......@@ -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
......
# -*- 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('<a href="{link}">{title}</a>',
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)
from django.apps import AppConfig
class EventsConfig(AppConfig):
name = 'events'
# -*- 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')]),
),
]
# -*- 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'),
),
]
# -*- 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',
),
]
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"""