models.py 24.8 KB
Newer Older
1
"""The models defined by the events package"""
2
from django.conf import settings
Thom Wiggers's avatar
Thom Wiggers committed
3
from django.core import validators
4
from django.core.exceptions import ValidationError, ObjectDoesNotExist
Thom Wiggers's avatar
Thom Wiggers committed
5
6
from django.db import models
from django.db.models import Q
7
from django.urls import reverse
Thom Wiggers's avatar
Thom Wiggers committed
8
from django.utils import timezone
9
from django.utils.crypto import get_random_string
10
from django.utils.text import format_lazy
11
from django.utils.translation import ugettext_lazy as _
12
from tinymce.models import HTMLField
13

14
15
from members.models import Member
from pushnotifications.models import ScheduledMessage, Category
16
from utils.translation import ModelTranslateMeta, MultilingualField
17
from announcements.models import Slide
Thom Wiggers's avatar
Thom Wiggers committed
18
19


20
class Event(models.Model, metaclass=ModelTranslateMeta):
21
    """Describes an event"""
Thom Wiggers's avatar
Thom Wiggers committed
22

Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
23
    CATEGORY_ALUMNI = 'alumni'
24
25
26
27
    CATEGORY_EDUCATION = 'education'
    CATEGORY_CAREER = 'career'
    CATEGORY_LEISURE = 'leisure'
    CATEGORY_ASSOCIATION = 'association'
Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
28
29
    CATEGORY_OTHER = 'other'

30
    EVENT_CATEGORIES = (
Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
31
        (CATEGORY_ALUMNI, _('Alumni')),
32
33
34
35
        (CATEGORY_EDUCATION, _('Education')),
        (CATEGORY_CAREER, _('Career')),
        (CATEGORY_LEISURE, _('Leisure')),
        (CATEGORY_ASSOCIATION, _('Association Affairs')),
Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
36
        (CATEGORY_OTHER, _('Other')))
37

38
39
    DEFAULT_NO_REGISTRATION_MESSAGE = _('No registration required / '
                                        'Geen aanmelding vereist')
Thom Wiggers's avatar
Thom Wiggers committed
40

41
42
43
44
45
    title = MultilingualField(
        models.CharField,
        _("title"),
        max_length=100
    )
Thom Wiggers's avatar
Thom Wiggers committed
46

47
    description = MultilingualField(
48
        HTMLField,
Thijs de Jong's avatar
Thijs de Jong committed
49
50
51
52
        _("description"),
        help_text=_("Please fill in both of the description boxes (EN/NL),"
                    " even if your event is Dutch only! Fill in the English "
                    "description in Dutch then.")
53
    )
Thom Wiggers's avatar
Thom Wiggers committed
54
55
56
57
58
59

    start = models.DateTimeField(_("start time"))

    end = models.DateTimeField(_("end time"))

    organiser = models.ForeignKey(
60
        'activemembers.MemberGroup',
Thom Wiggers's avatar
Thom Wiggers committed
61
        models.PROTECT,
62
        verbose_name=_("organiser")
Thom Wiggers's avatar
Thom Wiggers committed
63
64
    )

65
66
67
68
    category = models.CharField(
        max_length=40,
        choices=EVENT_CATEGORIES,
        verbose_name=_('category'),
69
70
71
72
73
74
75
        help_text=_('Alumni: Events organised for alumni, '
                    'Education: Education focused events, '
                    'Career: Career focused events, '
                    'Leisure: borrels, parties, game activities etc., '
                    'Association Affairs: general meetings or '
                    'any other board related events, '
                    'Other: anything else.')
76
77
    )

Thom Wiggers's avatar
Thom Wiggers committed
78
79
80
81
    registration_start = models.DateTimeField(
        _("registration start"),
        null=True,
        blank=True,
82
83
        help_text=_("If you set a registration period registration will be "
                    "required. If you don't set one, registration won't be "
84
85
                    "required. Prefer times when people don't have lectures, "
                    "e.g. 12:30 instead of 13:37.")
Thom Wiggers's avatar
Thom Wiggers committed
86
87
88
89
90
    )

    registration_end = models.DateTimeField(
        _("registration end"),
        null=True,
91
92
93
94
        blank=True,
        help_text=_("If you set a registration period registration will be "
                    "required. If you don't set one, registration won't be "
                    "required.")
95
96
97
98
99
    )

    cancel_deadline = models.DateTimeField(
        _("cancel deadline"),
        null=True,
Thom Wiggers's avatar
Thom Wiggers committed
100
101
102
        blank=True
    )

