models.py 25.9 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
5
from django.db import models, router
Thom Wiggers's avatar
Thom Wiggers committed
6
from django.db.models import Q
7
from django.db.models.deletion import Collector
8
from django.urls import reverse
Thom Wiggers's avatar
Thom Wiggers committed
9
from django.utils import timezone
10
from django.utils.crypto import get_random_string
11
from django.utils.text import format_lazy
12
from django.utils.translation import gettext_lazy as _
13
from tinymce.models import HTMLField
14

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


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

Luko van der Maas's avatar
Luko van der Maas committed
24
25
26
27
28
29
    CATEGORY_ALUMNI = "alumni"
    CATEGORY_EDUCATION = "education"
    CATEGORY_CAREER = "career"
    CATEGORY_LEISURE = "leisure"
    CATEGORY_ASSOCIATION = "association"
    CATEGORY_OTHER = "other"
Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
30

31
    EVENT_CATEGORIES = (
Luko van der Maas's avatar
Luko van der Maas committed
32
33
34
35
36
37
38
        (CATEGORY_ALUMNI, _("Alumni")),
        (CATEGORY_EDUCATION, _("Education")),
        (CATEGORY_CAREER, _("Career")),
        (CATEGORY_LEISURE, _("Leisure")),
        (CATEGORY_ASSOCIATION, _("Association Affairs")),
        (CATEGORY_OTHER, _("Other")),
    )
Thom Wiggers's avatar
Thom Wiggers committed
39

Luko van der Maas's avatar
Luko van der Maas committed
40
41
    DEFAULT_NO_REGISTRATION_MESSAGE = _(
        "No registration required / " "Geen aanmelding vereist"
42
    )
Thom Wiggers's avatar
Thom Wiggers committed
43

Luko van der Maas's avatar
Luko van der Maas committed
44
45
    title = MultilingualField(models.CharField, _("title"), max_length=100)

46
    description = MultilingualField(
47
        HTMLField,
Thijs de Jong's avatar
Thijs de Jong committed
48
        _("description"),
Luko van der Maas's avatar
Luko van der Maas committed
49
50
51
52
53
        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."
        ),
54
    )
Thom Wiggers's avatar
Thom Wiggers committed
55
56
57
58
59
60

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

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

    organiser = models.ForeignKey(
Luko van der Maas's avatar
Luko van der Maas committed
61
        "activemembers.MemberGroup", models.PROTECT, verbose_name=_("organiser")
Thom Wiggers's avatar
Thom Wiggers committed
62
63
    )

64
65
66
    category = models.CharField(
        max_length=40,
        choices=EVENT_CATEGORIES,
Luko van der Maas's avatar
Luko van der Maas committed
67
68
69
70
71
72
73
74
75
76
        verbose_name=_("category"),
        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."
        ),
77
78
    )

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

    registration_end = models.DateTimeField(
        _("registration end"),
        null=True,
94
        blank=True,
Luko van der Maas's avatar
Luko van der Maas committed
95
96
97
98
99
        help_text=_(
            "If you set a registration period registration will be "
            "required. If you don't set one, registration won't be "
            "required."
        ),
100
101
    )

Luko van der Maas's avatar
Luko van der Maas committed
102
    cancel_deadline = models.DateTimeField(_("cancel deadline"), null=True, blank=True)
Thom Wiggers's avatar
Thom Wiggers committed
103

104
    send_cancel_email = models.BooleanField(
Luko van der Maas's avatar
Luko van der Maas committed
105
        _("send cancellation notifications"),
106
        default=True,
Luko van der Maas's avatar
Luko van der Maas committed
107
108
109
110
        help_text=_(
            "Send an email to the organising party when a member "
            "cancels their registration after the deadline."
        ),
111
112
    )

Luko van der Maas's avatar
Luko van der Maas committed
113
    location = MultilingualField(models.CharField, _("location"), max_length=255,)
Thom Wiggers's avatar
Thom Wiggers committed
114
115
116
117

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

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

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

    max_participants = models.PositiveSmallIntegerField(
Luko van der Maas's avatar
Luko van der Maas committed
145
        _("maximum number of participants"), blank=True, null=True,
Thom Wiggers's avatar
Thom Wiggers committed
146
147
    )

