Initial events app

* Models and admin page for events
* Add organisers to events
* event overview page for organisers
parent 5a96665c
......@@ -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')]),
),
]
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,
blank=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,
)
registration_required = models.BooleanField(
_('registration required'),
default=False
)
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 clean(self):
super().clean()
if self.end < self.start:
raise ValidationError(
{'end': _("Can't have an event travel back in time")})
if self.registration_required and self.no_registration_message:
raise ValidationError(
{'no_registration_message': _(
"Doesn't make sense to have this if you require "
"registrations.")})
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()
{% extends 'admin/index.html' %}
{% load i18n admin_urls static admin_modify %}
{% block title %}{{ event.title }}{{ block.super }}{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'home'|capfirst %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label='events' %}">{% trans 'events'|capfirst %}</a>
&rsaquo; <a href="/admin/events/event/">{% trans 'events'|capfirst %}</a>
&rsaquo; {{ event.title }}
</div>
{% endblock %}
{% block content_title %}<h1>{% blocktrans with event=event.title %}Event overview: {{ event }}{% endblocktrans %}</h1>{% endblock %}
{% block content %}
<div id="content-main">
<div class="module">
{% with event.registrationinformationfield_set.all as fields %}
<h2>{% trans "registrations"|capfirst %}</h2>
{% include 'events/admin/registrations_table.html' with registrations=registrations %}
<br>
{% if waiting %}
<h2>{% trans "waiting"|capfirst %}</h2>
{% include 'events/admin/registrations_table.html' with registrations=waiting verb='queued' addlink=False %}
<br>
{% endif %}
<h2>{% trans "cancellations"|capfirst %}</h2>
{% include 'events/admin/registrations_table.html' with registrations=cancellations verb='cancelled' addlink=False %}
{% endwith %}
</div>
</div>
{% endblock %}
{% block sidebar %}
<div id="content-related">
<div class="module">
<h2>{% trans "Event properties" %} —
<a class="changelink"
href="{% url 'admin:events_event_change' event.pk %}?next={{ request.get_full_path|urlencode }}">
{% trans "change"|capfirst %}
</a>
</h2>
<dl class="actionlist">
<dt>{% trans "title"|capfirst %}</dt>
<dd>{{ event.title }}</dd>
<dt>{% trans "date"|capfirst %}</dt>
<dd>{{ event.start }} <br>— {{ event.end }}</dt>
<dt>{% trans "organiser"|capfirst %}</dt>
<dd>{{ event.organiser }}</dd>
<dt>{% trans "registration period"|capfirst %}</dt>
<dd>{{ event.registration_start }} <br>— {{ event.registration_end }}</dd>
<dt>{% trans "location"|capfirst %}</dt>
<dd>{{ event.location }} ({{ event.map_location }})</dd>
<dt>{% trans "price"|capfirst %}</dt>
<dd>{{ event.price }}</dd>
<dt>{% trans "cost"|capfirst %}</dt>
<dd>{{ event.cost }}</dd>
<dt>{% trans "registration required"|capfirst %}</dt>
<dd>{{ event.registration_required|yesno }}</dd>
{% if not event.registration_required %}
<dt>{% trans "registration message"|capfirst %}</dt>
<dd>{{ event.registration_message|default:event.DEFAULT_NO_REGISTRATION_MESSAGE }}</dd>
{% endif %}
</dl>
</div>
</div>
{% endblock %}
{% load i18n %}
<div class="results">
<table id="result_list">
<thead>
<tr>
<th scope="col">{% trans "name"|capfirst %}</th>
<th scope="col">{% trans "date"|