103
104
105
106
107
108
109
    send_cancel_email = models.BooleanField(
        _('send cancellation notifications'),
        default=True,
        help_text=_("Send an email to the organising party when a member "
                    "cancels their registration after the deadline."),
    )

110
111
112
113
114
    location = MultilingualField(
        models.CharField,
        _("location"),
        max_length=255,
    )
Thom Wiggers's avatar
Thom Wiggers committed
115
116
117
118
119

    map_location = models.CharField(
        _("location for minimap"),
        max_length=255,
        help_text=_('Location of Huygens: Heyendaalseweg 135, Nijmegen. '
120
                    'Location of Mercator 1: Toernooiveld 212, Nijmegen. '
Thom Wiggers's avatar
Thom Wiggers committed
121
122
123
124
125
126
127
128
129
130
131
                    'Not shown as text!!'),
    )

    price = models.DecimalField(
        _("price"),
        max_digits=5,
        decimal_places=2,
        default=0,
        validators=[validators.MinValueValidator(0)],
    )

132
133
    fine = models.DecimalField(
        _("fine"),
Thom Wiggers's avatar
Thom Wiggers committed
134
135
136
        max_digits=5,
        decimal_places=2,
        default=0,
137
138
        # Minimum fine is checked in this model's clean(), as it is only for
        # events that require registration.
139
        help_text=_("Fine if participant does not show up (at least €5)."),
140
        validators=[validators.MinValueValidator(0)],
Thom Wiggers's avatar
Thom Wiggers committed
141
142
143
144
145
146
147
148
    )

    max_participants = models.PositiveSmallIntegerField(
        _('maximum number of participants'),
        blank=True,
        null=True,
    )

149
150
    no_registration_message = MultilingualField(
        models.CharField,
Thom Wiggers's avatar
Thom Wiggers committed
151
152
153
154
        _('message when there is no registration'),
        max_length=200,
        blank=True,
        null=True,
155
156
        help_text=(format_lazy("{} {}", _("Default:"),
                               DEFAULT_NO_REGISTRATION_MESSAGE)),
Thom Wiggers's avatar
Thom Wiggers committed
157
158
159
160
    )

    published = models.BooleanField(_("published"), default=False)

161
162
163
164
165
166
167
    registration_reminder = models.ForeignKey(
        ScheduledMessage, on_delete=models.deletion.SET_NULL,
        related_name='registration_event', blank=True, null=True)
    start_reminder = models.ForeignKey(
        ScheduledMessage, on_delete=models.deletion.SET_NULL,
        related_name='start_event', blank=True, null=True)

168
169
170
171
172
173
    documents = models.ManyToManyField(
        'documents.Document',
        verbose_name=_("documents"),
        blank=True,
    )

174
175
176
177
178
179
180
181
182
183
    slide = models.ForeignKey(
        Slide,
        verbose_name='slide',
        help_text=_("Change the header-image on the event's info-page to one"
                    "specific to this event."),
        blank=True,
        on_delete=models.deletion.SET_NULL,
        null=True
    )

184
185
186
187
188
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._price = self.price
        self._registration_start = self.registration_start

189
    @property
190
    def after_cancel_deadline(self):
191
192
193
194
195
        return self.cancel_deadline and self.cancel_deadline <= timezone.now()

    @property
    def registration_started(self):
        return self.registration_start <= timezone.now()
196

197
    @property
198
199
200
    def registration_required(self):
        return bool(self.registration_start) or bool(self.registration_end)

201
202
203
204
    def has_fields(self):
        return self.registrationinformationfield_set.count() > 0

    def reached_participants_limit(self):
205
        """Is this event up to capacity?"""
206
        return (self.max_participants is not None and
207
208
                self.max_participants <= self.registration_set.filter(
                    date_cancelled=None).count())