148
149
    no_registration_message = MultilingualField(
        models.CharField,
Luko van der Maas's avatar
Luko van der Maas committed
150
        _("message when there is no registration"),
Thom Wiggers's avatar
Thom Wiggers committed
151
152
153
        max_length=200,
        blank=True,
        null=True,
Luko van der Maas's avatar
Luko van der Maas committed
154
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
    registration_reminder = models.ForeignKey(
Luko van der Maas's avatar
Luko van der Maas committed
162
163
164
165
166
167
        ScheduledMessage,
        on_delete=models.deletion.SET_NULL,
        related_name="registration_event",
        blank=True,
        null=True,
    )
168
    start_reminder = models.ForeignKey(
Luko van der Maas's avatar
Luko van der Maas committed
169
170
171
172
173
174
        ScheduledMessage,
        on_delete=models.deletion.SET_NULL,
        related_name="start_event",
        blank=True,
        null=True,
    )
175

176
    documents = models.ManyToManyField(
Luko van der Maas's avatar
Luko van der Maas committed
177
        "documents.Document", verbose_name=_("documents"), blank=True,
178
179
    )

180
181
    slide = models.ForeignKey(
        Slide,
Luko van der Maas's avatar
Luko van der Maas committed
182
183
184
185
186
        verbose_name="slide",
        help_text=_(
            "Change the header-image on the event's info-page to one"
            "specific to this event."
        ),
187
188
        blank=True,
        on_delete=models.deletion.SET_NULL,
Luko van der Maas's avatar
Luko van der Maas committed
189
        null=True,
190
191
    )

192
193
194
195
196
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._price = self.price
        self._registration_start = self.registration_start

197
    @property
198
    def after_cancel_deadline(self):
199
200
201
202
203
        return self.cancel_deadline and self.cancel_deadline <= timezone.now()

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

205
    @property
206
207
208
    def registration_required(self):
        return bool(self.registration_start) or bool(self.registration_end)

209
210
211
212
    def has_fields(self):
        return self.registrationinformationfield_set.count() > 0

    def reached_participants_limit(self):
213
        """Is this event up to capacity?"""
Luko van der Maas's avatar
Luko van der Maas committed
214
215
216
217
218
        return (
            self.max_participants is not None
            and self.max_participants
            <= self.registration_set.filter(date_cancelled=None).count()
        )
219

220
221
    @property
    def registrations(self):
222
        """Queryset with all non-cancelled registrations"""
223
224
225
226
        return self.registration_set.filter(date_cancelled=None)

    @property
    def participants(self):
227
        """Return the active participants"""
228
        if self.max_participants is not None:
Luko van der Maas's avatar
Luko van der Maas committed
229
230
            return self.registrations.order_by("date")[: self.max_participants]
        return self.registrations.order_by("date")
231
232
233

    @property
    def queue(self):
234
        """Return the waiting queue"""
235
        if self.max_participants is not None:
Luko van der Maas's avatar
Luko van der Maas committed
236
            return self.registrations.order_by("date")[self.max_participants :]
237
        return []
238

239
240
    @property
    def cancellations(self):
241
        """Return a queryset with the cancelled events"""
Luko van der Maas's avatar
Luko van der Maas committed
242
243
244
        return self.registration_set.exclude(date_cancelled=None).order_by(
            "date_cancelled"
        )
245

246
    @property
247
    def registration_allowed(self):
248
        now = timezone.now()
Luko van der Maas's avatar
Luko van der Maas committed
249
250
251
252
        return (
            bool(self.registration_start or self.registration_end)
            and self.registration_end > now >= self.registration_start
        )
253
254
255
256

    @property
    def cancellation_allowed(self):
        now = timezone.now()
Luko van der Maas's avatar
Luko van der Maas committed
257
258
259
260
        return (
            bool(self.registration_start or self.registration_end)
            and self.registration_start <= now < self.start
        )
261

262
263
264
265
266
267
268
    def is_pizza_event(self):
        try:
            self.pizzaevent
            return True
        except ObjectDoesNotExist:
            return False

Thom Wiggers's avatar
Thom Wiggers committed
269
270
    def clean(self):
        super().clean()
Thom Wiggers's avatar
Thom Wiggers committed
271
        errors = {}
272
        if self.start is None:
Luko van der Maas's avatar
Luko van der Maas committed
273
            errors.update({"start": _("Start cannot have an empty date or time field")})
274
        if self.end is None:
Luko van der Maas's avatar
Luko van der Maas committed
275
            errors.update({"end": _("End cannot have an empty date or time field")})
276
277
        if self.start is not None and self.end is not None:
            if self.end < self.start:
Luko van der Maas's avatar
Luko van der Maas committed
278
                errors.update({"end": _("Can't have an event travel back in time")})
279
280
            if self.registration_required:
                if self.fine < 5:
Luko van der Maas's avatar
Luko van der Maas committed
281
282
283
284
285
286
287
288
                    errors.update(
                        {
                            "fine": _(
                                "The fine for this event is too low "
                                "(must be at least €5)."
                            )
                        }
                    )
289
                for lang in settings.LANGUAGES:
Luko van der Maas's avatar
Luko van der Maas committed
290
                    field = "no_registration_message_" + lang[0]
291
292
                    if getattr(self, field):
                        errors.update(
Luko van der Maas's avatar
Luko van der Maas committed
293
294
295
296
297
298
299
                            {
                                field: _(
                                    "Doesn't make sense to have this "
                                    "if you require registrations."
                                )
                            }
                        )
300
                if not self.registration_start:
301
                    errors.update(
Luko van der Maas's avatar
Luko van der Maas committed
302
303
304
305
306
307
308
                        {
                            "registration_start": _(
                                "If registration is required, you need a start of "
                                "registration"
                            )
                        }
                    )
309
310
                if not self.registration_end:
                    errors.update(
Luko van der Maas's avatar
Luko van der Maas committed
311
312
313
314
315
316
317
                        {
                            "registration_end": _(
                                "If registration is required, you need an end of "
                                "registration"
                            )
                        }
                    )
318
319
                if not self.cancel_deadline:
                    errors.update(
Luko van der Maas's avatar
Luko van der Maas committed
320
321
322
323
324
325
326
                        {
                            "cancel_deadline": _(
                                "If registration is required, "
                                "you need a deadline for the cancellation"
                            )
                        }
                    )
327
328
                elif self.cancel_deadline > self.start:
                    errors.update(
Luko van der Maas's avatar
Luko van der Maas committed
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
                        {
                            "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}
                    )
347
348

        try:
Luko van der Maas's avatar
Luko van der Maas committed
349
350
351
352
353
            if (
                self.organiser is not None
                and self.send_cancel_email
                and self.organiser.contact_mailinglist is None
            ):
354
                errors.update(
Luko van der Maas's avatar
Luko van der Maas committed
355
356
357
358
359
360
                    {
                        "send_cancel_email": _(
                            "This organiser does not " "have a contact mailinglist."
                        )
                    }
                )
361
362
363
        except ObjectDoesNotExist:
            pass

364
        if self.published:
Luko van der Maas's avatar
Luko van der Maas committed
365
366
367
368
369
            if (
                self.price != self._price
                and self._registration_start
                and self._registration_start <= timezone.now()
            ):
370
                errors.update(
Luko van der Maas's avatar
Luko van der Maas committed
371
372
373
374
375
376
377
378
379
380
381
382
                    {
                        "price": _(
                            "You cannot change this field after "
                            "the registration has started."
                        )
                    }
                )
            if (
                self._registration_start
                and self.registration_start != self._registration_start
                and self._registration_start <= timezone.now()
            ):
383
                errors.update(
Luko van der Maas's avatar
Luko van der Maas committed
384
385
386
387
388
389
390
                    {
                        "registration_start": _(
                            "You cannot change this field after "
                            "the registration has started."
                        )
                    }
                )
391

Thom Wiggers's avatar
Thom Wiggers committed
392
393
        if errors:
            raise ValidationError(errors)
Thom Wiggers's avatar
Thom Wiggers committed
394
395

    def get_absolute_url(self):
Luko van der Maas's avatar
Luko van der Maas committed
396
        return reverse("events:event", args=[str(self.pk)])
Thom Wiggers's avatar
Thom Wiggers committed
397

398
    def save(self, *args, **kwargs):
399
400
401
402
        delete_collector = Collector(
            using=router.db_for_write(self.__class__, instance=self)
        )

403
404
405
        if not self.pk:
            super().save(*args, **kwargs)

406
407
        if self.published:
            if self.registration_required:
Luko van der Maas's avatar
Luko van der Maas committed
408
409
410
                registration_reminder_time = (
                    self.registration_start - timezone.timedelta(hours=1)
                )
411
                registration_reminder = ScheduledMessage()
Luko van der Maas's avatar
Luko van der Maas committed
412
413
414
415
                if (
                    self.registration_reminder is not None
                    and not self.registration_reminder.sent
                ):
416
                    registration_reminder = self.registration_reminder
417
418

                if registration_reminder_time > timezone.now():
Luko van der Maas's avatar
Luko van der Maas committed
419
420
421
422
423
424
425
426
427
                    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)
                    )
