models.py 13.8 KB
Newer Older
Thom Wiggers's avatar
Thom Wiggers committed
1
2
3
4
from django.core import validators
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Q
5
from django.urls import reverse
Thom Wiggers's avatar
Thom Wiggers committed
6
from django.utils import timezone
7
8
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import string_concat
9
from tinymce.models import HTMLField
10

11
from thaliawebsite.settings import settings
12
from utils.translation import ModelTranslateMeta, MultilingualField
Thom Wiggers's avatar
Thom Wiggers committed
13
14


15
class Event(models.Model, metaclass=ModelTranslateMeta):
Thom Wiggers's avatar
Thom Wiggers committed
16
17
    """Represents events"""

18
19
20
21
22
23
24
    REGISTRATION_NOT_NEEDED = -1
    REGISTRATION_NOT_YET_OPEN = 0
    REGISTRATION_OPEN = 1
    REGISTRATION_OPEN_NO_CANCEL = 2
    REGISTRATION_CLOSED = 3
    REGISTRATION_CLOSED_CANCEL_ONLY = 4

Thom Wiggers's avatar
Thom Wiggers committed
25
26
    DEFAULT_NO_REGISTRATION_MESSAGE = _('No registration required')

27
28
29
30
31
    title = MultilingualField(
        models.CharField,
        _("title"),
        max_length=100
    )
Thom Wiggers's avatar
Thom Wiggers committed
32

33
    description = MultilingualField(
34
        HTMLField,
35
36
        _("description")
    )
Thom Wiggers's avatar
Thom Wiggers committed
37
38
39
40
41
42

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

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

    organiser = models.ForeignKey(
43
        'activemembers.Committee',
Thom Wiggers's avatar
Thom Wiggers committed
44
45
        models.SET_NULL,
        null=True,
46
        verbose_name=_("organiser")
Thom Wiggers's avatar
Thom Wiggers committed
47
48
49
50
51
52
    )

    registration_start = models.DateTimeField(
        _("registration start"),
        null=True,
        blank=True,
53
54
55
        help_text=_("If you set a registration period registration will be "
                    "required. If you don't set one, registration won't be "
                    "required.")
Thom Wiggers's avatar
Thom Wiggers committed
56
57
58
59
60
    )

    registration_end = models.DateTimeField(
        _("registration end"),
        null=True,
61
62
63
64
        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.")
65
66
67
68
69
    )

    cancel_deadline = models.DateTimeField(
        _("cancel deadline"),
        null=True,
Thom Wiggers's avatar
Thom Wiggers committed
70
71
72
        blank=True
    )

73
74
75
76
77
    location = MultilingualField(
        models.CharField,
        _("location"),
        max_length=255,
    )
Thom Wiggers's avatar
Thom Wiggers committed
78
79
80
81
82

    map_location = models.CharField(
        _("location for minimap"),
        max_length=255,
        help_text=_('Location of Huygens: Heyendaalseweg 135, Nijmegen. '
83
                    'Location of Mercator 1: Toernooiveld 212, Nijmegen. '
Thom Wiggers's avatar
Thom Wiggers committed
84
85
86
87
88
89
90
91
92
93
94
                    'Not shown as text!!'),
    )

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

95
96
    fine = models.DecimalField(
        _("fine"),
Thom Wiggers's avatar
Thom Wiggers committed
97
98
99
        max_digits=5,
        decimal_places=2,
        default=0,
100
101
        help_text=_("Fine if participant does not show up (at least €5)."),
        validators=[validators.MinValueValidator(5)],
Thom Wiggers's avatar
Thom Wiggers committed
102
103
104
105
106
107
108
109
    )

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

110
111
    no_registration_message = MultilingualField(
        models.CharField,
Thom Wiggers's avatar
Thom Wiggers committed
112
113
114
115
        _('message when there is no registration'),
        max_length=200,
        blank=True,
        null=True,
116
117
        help_text=(string_concat(_("Default: "),
                                 DEFAULT_NO_REGISTRATION_MESSAGE)),
Thom Wiggers's avatar
Thom Wiggers committed
118
119
120
121
    )

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

122
123
    def after_cancel_deadline(self):
        return self.cancel_deadline <= timezone.now()
124

125
126
127
    def registration_required(self):
        return bool(self.registration_start) or bool(self.registration_end)