209

210
211
    @property
    def registrations(self):
212
        """Queryset with all non-cancelled registrations"""
213
214
215
216
        return self.registration_set.filter(date_cancelled=None)

    @property
    def participants(self):
217
        """Return the active participants"""
218
219
220
        if self.max_participants is not None:
            return self.registrations.order_by('date')[:self.max_participants]
        return self.registrations.order_by('date')
221
222
223

    @property
    def queue(self):
224
        """Return the waiting queue"""
225
226
227
        if self.max_participants is not None:
            return self.registrations.order_by('date')[self.max_participants:]
        return []
228

229
230
    @property
    def cancellations(self):
231
232
233
234
        """Return a queryset with the cancelled events"""
        return (self.registration_set
                .exclude(date_cancelled=None)
                .order_by('date_cancelled'))
235

236
    @property
237
    def registration_allowed(self):
238
        now = timezone.now()
239
240
        return (bool(self.registration_start or self.registration_end)
                and self.registration_end > now >= self.registration_start)
241
242
243
244

    @property
    def cancellation_allowed(self):
        now = timezone.now()
245
        return (bool(self.registration_start or self.registration_end)
246
                and self.registration_start <= now < self.start)
247

248
249
250
251
252
253
254
    def is_pizza_event(self):
        try:
            self.pizzaevent
            return True
        except ObjectDoesNotExist:
            return False

Thom Wiggers's avatar
Thom Wiggers committed
255
256
    def clean(self):
        super().clean()
Thom Wiggers's avatar
Thom Wiggers committed
257
        errors = {}
258
        if self.start is None:
Thom Wiggers's avatar
Thom Wiggers committed
259
            errors.update({
260
261
262
263
264
265
266
267
                'start': _("Start cannot have an empty date or time field")
            })
        if self.end is None:
            errors.update({
                'end': _("End cannot have an empty date or time field")
            })
        if self.start is not None and self.end is not None:
            if self.end < self.start:
268
                errors.update({
269
270
271
272
273
274
275
276
277
278
279
280
281
282
                    'end': _("Can't have an event travel back in time")})
            if self.registration_required:
                if self.fine < 5:
                    errors.update({
                        'fine': _("The fine for this event is too low "
                                  "(must be at least €5).")
                    })
                for lang in settings.LANGUAGES:
                    field = 'no_registration_message_' + lang[0]
                    if getattr(self, field):
                        errors.update(
                            {field: _("Doesn't make sense to have this "
                                      "if you require registrations.")})
                if not self.registration_start:
283
                    errors.update(
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
                        {'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 not self.cancel_deadline:
                    errors.update(
                        {'cancel_deadline': _(
                            "If registration is required, "
                            "you need a deadline for the cancellation")})
                elif self.cancel_deadline > self.start:
                    errors.update(
                        {'cancel_deadline': _(
                            "The cancel deadline should be"
                            " before the start of the event.")})
                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})
309
310
311
312
313
314
315
316
317
318
319
320

        try:
            if (self.organiser is not None and
                    self.send_cancel_email and
                    self.organiser.contact_mailinglist is None):
                errors.update(
                    {'send_cancel_email': _(
                        "This organiser does not "
                        "have a contact mailinglist.")})
        except ObjectDoesNotExist:
            pass

321
        if self.published:
Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
322
            if (self.price != self._price and self._registration_start
323
324
325
326
                    and self._registration_start <= timezone.now()):
                errors.update(
                    {'price': _("You cannot change this field after "
                                "the registration has started.")})
Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
327
328
            if (self._registration_start
                    and self.registration_start != self._registration_start
329
330
331
332
333
                    and self._registration_start <= timezone.now()):
                errors.update(
                    {'registration_start':
                     _("You cannot change this field after "
                       "the registration has started.")})
334

Thom Wiggers's avatar
Thom Wiggers committed
335
336
        if errors:
            raise ValidationError(errors)
Thom Wiggers's avatar
Thom Wiggers committed
337
338

    def get_absolute_url(self):
339
        return reverse('events:event', args=[str(self.pk)])