428
                    registration_reminder.category = Category.objects.get(
Luko van der Maas's avatar
Luko van der Maas committed
429
430
                        key=Category.EVENT
                    )
431
                    registration_reminder.time = registration_reminder_time
432
                    registration_reminder.url = (
Luko van der Maas's avatar
Luko van der Maas committed
433
434
435
                        f"{settings.BASE_URL}"
                        f'{reverse("events:event", args=[self.id])}'
                    )
436

437
438
                    registration_reminder.save()
                    self.registration_reminder = registration_reminder
Luko van der Maas's avatar
Luko van der Maas committed
439
                    self.registration_reminder.users.set(Member.current_members.all())
440
                elif registration_reminder.pk is not None:
441
                    delete_collector.add([self.registration_reminder])
442
                    self.registration_reminder = None
443

Luko van der Maas's avatar
Luko van der Maas committed
444
            start_reminder_time = self.start - timezone.timedelta(hours=1)
445
            start_reminder = ScheduledMessage()
Luko van der Maas's avatar
Luko van der Maas committed
446
            if self.start_reminder is not None and not self.start_reminder.sent:
447
                start_reminder = self.start_reminder
448
449

            if start_reminder_time > timezone.now():
Luko van der Maas's avatar
Luko van der Maas committed
450
451
452
453
454
                start_reminder.title_en = "Event"
                start_reminder.title_nl = "Evenement"
                start_reminder.body_en = f"'{self.title_en}' starts in " "1 hour"
                start_reminder.body_nl = f"'{self.title_nl}' begint over " "1 uur"
                start_reminder.category = Category.objects.get(key=Category.EVENT)
