models.py 12.9 KB
Newer Older
1
"""The models defined by the activemembers package"""
Thom Wiggers's avatar
Thom Wiggers committed
2
import datetime
3
import logging
Thom Wiggers's avatar
Thom Wiggers committed
4

5
from django.conf import settings
6
from django.contrib.auth.models import Permission
7
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
8
from django.core.validators import MinValueValidator
Thom Wiggers's avatar
Thom Wiggers committed
9
from django.db import models
Thom Wiggers's avatar
Thom Wiggers committed
10
from django.urls import reverse
11
from django.utils import timezone
Thom Wiggers's avatar
Thom Wiggers committed
12
from django.utils.translation import ugettext_lazy as _
13
from tinymce import HTMLField
14

15 16
from utils.translation import (ModelTranslateMeta, MultilingualField,
                               localize_attr_name)
Thom Wiggers's avatar
Thom Wiggers committed
17

18 19 20
logger = logging.getLogger(__name__)


21 22
class ActiveMemberGroupManager(models.Manager):
    """Returns active objects only sorted by the localized name"""
23

Thom Wiggers's avatar
Thom Wiggers committed
24
    def get_queryset(self):
Thom Wiggers's avatar
Thom Wiggers committed
25
        return (super().get_queryset()
26 27
                .exclude(active=False)
                .order_by(localize_attr_name('name')))
Thom Wiggers's avatar
Thom Wiggers committed
28 29


30
class MemberGroup(models.Model, metaclass=ModelTranslateMeta):
31
    """Describes a groups of members"""
Thom Wiggers's avatar
Thom Wiggers committed
32

33
    objects = models.Manager()
34
    active_objects = ActiveMemberGroupManager()
Thom Wiggers's avatar
Thom Wiggers committed
35

36 37
    name = MultilingualField(
        models.CharField,
Thom Wiggers's avatar
Thom Wiggers committed
38
        max_length=40,
39
        verbose_name=_('Name'),
Thom Wiggers's avatar
Thom Wiggers committed
40 41 42
        unique=True,
    )

43
    description = MultilingualField(
44
        HTMLField,
Thom Wiggers's avatar
Thom Wiggers committed
45 46 47 48 49
        verbose_name=_('Description'),
    )

    photo = models.ImageField(
        verbose_name=_('Image'),
Thom Wiggers's avatar
Thom Wiggers committed
50
        upload_to='public/committeephotos/',
Thom Wiggers's avatar
Thom Wiggers committed
51 52
        null=True,
        blank=True,
Thom Wiggers's avatar
Thom Wiggers committed
53 54 55
    )

    members = models.ManyToManyField(
56
        'members.Member',
57
        through='activemembers.MemberGroupMembership'
Thom Wiggers's avatar
Thom Wiggers committed
58 59 60 61 62 63 64 65
    )

    permissions = models.ManyToManyField(
        Permission,
        verbose_name=_('permissions'),
        blank=True,
    )

Thom Wiggers's avatar
Thom Wiggers committed
66 67 68 69 70
    since = models.DateField(
        _('founded in'),
        null=True,
        blank=True,
    )
Thom Wiggers's avatar
Thom Wiggers committed
71

Thom Wiggers's avatar
Thom Wiggers committed
72 73 74 75 76
    until = models.DateField(
        _('existed until'),
        null=True,
        blank=True,
    )
Thom Wiggers's avatar
Thom Wiggers committed
77

78 79 80 81 82 83
    active = models.BooleanField(
        default=False,
        help_text=_("This should only be unchecked if the committee has been "
                    "dissolved. The websites assumes that any committees on it"
                    " existed at some point."),
    )
84

85 86 87 88 89 90
    contact_email = models.EmailField(
        _('contact email address'),
        blank=True,
        null=True,
    )