Thom Wiggers's avatar
Thom Wiggers committed
340

341
    def save(self, *args, **kwargs):
342
343
344
        if not self.pk:
            super().save(*args, **kwargs)

345
346
        if self.published:
            if self.registration_required:
347
                registration_reminder_time = (self.registration_start -
348
                                              timezone.timedelta(hours=1))
349
350
                registration_reminder = ScheduledMessage()
                if (self.registration_reminder is not None
351
                        and not self.registration_reminder.sent):
352
                    registration_reminder = self.registration_reminder
353
354
355
356
357
358
359
360
361
362
363

                if registration_reminder_time > timezone.now():
                    registration_reminder.title_en = 'Event registration'
                    registration_reminder.title_nl = 'Evenement registratie'
                    registration_reminder.body_en = ('Registration for \'{}\' '
                                                     'starts in 1 hour'
                                                     .format(self.title_en))
                    registration_reminder.body_nl = ('Registratie voor \'{}\' '
                                                     'start in 1 uur'
                                                     .format(self.title_nl))
                    registration_reminder.category = Category.objects.get(
364
                        key=Category.EVENT)
365
                    registration_reminder.time = registration_reminder_time
366
367
368
                    registration_reminder.url = (
                        f'{settings.BASE_URL}'
                        f'{reverse("events:event", args=[self.id])}')
369

370
371
372
                    registration_reminder.save()
                    self.registration_reminder = registration_reminder
                    self.registration_reminder.users.set(
373
                        Member.current_members.all())
374
375
376
                elif registration_reminder.pk is not None:
                    self.registration_reminder = None
                    registration_reminder.delete()
377

378
            start_reminder_time = (self.start - timezone.timedelta(hours=1))
379
380
            start_reminder = ScheduledMessage()
            if (self.start_reminder is not None
381
                    and not self.start_reminder.sent):
382
                start_reminder = self.start_reminder
383
384
385
386

            if start_reminder_time > timezone.now():
                start_reminder.title_en = 'Event'
                start_reminder.title_nl = 'Evenement'
387
388
389
390
                start_reminder.body_en = (f'\'{self.title_en}\' starts in '
                                          '1 hour')
                start_reminder.body_nl = (f'\'{self.title_nl}\' begint over '
                                          '1 uur')
391
392
                start_reminder.category = Category.objects.get(
                    key=Category.EVENT)
393
394
395
396
                start_reminder.time = start_reminder_time
                start_reminder.save()
                self.start_reminder = start_reminder
                if self.registration_required:
Thom Wiggers's avatar
Thom Wiggers committed
397
                    self.start_reminder.users.set(
398
399
                        [r.member for r in self.participants
                         if r.member])
400
                else:
401
                    self.start_reminder.users.set(Member.current_members.all())
402
403
404
            elif start_reminder.pk is not None:
                self.start_reminder = None
                start_reminder.delete()
405
        else:
406
407
408
            if (self.registration_reminder is not None
                    and not self.registration_reminder.sent):
                self.registration_reminder.delete()
Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
409
                self.registration_reminder = None
410
411
412
            if (self.start_reminder is not None
                    and not self.start_reminder.sent):
                self.start_reminder.delete()
Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
413
                self.start_reminder = None
414

415
        super().save()
416

417
418
419
420
421
422
423
    def delete(self, using=None, keep_parents=False):
        if (self.registration_reminder is not None
                and not self.registration_reminder.sent):
            self.registration_reminder.delete()
        if (self.start_reminder is not None
                and not self.start_reminder.sent):
            self.start_reminder.delete()
Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
424
425
        if self.is_pizza_event():
            self.pizzaevent.delete()
426
427
        return super().delete(using, keep_parents)

Thom Wiggers's avatar
Thom Wiggers committed
428
429
430
431
432
433
434
    def __str__(self):
        return '{}: {}'.format(
            self.title,
            timezone.localtime(self.start).strftime('%Y-%m-%d %H:%M'))

    class Meta:
        ordering = ('-start',)
435
436
437
        permissions = (
            ("override_organiser", "Can access events as if organizing"),
        )
Thom Wiggers's avatar
Thom Wiggers committed
438
439