455
456
457
458
                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
459
                    self.start_reminder.users.set(
Luko van der Maas's avatar
Luko van der Maas committed
460
461
                        [r.member for r in self.participants if r.member]
                    )
462
                else:
463
                    self.start_reminder.users.set(Member.current_members.all())
464
            elif start_reminder.pk is not None:
465
                delete_collector.add([self.start_reminder])
466
                self.start_reminder = None
467
        else:
Luko van der Maas's avatar
Luko van der Maas committed
468
469
470
471
            if (
                self.registration_reminder is not None
                and not self.registration_reminder.sent
            ):
472
                delete_collector.add([self.registration_reminder])
Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
473
                self.registration_reminder = None
474

Luko van der Maas's avatar
Luko van der Maas committed
475
            if self.start_reminder is not None and not self.start_reminder.sent:
476
                delete_collector.add([self.start_reminder])
Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
477
                self.start_reminder = None
478

479
        super().save()
480
        delete_collector.delete()
481

482
    def delete(self, using=None, keep_parents=False):
483
484
485
486
        using = using or router.db_for_write(self.__class__, instance=self)
        collector = Collector(using=using)
        collector.collect([self], keep_parents=keep_parents)

Luko van der Maas's avatar
Luko van der Maas committed
487
488
489
490
        if (
            self.registration_reminder is not None
            and not self.registration_reminder.sent
        ):
491
            collector.add([self.registration_reminder])
Luko van der Maas's avatar
Luko van der Maas committed
492
        if self.start_reminder is not None and not self.start_reminder.sent:
493
            collector.add([self.start_reminder])
Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
494
        if self.is_pizza_event():
495
496
497
            collector.add([self.pizzaevent])

        return collector.delete()
498

Thom Wiggers's avatar
Thom Wiggers committed
499
    def __str__(self):
Luko van der Maas's avatar
Luko van der Maas committed
500
501
502
        return "{}: {}".format(
            self.title, timezone.localtime(self.start).strftime("%Y-%m-%d %H:%M")
        )
Thom Wiggers's avatar
Thom Wiggers committed
503
504

    class Meta:
Luko van der Maas's avatar
Luko van der Maas committed
505
506
        ordering = ("-start",)
        permissions = (("override_organiser", "Can access events as if organizing"),)
Thom Wiggers's avatar
Thom Wiggers committed
507
508


509
def registration_member_choices_limit():
510
    """Defines queryset filters to only include current members"""
Luko van der Maas's avatar
Luko van der Maas committed
511
512
513
    return Q(membership__until__isnull=True) | Q(
        membership__until__gt=timezone.now().date()
    )
514
515


Thom Wiggers's avatar
Thom Wiggers committed
516
class Registration(models.Model):
517
    """Describes a registration for an Event"""