91 92
    contact_mailinglist = models.OneToOneField(
        'mailinglists.MailingList',
93
        verbose_name=_('contact mailing list'),
94 95
        null=True,
        blank=True,
Thom Wiggers's avatar
Thom Wiggers committed
96
        on_delete=models.SET_NULL,
97
    )
Thom Wiggers's avatar
Thom Wiggers committed
98

99 100 101 102
    display_members = models.BooleanField(
        default=False,
    )

103 104 105
    @property
    def contact_address(self):
        if self.contact_mailinglist:
106
            return f"{self.contact_mailinglist.name}@{settings.SITE_DOMAIN}"
107 108
        return self.contact_email

109 110 111 112 113 114 115 116 117 118 119 120 121 122
    def clean(self):
        if ((self.contact_email is not None and
                self.contact_mailinglist is not None) or
            (self.contact_email is None and
                self.contact_mailinglist is None)):
            raise ValidationError({
                'contact_email':
                    _("Please use either the mailing list "
                      "or email address option."),
                'contact_mailinglist':
                    _("Please use either the mailing list "
                      "or email address option.")
            })

Thom Wiggers's avatar
Thom Wiggers committed
123 124 125
    def __str__(self):
        return self.name

126 127 128 129 130 131 132
    def get_absolute_url(self):
        try:
            return self.board.get_absolute_url()
        except self.DoesNotExist:
            try:
                return self.committee.get_absolute_url()
            except self.DoesNotExist:
Thijs de Jong's avatar
Thijs de Jong committed
133 134 135 136
                try:
                    return self.society.get_absolute_url()
                except self.DoesNotExist:
                    pass
137

Thom Wiggers's avatar
Thom Wiggers committed
138
    class Meta:
139 140
        verbose_name = _('member group')
        verbose_name_plural = _('member groups')
141
        # ordering is done in the manager, to sort on a translated field
Thom Wiggers's avatar
Thom Wiggers committed
142 143


144
class Committee(MemberGroup):
145 146 147 148 149
    """Describes a committee, which is a type of MemberGroup"""

    objects = models.Manager()
    active_objects = ActiveMemberGroupManager()

150 151 152 153 154
    wiki_namespace = models.CharField(
        _('Wiki namespace'),
        null=True,
        blank=True,
        max_length=50)
Thom Wiggers's avatar
Thom Wiggers committed
155

Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
156 157 158
    def get_absolute_url(self):
        return reverse('activemembers:committee', args=[str(self.pk)])

159 160 161 162
    class Meta:
        verbose_name = _('committee')
        verbose_name_plural = _('committees')
        # ordering is done in the manager, to sort on a translated field
Thom Wiggers's avatar
Thom Wiggers committed
163 164


165
class Society(MemberGroup):
166 167 168 169 170
    """Describes a society, which is a type of MemberGroup"""

    objects = models.Manager()
    active_objects = ActiveMemberGroupManager()

Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
171 172 173
    def get_absolute_url(self):
        return reverse('activemembers:society', args=[str(self.pk)])

174 175 176 177
    class Meta:
        verbose_name = _('society')
        verbose_name_plural = _('societies')
        # ordering is done in the manager, to sort on a translated field
Thom Wiggers's avatar
Thom Wiggers committed
178 179


180
class Board(MemberGroup):
181 182
    """Describes a board, which is a type of MemberGroup"""

183
    class Meta:
184 185
        verbose_name = _('board')
        verbose_name_plural = _('boards')
186
        ordering = ['-since']
187 188 189
        permissions = (
            ('board_wiki', _("Access the board wiki")),
        )
190

191 192 193 194
    def save(self, *args, **kwargs):
        self.active = True
        super().save(*args, **kwargs)

Thom Wiggers's avatar
Thom Wiggers committed
195
    def get_absolute_url(self):
196 197
        return reverse('activemembers:board', args=[str(self.since.year),
                                                    str(self.until.year)])
198 199 200

    def validate_unique(self, *args, **kwargs):
        super().validate_unique(*args, **kwargs)