440
def registration_member_choices_limit():
441
    """Defines queryset filters to only include current members"""
442
443
444
445
    return (Q(membership__until__isnull=True) |
            Q(membership__until__gt=timezone.now().date()))


Thom Wiggers's avatar
Thom Wiggers committed
446
class Registration(models.Model):
447
    """Describes a registration for an Event"""
Thom Wiggers's avatar
Thom Wiggers committed
448
449
450
451

    event = models.ForeignKey(Event, models.CASCADE)

    member = models.ForeignKey(
452
        'members.Member', models.CASCADE,
Thom Wiggers's avatar
Thom Wiggers committed
453
454
        blank=True,
        null=True,
455
        limit_choices_to=registration_member_choices_limit
Thom Wiggers's avatar
Thom Wiggers committed
456
457
458
459
460
461
462
463
464
465
    )

    name = models.CharField(
        _('name'),
        max_length=50,
        help_text=_('Use this for non-members'),
        null=True,
        blank=True
    )

466
467
    date = models.DateTimeField(_('registration date'),
                                default=timezone.now)
Thom Wiggers's avatar
Thom Wiggers committed
468
469
470
471
472
    date_cancelled = models.DateTimeField(_('cancellation date'),
                                          null=True,
                                          blank=True)

    present = models.BooleanField(
473
        _('present'),
Thom Wiggers's avatar
Thom Wiggers committed
474
475
        default=False,
    )
476

477
478
479
480
481
482
    payment = models.OneToOneField(
        'payments.Payment',
        related_name='events_registration',
        on_delete=models.PROTECT,
        blank=True,
        null=True,
Thom Wiggers's avatar
Thom Wiggers committed
483
484
    )

485
486
    @property
    def information_fields(self):
Thom Wiggers's avatar
Thom Wiggers committed
487
488
489
490
        fields = self.event.registrationinformationfield_set.all()
        return [{'field': field, 'value': field.get_value_for(self)}
                for field in fields]

491
492
493
494
495
496
497
498
499
500
501
502
503
    @property
    def is_registered(self):
        return self.date_cancelled is None

    @property
    def queue_position(self):
        if self.event.max_participants is not None:
            try:
                return list(self.event.queue).index(self) + 1
            except ValueError:
                pass
        return 0

504
505
506
507
508
    @property
    def is_invited(self):
        return (self.is_registered and
                self.queue_position == 0)

509
510
511
    def is_external(self):
        return bool(self.name)

Luuk Scholten's avatar
Luuk Scholten committed
512
    def is_late_cancellation(self):
513
514
        # First check whether or not the user cancelled
        # If the user cancelled then check if this was after the deadline
515
516
        # And if there is a max participants number:
        # do a complex check to calculate if this user was on
517
518
519
        # the waiting list at the time of cancellation, since
        # you shouldn't need to pay the costs of something
        # you weren't even able to go to.
Luuk Scholten's avatar
Luuk Scholten committed
520
        return (self.date_cancelled and
521
                self.event.cancel_deadline and
522
                self.date_cancelled > self.event.cancel_deadline and
523
524
                (self.event.max_participants is None or
                 self.event.registration_set.filter(
525
526
527
                     (Q(date_cancelled__gte=self.date_cancelled) |
                      Q(date_cancelled=None)) &
                     Q(date__lte=self.date)
528
                 ).count() < self.event.max_participants))
Luuk Scholten's avatar
Luuk Scholten committed
529

530
    def is_paid(self):
531
        return self.payment and self.payment.processed
532

533
    def would_cancel_after_deadline(self):
534
535
536
        now = timezone.now()
        return (self.queue_position == 0 and
                now >= self.event.cancel_deadline)
537

Thom Wiggers's avatar
Thom Wiggers committed
538
539
540
541
542
543
544
545
546
547
548
    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)

549
550
551
552
553
554
555
556
557
558
    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)

        if self.event.start_reminder and self.date_cancelled:
            self.event.start_reminder.users.remove(self.member)
        elif (self.event.start_reminder and self.member is not None and
              not self.event.start_reminder.users
                  .filter(pk=self.member.pk).exists()):
            self.event.start_reminder.users.add(self.member)