Thom Wiggers's avatar
Thom Wiggers committed
518
519
520
521

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

    member = models.ForeignKey(
Luko van der Maas's avatar
Luko van der Maas committed
522
523
        "members.Member",
        models.CASCADE,
Thom Wiggers's avatar
Thom Wiggers committed
524
525
        blank=True,
        null=True,
Luko van der Maas's avatar
Luko van der Maas committed
526
        limit_choices_to=registration_member_choices_limit,
Thom Wiggers's avatar
Thom Wiggers committed
527
528
529
    )

    name = models.CharField(
Luko van der Maas's avatar
Luko van der Maas committed
530
        _("name"),
Thom Wiggers's avatar
Thom Wiggers committed
531
        max_length=50,
Luko van der Maas's avatar
Luko van der Maas committed
532
        help_text=_("Use this for non-members"),
Thom Wiggers's avatar
Thom Wiggers committed
533
        null=True,
Luko van der Maas's avatar
Luko van der Maas committed
534
        blank=True,
Thom Wiggers's avatar
Thom Wiggers committed
535
536
    )

Luko van der Maas's avatar
Luko van der Maas committed
537
538
    date = models.DateTimeField(_("registration date"), default=timezone.now)
    date_cancelled = models.DateTimeField(_("cancellation date"), null=True, blank=True)
Thom Wiggers's avatar
Thom Wiggers committed
539

Luko van der Maas's avatar
Luko van der Maas committed
540
    present = models.BooleanField(_("present"), default=False,)
541

542
    payment = models.OneToOneField(
Luko van der Maas's avatar
Luko van der Maas committed
543
544
        "payments.Payment",
        related_name="events_registration",
545
546
547
        on_delete=models.PROTECT,
        blank=True,
        null=True,
Thom Wiggers's avatar
Thom Wiggers committed
548
549
    )

550
551
    @property
    def information_fields(self):
Thom Wiggers's avatar
Thom Wiggers committed
552
        fields = self.event.registrationinformationfield_set.all()
Luko van der Maas's avatar
Luko van der Maas committed
553
554
555
        return [
            {"field": field, "value": field.get_value_for(self)} for field in fields
        ]
Thom Wiggers's avatar
Thom Wiggers committed
556

557
558
559
560
561
562
563
564
565
566
567
568
569
    @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

570
571
    @property
    def is_invited(self):
Luko van der Maas's avatar
Luko van der Maas committed
572
        return self.is_registered and self.queue_position == 0
573

574
575
576
    def is_external(self):
        return bool(self.name)

Luuk Scholten's avatar
Luuk Scholten committed
577
    def is_late_cancellation(self):
578
579
        # First check whether or not the user cancelled
        # If the user cancelled then check if this was after the deadline
580
581
        # And if there is a max participants number:
        # do a complex check to calculate if this user was on
582
583
584
        # 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.
Luko van der Maas's avatar
Luko van der Maas committed
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
        return (
            self.date_cancelled
            and self.event.cancel_deadline
            and self.date_cancelled > self.event.cancel_deadline
            and (
                self.event.max_participants is None
                or self.event.registration_set.filter(
                    (
                        Q(date_cancelled__gte=self.date_cancelled)
                        | Q(date_cancelled=None)
                    )
                    & Q(date__lte=self.date)
                ).count()
                < self.event.max_participants
            )
        )
Luuk Scholten's avatar
Luuk Scholten committed
601

602
    def is_paid(self):
603
        return self.payment and self.payment.processed
604

605
    def would_cancel_after_deadline(self):
606
        now = timezone.now()
Luko van der Maas's avatar
Luko van der Maas committed
607
        return self.queue_position == 0 and now >= self.event.cancel_deadline
608

Thom Wiggers's avatar
Thom Wiggers committed
609
    def clean(self):
Luko van der Maas's avatar
Luko van der Maas committed
610
611
612
613
614
615
616
        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"),
                }
            )
Thom Wiggers's avatar
Thom Wiggers committed
617
618
619
620

    def validate_unique(self, exclude=None):
        super().validate_unique(exclude)

621
622
623
624
625
    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)
Luko van der Maas's avatar
Luko van der Maas committed
626
627
628
629
630
        elif (
            self.event.start_reminder
            and self.member is not None
            and not self.event.start_reminder.users.filter(pk=self.member.pk).exists()
        ):
631
632
            self.event.start_reminder.users.add(self.member)

Thom Wiggers's avatar
Thom Wiggers committed
633
634
    def __str__(self):
        if self.member:
Luko van der Maas's avatar
Luko van der Maas committed
635
            return "{}: {}".format(self.member.get_full_name(), self.event)
Thom Wiggers's avatar
Thom Wiggers committed
636
        else:
Luko van der Maas's avatar
Luko van der Maas committed
637
            return "{}: {}".format(self.name, self.event)
Thom Wiggers's avatar
Thom Wiggers committed
638
639

    class Meta:
Luko van der Maas's avatar
Luko van der Maas committed
640
641
        ordering = ("date",)
        unique_together = (("member", "event"),)
Thom Wiggers's avatar
Thom Wiggers committed
642
643


644
class RegistrationInformationField(models.Model, metaclass=ModelTranslateMeta):
645
    """Describes a field description to ask for when registering"""
646

Luko van der Maas's avatar
Luko van der Maas committed
647
648
649
650
651
652
653
654
655
    BOOLEAN_FIELD = "boolean"
    INTEGER_FIELD = "integer"
    TEXT_FIELD = "text"

    FIELD_TYPES = (
        (BOOLEAN_FIELD, _("Checkbox")),
        (TEXT_FIELD, _("Text")),
        (INTEGER_FIELD, _("Integer")),
    )
656
657
658

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

Luko van der Maas's avatar
Luko van der Maas committed
659
    type = models.CharField(_("field type"), choices=FIELD_TYPES, max_length=10,)
660

Luko van der Maas's avatar
Luko van der Maas committed
661
    name = MultilingualField(models.CharField, _("field name"), max_length=100,)
662
663

    description = MultilingualField(
Luko van der Maas's avatar
Luko van der Maas committed
664
        models.TextField, _("description"), null=True, blank=True,
665
666
    )

Luko van der Maas's avatar
Luko van der Maas committed
667
    required = models.BooleanField(_("required"),)
668
669
670
671
672
673
674
675
676
677
678

    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
Luko van der Maas's avatar
Luko van der Maas committed
679
680
681
682
683
        except (
            TextRegistrationInformation.DoesNotExist,
            BooleanRegistrationInformation.DoesNotExist,
            IntegerRegistrationInformation.DoesNotExist,
        ):
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
            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:
Luko van der Maas's avatar
Luko van der Maas committed
712
        order_with_respect_to = "event"
713
714


Thom Wiggers's avatar
Thom Wiggers committed
715
716
717
718
719
class AbstractRegistrationInformation(models.Model):
    """Abstract to contain common things for registration information"""

    registration = models.ForeignKey(Registration, models.CASCADE)
    field = models.ForeignKey(RegistrationInformationField, models.CASCADE)
Luko van der Maas's avatar
Luko van der Maas committed
720
    changed = models.DateTimeField(_("last changed"), auto_now=True)
Thom Wiggers's avatar
Thom Wiggers committed
721
722

    def __str__(self):
Luko van der Maas's avatar
Luko van der Maas committed
723
        return "{} - {}: {}".format(self.registration, self.field, self.value)
Thom Wiggers's avatar
Thom Wiggers committed
724
725
726
727
728
729

    class Meta:
        abstract = True


class BooleanRegistrationInformation(AbstractRegistrationInformation):
Joren Vrancken's avatar
Joren Vrancken committed
730
    """Checkbox information filled in by members when registering"""
Thom Wiggers's avatar
Thom Wiggers committed
731
732
733
734
735

    value = models.BooleanField()


class TextRegistrationInformation(AbstractRegistrationInformation):
Joren Vrancken's avatar
Joren Vrancken committed
736
    """Checkbox information filled in by members when registering"""
Luko van der Maas's avatar
Luko van der Maas committed
737

Thom Wiggers's avatar
Thom Wiggers committed
738
739
740
741
    value = models.TextField()


class IntegerRegistrationInformation(AbstractRegistrationInformation):
Joren Vrancken's avatar
Joren Vrancken committed
742
    """Checkbox information filled in by members when registering"""
Luko van der Maas's avatar
Luko van der Maas committed
743

Thom Wiggers's avatar
Thom Wiggers committed
744
    value = models.IntegerField()
745
746
747
748
749


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

Luko van der Maas's avatar
Luko van der Maas committed
750
    member = models.OneToOneField("members.Member", models.CASCADE)
751
752
753
754
755
756
757
758
759
760
761
762
763
764
    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):
Luko van der Maas's avatar
Luko van der Maas committed
765
        return "{} ({})".format(self.member.get_full_name(), self.token)