201 202 203 204 205 206 207
        boards = Board.objects.all()
        if self.since is not None:
            for board in boards:
                if board.pk == self.pk:
                    continue
                if ((board.until is None and (
                        self.until is None or self.until >= board.since)) or
208 209 210 211
                        (self.until is None and self.since <= board.until) or
                        (self.until and board.until and
                            self.since <= board.until and
                            self.until >= board.since)):
212 213 214
                    raise ValidationError({
                        'since': _('A board already exists for those years'),
                        'until': _('A board already exists for those years')})
Thom Wiggers's avatar
Thom Wiggers committed
215

Thom Wiggers's avatar
Thom Wiggers committed
216

Thom Wiggers's avatar
Thom Wiggers committed
217
class ActiveMembershipManager(models.Manager):
218
    """
219
    Custom manager that gets the currently active membergroup memberships
220
    """
221

Thom Wiggers's avatar
Thom Wiggers committed
222
    def get_queryset(self):
223
        return super().get_queryset().exclude(until__lt=timezone.now().date())
Thom Wiggers's avatar
Thom Wiggers committed
224 225


226 227
class MemberGroupMembership(models.Model, metaclass=ModelTranslateMeta):
    """Describes a group membership"""
Thom Wiggers's avatar
Thom Wiggers committed
228
    objects = models.Manager()
229
    active_objects = ActiveMembershipManager()
Thom Wiggers's avatar
Thom Wiggers committed
230 231

    member = models.ForeignKey(
232
        'members.Member',
Thom Wiggers's avatar
Thom Wiggers committed
233 234 235 236
        on_delete=models.CASCADE,
        verbose_name=_('Member'),
    )

237
    group = models.ForeignKey(
238
        MemberGroup,
Thom Wiggers's avatar
Thom Wiggers committed
239
        on_delete=models.CASCADE,
240
        verbose_name=_('Group'),
Thom Wiggers's avatar
Thom Wiggers committed
241 242 243
    )

    since = models.DateField(
244 245
        verbose_name=_('Member since'),
        help_text=_('The date this member joined in this role'),
Thom Wiggers's avatar
Thom Wiggers committed
246
        default=datetime.date.today
Thom Wiggers's avatar
Thom Wiggers committed
247 248 249
    )

    until = models.DateField(
250 251
        verbose_name=_('Member until'),
        help_text=_("A member until this time "
Thom Wiggers's avatar
Thom Wiggers committed
252 253 254 255 256 257
                    "(can't be in the future)."),
        blank=True,
        null=True,
    )

    chair = models.BooleanField(
258
        verbose_name=_('Chair of the group'),
Thom Wiggers's avatar
Thom Wiggers committed
259 260 261 262
        help_text=_('There can only be one chair at a time!'),
        default=False,
    )

263 264
    role = MultilingualField(
        models.CharField,
Thom Wiggers's avatar
Thom Wiggers committed
265 266 267 268 269 270 271
        _('role'),
        help_text=_('The role of this member'),
        max_length=255,
        blank=True,
        null=True,
    )

272 273
    @property
    def initial_connected_membership(self):
274
        """Find the oldest membership directly connected to the current one"""
275 276
        qs = MemberGroupMembership.objects.filter(
            group=self.group,
277 278 279
            member=self.member,
            until__lte=self.since,
            until__gte=self.since - datetime.timedelta(days=1))
280
        if qs.count() >= 1:  # should only be one; should be unique
281 282 283
            return qs.first().initial_connected_membership
        else:
            return self
284 285 286

    @property
    def latest_connected_membership(self):
287 288 289 290
        """
        Find the newest membership directly connected to the current one
        (thus the membership that started at the moment the current one ended)
        """
291 292 293 294 295 296 297 298 299
        if self.until:
            qs = MemberGroupMembership.objects.filter(
                group=self.group,
                member=self.member,
                since__lte=self.until,
                since__gte=self.until + datetime.timedelta(days=1))
            if qs.count() >= 1:  # should only be one; should be unique
                return qs.last().latest_connected_membership
        return self