128
129
130
    def has_fields(self):
        return self.registrationinformationfield_set.count() > 0

Tom van Bussel's avatar
Tom van Bussel committed
131
132
133
    def num_participants(self):
        return self.registration_set.filter(date_cancelled=None).count()

134
    def reached_participants_limit(self):
135
        return (self.max_participants is not None and
136
137
                self.max_participants <= self.registration_set.filter(
                    date_cancelled=None).count())
138

139
140
141
142
143
144
145
    def is_member_registered(self, member):
        if not self.registration_required():
            return None

        registrations = self.registration_set.filter(member=member)
        return len(registrations) == 1 and registrations[0].is_registered()

146
    @property
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
    def status(self):
        now = timezone.now()
        if bool(self.registration_start) or bool(self.registration_end):
            if now <= self.registration_start:
                return Event.REGISTRATION_NOT_YET_OPEN
            elif self.registration_end <= now < self.cancel_deadline:
                return Event.REGISTRATION_CLOSED_CANCEL_ONLY
            elif self.cancel_deadline <= now < self.registration_end:
                return Event.REGISTRATION_OPEN_NO_CANCEL
            elif now >= self.registration_end and now >= self.cancel_deadline:
                return Event.REGISTRATION_CLOSED
            else:
                return Event.REGISTRATION_OPEN
        else:
            return Event.REGISTRATION_NOT_NEEDED

Thom Wiggers's avatar
Thom Wiggers committed
163
164
    def clean(self):
        super().clean()
Thom Wiggers's avatar
Thom Wiggers committed
165
        errors = {}
166
167
        if self.end is not None and self.start is not None and (
                    self.end < self.start):
Thom Wiggers's avatar
Thom Wiggers committed
168
            errors.update({
169
                'end': _("Can't have an event travel back in time")})
170
        if self.registration_required():
171
172
173
174
175
176
            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.")})
Thom Wiggers's avatar
Thom Wiggers committed
177
178
179
180
181
182
183
184
185
186
            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")})
187
188
189
190
191
            if not self.cancel_deadline:
                errors.update(
                    {'cancel_deadline': _(
                        "If registration is required, you need a deadline for "
                        "the cancellation")})
Thom Wiggers's avatar
Thom Wiggers committed
192
            if self.registration_start and self.registration_end and (
193
                        self.registration_start >= self.registration_end):
Thom Wiggers's avatar
Thom Wiggers committed
194
195
196
197
198
199
200
                message = _('Registration start should be before '
                            'registration end')
                errors.update({
                    'registration_start': message,
                    'registration_end': message})
        if errors:
            raise ValidationError(errors)
Thom Wiggers's avatar
Thom Wiggers committed
201
202

    def get_absolute_url(self):
203
        return reverse('events:event', args=[str(self.pk)])
Thom Wiggers's avatar
Thom Wiggers committed
204
205
206
207
208
209
210
211

    def __str__(self):
        return '{}: {}'.format(
            self.title,
            timezone.localtime(self.start).strftime('%Y-%m-%d %H:%M'))

    class Meta:
        ordering = ('-start',)
212
213
214
        permissions = (
            ("override_organiser", "Can access events as if organizing"),
        )
Thom Wiggers's avatar
Thom Wiggers committed
215
216


Joren Vrancken's avatar
Joren Vrancken committed
217
class RegistrationInformationField(models.Model, metaclass=ModelTranslateMeta):
Thom Wiggers's avatar
Thom Wiggers committed
218
    """Field description to ask for when registering"""
219
220
221
222
223
224
225
    BOOLEAN_FIELD = 'boolean'
    INTEGER_FIELD = 'integer'
    TEXT_FIELD = 'text'

    FIELD_TYPES = ((BOOLEAN_FIELD, _('Checkbox')),
                   (TEXT_FIELD, _('Text')),
                   (INTEGER_FIELD, _('Integer')),)
Thom Wiggers's avatar
Thom Wiggers committed
226
227
228
229
230
231
232
233
234

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

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

Joren Vrancken's avatar
Joren Vrancken committed
235
236
    name = MultilingualField(
        models.CharField,
Thom Wiggers's avatar
Thom Wiggers committed
237
238
239
240
        _('field name'),
        max_length=100,
    )

