models.py 8.74 KB
Newer Older
Thom Wiggers's avatar
Thom Wiggers committed
1
import datetime
2
import logging
Thom Wiggers's avatar
Thom Wiggers committed
3
4

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

Thom Wiggers's avatar
Thom Wiggers committed
12
from members.models import Member
13
14
from utils.translation import (ModelTranslateMeta, MultilingualField,
                               localize_attr_name)
Thom Wiggers's avatar
Thom Wiggers committed
15

16
17
18
logger = logging.getLogger(__name__)


19
20
21
22
23
24
25
class UnfilteredSortedManager(models.Manager):
    """Returns committees and boards, sorted by name"""
    def get_queryset(self):
        return (super().get_queryset()
                .order_by(localize_attr_name('name')))


Thom Wiggers's avatar
Thom Wiggers committed
26
27
28
29
class CommitteeManager(models.Manager):
    """Returns committees only"""
    def get_queryset(self):
        return (super().get_queryset()
30
31
                .exclude(board__is_board=True)
                .order_by(localize_attr_name('name')))
Thom Wiggers's avatar
Thom Wiggers committed
32
33
34


class ActiveCommitteeManager(models.Manager):
Thom Wiggers's avatar
Thom Wiggers committed
35
36
    """Returns active committees only"""
    def get_queryset(self):
Thom Wiggers's avatar
Thom Wiggers committed
37
        return (super().get_queryset()
38
                .exclude(board__is_board=True)
39
40
                .exclude(active=False)
                .order_by(localize_attr_name('name')))
Thom Wiggers's avatar
Thom Wiggers committed
41
42


43
class Committee(models.Model, metaclass=ModelTranslateMeta):
Thom Wiggers's avatar
Thom Wiggers committed
44
45
    """A committee"""

46
    unfiltered_objects = UnfilteredSortedManager()
Thom Wiggers's avatar
Thom Wiggers committed
47
    objects = CommitteeManager()
48
    active_committees = ActiveCommitteeManager()
Thom Wiggers's avatar
Thom Wiggers committed
49

50
51
    name = MultilingualField(
        models.CharField,
Thom Wiggers's avatar
Thom Wiggers committed
52
53
54
55
56
        max_length=40,
        verbose_name=_('Committee name'),
        unique=True,
    )

57
58
    description = MultilingualField(
        models.TextField,
Thom Wiggers's avatar
Thom Wiggers committed
59
60
61
62
63
        verbose_name=_('Description'),
    )

    photo = models.ImageField(
        verbose_name=_('Image'),
Thom Wiggers's avatar
Thom Wiggers committed
64
        upload_to='public/committeephotos/',
Thom Wiggers's avatar
Thom Wiggers committed
65
66
        null=True,
        blank=True,
Thom Wiggers's avatar
Thom Wiggers committed
67
68
69
70
71
72
73
74
75
76
77
78
79
    )

    members = models.ManyToManyField(
        Member,
        through='CommitteeMembership'
    )

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

Thom Wiggers's avatar
Thom Wiggers committed
80
81
82
83
84
    since = models.DateField(
        _('founded in'),
        null=True,
        blank=True,
    )
Thom Wiggers's avatar
Thom Wiggers committed
85

Thom Wiggers's avatar
Thom Wiggers committed
86
87
88
89
90
    until = models.DateField(
        _('existed until'),
        null=True,
        blank=True,
    )
Thom Wiggers's avatar
Thom Wiggers committed
91

92
93
    active = models.BooleanField(default=False)

Thom Wiggers's avatar
Thom Wiggers committed
94
95
    contact_email = models.EmailField(_('contact email address'))

Thom Wiggers's avatar
Thom Wiggers committed
96
97
98
99
100
101
    wiki_namespace = models.CharField(
        _('Wiki namespace'),
        null=True,
        blank=True,
        max_length=50)

Thom Wiggers's avatar
Thom Wiggers committed
102
103
104
    def __str__(self):
        return self.name

Thom Wiggers's avatar
Thom Wiggers committed
105
    def get_absolute_url(self):
106
        return reverse('activemembers:committee', args=[str(self.pk)])
Thom Wiggers's avatar
Thom Wiggers committed
107

Thom Wiggers's avatar
Thom Wiggers committed
108
109
110
    class Meta:
        verbose_name = _('committee')
        verbose_name_plural = _('committees')
111
        # ordering is done in the manager, to sort on a translated field
Thom Wiggers's avatar
Thom Wiggers committed
112
113


Thom Wiggers's avatar
Thom Wiggers committed
114
115
class BoardManager(models.Manager):
    def get_queryset(self):
116
117
118
119
        # sorting by descending order by default makes more sense for boards
        return (super().get_queryset()
                       .filter(is_board=True)
                       .order_by(localize_attr_name('-name')))
Thom Wiggers's avatar
Thom Wiggers committed
120
121
122


class Board(Committee):
123
124
125
126
127
    """ Because Board inherits from Committee, Django creates a OneToOneField
    linking the two models together. This can be accessed as usual;
    given a Committee or Board b, one can access b.board, which will either
    return the object b if b is a Board, or a Board.DoesNotExist exception.
    """