Thom Wiggers's avatar
Thom Wiggers committed
559
560
561
562
563
564
565
566
    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',)
567
        unique_together = (('member', 'event'),)
Thom Wiggers's avatar
Thom Wiggers committed
568
569


570
class RegistrationInformationField(models.Model, metaclass=ModelTranslateMeta):
571
    """Describes a field description to ask for when registering"""
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
    BOOLEAN_FIELD = 'boolean'
    INTEGER_FIELD = 'integer'
    TEXT_FIELD = 'text'

    FIELD_TYPES = ((BOOLEAN_FIELD, _('Checkbox')),
                   (TEXT_FIELD, _('Text')),
                   (INTEGER_FIELD, _('Integer')),)

    event = models.ForeignKey(Event, models.CASCADE)

    type = models.CharField(
        _('field type'),
        choices=FIELD_TYPES,
        max_length=10,
    )

    name = MultilingualField(
        models.CharField,
        _('field name'),
        max_length=100,
    )

    description = MultilingualField(
        models.TextField,
        _('description'),
        null=True,
        blank=True,
    )

    required = models.BooleanField(
        _('required'),
    )

    def get_value_for(self, registration):
        if self.type == self.TEXT_FIELD:
            value_set = self.textregistrationinformation_set
        elif self.type == self.BOOLEAN_FIELD:
            value_set = self.booleanregistrationinformation_set
        elif self.type == self.INTEGER_FIELD:
            value_set = self.integerregistrationinformation_set

        try:
            return value_set.get(registration=registration).value
        except (TextRegistrationInformation.DoesNotExist,
                BooleanRegistrationInformation.DoesNotExist,
                IntegerRegistrationInformation.DoesNotExist):
            return None

    def set_value_for(self, registration, value):
        if self.type == self.TEXT_FIELD:
            value_set = self.textregistrationinformation_set
        elif self.type == self.BOOLEAN_FIELD:
            value_set = self.booleanregistrationinformation_set
        elif self.type == self.INTEGER_FIELD:
            value_set = self.integerregistrationinformation_set

        try:
            field_value = value_set.get(registration=registration)
        except BooleanRegistrationInformation.DoesNotExist:
            field_value = BooleanRegistrationInformation()
        except TextRegistrationInformation.DoesNotExist:
            field_value = TextRegistrationInformation()
        except IntegerRegistrationInformation.DoesNotExist:
            field_value = IntegerRegistrationInformation()

        field_value.registration = registration
        field_value.field = self
        field_value.value = value
        field_value.save()

    def __str__(self):
        return "{} ({})".format(self.name, dict(self.FIELD_TYPES)[self.type])

    class Meta:
        order_with_respect_to = 'event'


Thom Wiggers's avatar
Thom Wiggers committed
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
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):
Joren Vrancken's avatar
Joren Vrancken committed
664
    """Checkbox information filled in by members when registering"""
Thom Wiggers's avatar
Thom Wiggers committed
665
666
667
668
669

    value = models.BooleanField()


class TextRegistrationInformation(AbstractRegistrationInformation):
Joren Vrancken's avatar
Joren Vrancken committed
670
    """Checkbox information filled in by members when registering"""
Thom Wiggers's avatar
Thom Wiggers committed
671
672
673
674
    value = models.TextField()


class IntegerRegistrationInformation(AbstractRegistrationInformation):
Joren Vrancken's avatar
Joren Vrancken committed
675
    """Checkbox information filled in by members when registering"""
Thom Wiggers's avatar
Thom Wiggers committed
676
    value = models.IntegerField()
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697


class FeedToken(models.Model):
    """Used to personalize the ical Feed"""

    member = models.OneToOneField('members.Member', models.CASCADE)
    token = models.CharField(max_length=32, editable=False)

    def save(self, *args, **kwargs):
        self.token = get_random_string(32)
        super().save(*args, **kwargs)

    @staticmethod
    def get_member(token):
        try:
            return FeedToken.objects.get(token=token).member
        except FeedToken.DoesNotExist:
            return None

    def __str__(self):
        return '{} ({})'.format(self.member.get_full_name(), self.token)