300

Thom Wiggers's avatar
Thom Wiggers committed
301 302 303
    @property
    def is_active(self):
        """Is this membership currently active"""
304
        return self.until is None or self.until > timezone.now().date()
Thom Wiggers's avatar
Thom Wiggers committed
305 306 307 308 309

    def clean(self):
        if self.until and (not self.since or self.until < self.since):
            raise ValidationError(
                {'until': _("End date can't be before start date")})
310 311 312
        if self.until and self.until > timezone.now().date():
            raise ValidationError(
                {'until': _("End date can't be in the future")})
313

314 315
        if (self.since and self.group.since and
                self.since < self.group.since):
316
            raise ValidationError(
317
                {'since': _("Start date can't be before group start date")}
318
                )
319 320
        if (self.since and self.group.until and
                self.since > self.group.until):
321
            raise ValidationError(
322
                {'since': _("Start date can't be after group end date")})
323

Thom Wiggers's avatar
Thom Wiggers committed
324 325
    def validate_unique(self, *args, **kwargs):
        super().validate_unique(*args, **kwargs)
326
        # Check if a group has more than one chair
327
        if self.chair:
328 329
            chairs = (MemberGroupMembership.objects
                      .filter(group=self.group,
330 331 332 333 334 335
                              chair=True))
            for chair in chairs:
                if chair.pk == self.pk:
                    continue
                if ((chair.until is None and
                        (self.until is None or self.until > chair.since)) or
336 337 338 339
                        (self.until is None and self.since < chair.until) or
                        (self.until and chair.until and
                            self.since < chair.until and
                            self.until > chair.since)):
340 341
                    raise ValidationError({
                        NON_FIELD_ERRORS:
342 343
                            _('There already is a '
                              'chair for this time period')})
344

345
        # check if this member is already in the group in this period
346 347
        memberships = (MemberGroupMembership.objects
                       .filter(group=self.group,
348 349 350 351 352 353
                               member=self.member))
        for mship in memberships:
            if mship.pk == self.pk:
                continue
            if ((mship.until is None and
                    (self.until is None or self.until > mship.since)) or
354 355 356 357
                    (self.until is None and self.since < mship.until) or
                    (self.until and mship.until and
                        self.since < mship.until and
                        self.until > mship.since)):
358
                raise ValidationError({
359
                    'member': _('This member is already in the group for '
360
                                'this period')})
361 362 363

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
364
        self.member.is_staff = (self.member
365
                                .membergroupmembership_set
366 367 368
                                .exclude(until__lte=timezone.now().date())
                                .count()) >= 1
        self.member.save()
369

Thom Wiggers's avatar
Thom Wiggers committed
370
    def __str__(self):
371 372 373 374 375
        return _("{member} membership of {group} "
                 "since {since}, until {until}").format(member=self.member,
                                                        group=self.group,
                                                        since=self.since,
                                                        until=self.until)
Thom Wiggers's avatar
Thom Wiggers committed
376 377

    class Meta:
378 379
        verbose_name = _('group membership')
        verbose_name_plural = _('group memberships')
Joost Rijneveld's avatar
Joost Rijneveld committed
380 381


382
class Mentorship(models.Model):
383
    """Describe a mentorship during the orientation"""
384
    member = models.ForeignKey(
385
        'members.Member',
386 387 388
        on_delete=models.CASCADE,
        verbose_name=_('Member'),
    )
Thom Wiggers's avatar
Thom Wiggers committed
389
    year = models.IntegerField(validators=[MinValueValidator(1990)])
Joost Rijneveld's avatar
Joost Rijneveld committed
390 391

    def __str__(self):
392 393 394 395 396
        return _("{name} mentor in {year}").format(name=self.member,
                                                   year=self.year)

    class Meta:
        unique_together = ('member', 'year')