Thom Wiggers's avatar
Thom Wiggers committed
128
129
130
131
132
133
134
    objects = BoardManager()

    is_board = models.BooleanField(
        verbose_name=_('Is this a board'),
        default=True,
    )

135
136
137
    class Meta:
        ordering = ['-since']

Thom Wiggers's avatar
Thom Wiggers committed
138
    def get_absolute_url(self):
139
        return reverse('activemembers:board', args=[str(self.pk)])
Thom Wiggers's avatar
Thom Wiggers committed
140

Thom Wiggers's avatar
Thom Wiggers committed
141

Thom Wiggers's avatar
Thom Wiggers committed
142
143
144
145
class ActiveMembershipManager(models.Manager):
    """Get only active memberships"""
    def get_queryset(self):
        """Get the currently active committee memberships"""
146
        return super().get_queryset().exclude(until__lt=timezone.now().date())
Thom Wiggers's avatar
Thom Wiggers committed
147
148


149
class CommitteeMembership(models.Model, metaclass=ModelTranslateMeta):
Thom Wiggers's avatar
Thom Wiggers committed
150
    objects = models.Manager()
Thom Wiggers's avatar
Thom Wiggers committed
151
    active_memberships = ActiveMembershipManager()
Thom Wiggers's avatar
Thom Wiggers committed
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167

    member = models.ForeignKey(
        Member,
        on_delete=models.CASCADE,
        verbose_name=_('Member'),
    )

    committee = models.ForeignKey(
        Committee,
        on_delete=models.CASCADE,
        verbose_name=_('Committee'),
    )

    since = models.DateField(
        verbose_name=_('Committee member since'),
        help_text=_('The date this member joined the committee in this role'),
Thom Wiggers's avatar
Thom Wiggers committed
168
        default=datetime.date.today
Thom Wiggers's avatar
Thom Wiggers committed
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
    )

    until = models.DateField(
        verbose_name=_('Committee member until'),
        help_text=_("A member of this committee until this time "
                    "(can't be in the future)."),
        blank=True,
        null=True,
    )

    chair = models.BooleanField(
        verbose_name=_('Chair of the committee'),
        help_text=_('There can only be one chair at a time!'),
        default=False,
    )

185
186
    role = MultilingualField(
        models.CharField,
Thom Wiggers's avatar
Thom Wiggers committed
187
188
189
190
191
192
193
        _('role'),
        help_text=_('The role of this member'),
        max_length=255,
        blank=True,
        null=True,
    )

Thom Wiggers's avatar
Thom Wiggers committed
194
195
196
    @property
    def is_active(self):
        """Is this membership currently active"""
197
        return self.until is None or self.until > timezone.now().date()
Thom Wiggers's avatar
Thom Wiggers committed
198
199
200
201
202
203

    def clean(self):
        """Validation"""
        if self.until and (not self.since or self.until < self.since):
            raise ValidationError(
                {'until': _("End date can't be before start date")})
204
205
206
207
208
209
        try:
            if self.until and self.committee.board:
                raise ValidationError(
                    {'until': _("End date cannot be set for boards")})
        except Board.DoesNotExist:
            pass
Thom Wiggers's avatar
Thom Wiggers committed
210
211
212
213
214

    def validate_unique(self, *args, **kwargs):
        """ Check uniqueness"""
        super().validate_unique(*args, **kwargs)
        # Check if a committee has more than one chair
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
        if self.chair:
            chairs = (CommitteeMembership.objects
                      .filter(committee=self.committee,
                              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
                    (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)):
                    raise ValidationError({
                        NON_FIELD_ERRORS:
                        _('There already is a chair for this time period')})

        # check if this member is already in the committee in this period
        memberships = (CommitteeMembership.objects
                       .filter(committee=self.committee,
                               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
                (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)):
245
246
247
                raise ValidationError({
                    'member': _('This member is already in the committee for '
                                'this period')})
248
249
250

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
251
252
253
254
255
256
        self.member.user.is_staff = (
            self.member
            .committeemembership_set
            .exclude(
                until__lt=timezone.now().date())
            .count()) >= 1
257
258
        self.member.user.save()

Thom Wiggers's avatar
Thom Wiggers committed
259
    def __str__(self):
260
261
262
263
        return "{} membership of {} since {}, until {}".format(self.member,
                                                               self.committee,
                                                               self.since,
                                                               self.until)
Thom Wiggers's avatar
Thom Wiggers committed
264
265
266
267

    class Meta:
        verbose_name = _('committee membership')
        verbose_name_plural = _('committee memberships')
Joost Rijneveld's avatar
Joost Rijneveld committed
268
269


270
class Mentorship(models.Model):
271
272
273
274
275
276
    member = models.ForeignKey(
        Member,
        on_delete=models.CASCADE,
        verbose_name=_('Member'),
    )
    year = models.IntegerField(validators=MinValueValidator(1990))
Joost Rijneveld's avatar
Joost Rijneveld committed
277
278

    def __str__(self):
279
280
281
282
283
        return _("{name} mentor in {year}").format(name=self.member,
                                                   year=self.year)

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