Joren Vrancken's avatar
Joren Vrancken committed
241
242
    description = MultilingualField(
        models.TextField,
Thom Wiggers's avatar
Thom Wiggers committed
243
244
245
246
247
        _('description'),
        null=True,
        blank=True,
    )

248
249
250
251
    required = models.BooleanField(
        _('required'),
    )

Thom Wiggers's avatar
Thom Wiggers committed
252
    def get_value_for(self, registration):
253
        if self.type == self.TEXT_FIELD:
Thom Wiggers's avatar
Thom Wiggers committed
254
            value_set = self.textregistrationinformation_set
255
        elif self.type == self.BOOLEAN_FIELD:
Thom Wiggers's avatar
Thom Wiggers committed
256
            value_set = self.booleanregistrationinformation_set
257
        elif self.type == self.INTEGER_FIELD:
Thom Wiggers's avatar
Thom Wiggers committed
258
            value_set = self.integerregistrationinformation_set
259

Thom Wiggers's avatar
Thom Wiggers committed
260
261
262
263
264
265
266
        try:
            return value_set.get(registration=registration).value
        except (TextRegistrationInformation.DoesNotExist,
                BooleanRegistrationInformation.DoesNotExist,
                IntegerRegistrationInformation.DoesNotExist):
            return None

267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
    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])

Thom Wiggers's avatar
Thom Wiggers committed
292
293
294
295
296
297
298
299
300
301
302
303
304
305
    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) |
306
                          Q(user__membership__until__gt=timezone.now().date()))
Thom Wiggers's avatar
Thom Wiggers committed
307
308
309
310
311
312
313
314
315
316
    )

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

317
318
    date = models.DateTimeField(_('registration date'),
                                default=timezone.now)
Thom Wiggers's avatar
Thom Wiggers committed
319
320
321
322
323
    date_cancelled = models.DateTimeField(_('cancellation date'),
                                          null=True,
                                          blank=True)

    present = models.BooleanField(
324
        _('present'),
Thom Wiggers's avatar
Thom Wiggers committed
325
326
327
        default=False,
    )
    paid = models.BooleanField(
328
        _('paid'),
Thom Wiggers's avatar
Thom Wiggers committed
329
330
331
332
333
334
335
336
        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]

337
338
339
    def is_external(self):
        return bool(self.name)

Luuk Scholten's avatar
Luuk Scholten committed
340
    def is_late_cancellation(self):
341
342
        # First check whether or not the user cancelled
        # If the user cancelled then check if this was after the deadline
343
344
        # And if there is a max participants number:
        # do a complex check to calculate if this user was on
345
346
347
        # 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
348
        return (self.date_cancelled and
349
                self.event.cancel_deadline and
350
                self.date_cancelled > self.event.cancel_deadline and
351
352
                (self.event.max_participants is None or
                 self.event.registration_set.filter(
353
354
                    (Q(date_cancelled__gte=self.date_cancelled) |
                     Q(date_cancelled=None)) &
355
                    Q(date__lte=self.date)
356
                 ).count() < self.event.max_participants))
Luuk Scholten's avatar
Luuk Scholten committed
357
358
359
360
361
362
363
364
365
366

    def is_registered(self):
        return self.date_cancelled is None

    def queue_position(self):
        if self.event.max_participants is None:
            return 0

        return max(self.event.registration_set.filter(
            date_cancelled=None,
367
            date__lte=self.date
Luuk Scholten's avatar
Luuk Scholten committed
368
369
        ).count() - self.event.max_participants, 0)

Thom Wiggers's avatar
Thom Wiggers committed
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
    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):
Joren Vrancken's avatar
Joren Vrancken committed
407
    """Checkbox information filled in by members when registering"""
Thom Wiggers's avatar
Thom Wiggers committed
408
409
410
411
412

    value = models.BooleanField()


class TextRegistrationInformation(AbstractRegistrationInformation):
Joren Vrancken's avatar
Joren Vrancken committed
413
    """Checkbox information filled in by members when registering"""
Thom Wiggers's avatar
Thom Wiggers committed
414
415
416
417
    value = models.TextField()


class IntegerRegistrationInformation(AbstractRegistrationInformation):
Joren Vrancken's avatar
Joren Vrancken committed
418
    """Checkbox information filled in by members when registering"""
Thom Wiggers's avatar
Thom Wiggers committed
419
    value = models.IntegerField()