-
-
Notifications
You must be signed in to change notification settings - Fork 684
Expand file tree
/
Copy pathmodels.py
More file actions
303 lines (229 loc) · 10.3 KB
/
Copy pathmodels.py
File metadata and controls
303 lines (229 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
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
292
293
294
295
296
297
298
299
300
301
302
303
"""Models for PSF board elections, nominees, and nominations."""
import datetime
from colorfield.fields import ColorField
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.urls import reverse
from django.utils import timezone
from django.utils.text import slugify
from markupfield.fields import MarkupField
from apps.users.models import User
from fastly.utils import purge_url
DEFAULT_ACCENT_COLOR = "#0073b7"
class ElectionKind(models.Model):
"""An admin-managed category of election (Board, Packaging Council, ...).
Each kind carries an accent color used to visually distinguish its
election pages. New kinds can be added in the Django admin without
code changes.
"""
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(max_length=120, unique=True, blank=True, null=True)
accent_color = ColorField(default=DEFAULT_ACCENT_COLOR, help_text="Accent color used to theme this kind's pages.")
class Meta:
"""Meta configuration for ElectionKind."""
ordering = ["name"]
def __str__(self):
"""Return the kind name."""
return self.name
def save(self, *args, **kwargs):
"""Generate slug from name before saving."""
self.slug = slugify(self.name)
super().save(*args, **kwargs)
class Election(models.Model):
"""A PSF board election with nomination open/close dates."""
name = models.CharField(max_length=100)
date = models.DateField()
kind = models.ForeignKey(
ElectionKind,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="elections",
)
nominations_open_at = models.DateTimeField(blank=True, null=True)
nominations_close_at = models.DateTimeField(blank=True, null=True)
description = MarkupField(escape_html=False, markup_type="markdown", blank=False, null=True)
slug = models.SlugField(max_length=255, blank=True, null=True) # noqa: DJ001
class Meta:
"""Meta configuration for Election."""
ordering = ["-date"]
def __str__(self):
"""Return election name and date."""
return f"{self.name} - {self.date}"
def save(self, *args, **kwargs):
"""Generate slug from name before saving."""
self.slug = slugify(self.name)
super().save(*args, **kwargs)
@property
def accent_color(self):
"""Return the CSS accent color for this election's kind."""
return self.kind.accent_color if self.kind else DEFAULT_ACCENT_COLOR
@property
def nominations_open(self):
"""Return True if the current time is within the nomination window."""
if self.nominations_open_at and self.nominations_close_at:
return self.nominations_open_at < datetime.datetime.now(datetime.UTC) < self.nominations_close_at
return False
@property
def nominations_complete(self):
"""Return True if the nomination window has closed."""
if self.nominations_close_at:
return self.nominations_close_at < datetime.datetime.now(datetime.UTC)
return False
@property
def status(self):
"""Return human-readable nomination status string."""
if self.nominations_open_at is not None and self.nominations_close_at is not None:
if not self.nominations_open:
if self.nominations_open_at > datetime.datetime.now(datetime.UTC):
return "Nominations Not Yet Open"
return "Nominations Closed"
return "Nominations Open"
if self.date >= timezone.now().date():
return "Commenced"
return "Voting Not Yet Begun"
class Nominee(models.Model):
"""A user nominated as a candidate in a PSF board election."""
election = models.ForeignKey(Election, related_name="nominees", on_delete=models.CASCADE)
user = models.ForeignKey(
User,
related_name="nominations_recieved",
null=True,
on_delete=models.CASCADE,
blank=True,
)
accepted = models.BooleanField(null=False, default=False)
approved = models.BooleanField(null=False, default=False)
slug = models.SlugField(max_length=255, blank=True, null=True) # noqa: DJ001
class Meta:
"""Meta configuration for Nominee."""
unique_together = ("user", "election")
def __str__(self):
"""Return the nominee's full name."""
return f"{self.name}"
def save(self, *args, **kwargs):
"""Generate slug from name before saving."""
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def get_absolute_url(self):
"""Return the URL for the nominee detail page."""
return reverse(
"nominations:nominee_detail",
kwargs={"election": self.election.slug, "slug": self.slug},
)
@property
def name(self):
"""Return the nominee's first and last name."""
return f"{self.user.first_name} {self.user.last_name}"
@property
def nominations_received(self):
"""Return accepted and approved nominations excluding self-nominations."""
return self.nominations.filter(accepted=True, approved=True).exclude(nominator=self.user).all()
@property
def nominations_pending(self):
"""Return pending nominations excluding self-nominations."""
return self.nominations.exclude(accepted=False, approved=False).exclude(nominator=self.user).all()
@property
def self_nomination(self):
"""Return the self-nomination for this nominee, if any."""
return self.nominations.filter(nominator=self.user).first()
@property
def display_name(self):
"""Return the display name for the nominee."""
return self.name
@property
def display_previous_board_service(self):
"""Return previous board service info, preferring self-nomination data."""
if self.self_nomination is not None and self.self_nomination.previous_board_service:
return self.self_nomination.previous_board_service
return self.nominations.first().previous_board_service
@property
def display_employer(self):
"""Return employer info, preferring self-nomination data."""
if self.self_nomination is not None and self.self_nomination.employer:
return self.self_nomination.employer
return self.nominations.first().employer
@property
def display_other_affiliations(self):
"""Return other affiliations, preferring self-nomination data."""
if self.self_nomination is not None and self.self_nomination.other_affiliations:
return self.self_nomination.other_affiliations
return self.nominations.first().other_affiliations
def visible(self, user=None):
"""Return True if the nominee is visible to the given user."""
if self.accepted and self.approved and not self.election.nominations_open:
return True
if user is None:
return False
return bool(user.is_staff or user == self.user)
class Nomination(models.Model):
"""A nomination submitted for a candidate in a board election."""
election = models.ForeignKey(Election, on_delete=models.CASCADE)
name = models.CharField(max_length=1024, blank=False, null=True) # noqa: DJ001
email = models.CharField(max_length=1024, blank=False, null=True) # noqa: DJ001
previous_board_service = models.CharField(max_length=1024, blank=False, null=True) # noqa: DJ001
employer = models.CharField(max_length=1024, blank=False, null=True) # noqa: DJ001
other_affiliations = models.CharField(max_length=2048, blank=True, null=True) # noqa: DJ001
nomination_statement = MarkupField(escape_html=True, markup_type="markdown", blank=False, null=True)
nominator = models.ForeignKey(User, related_name="nominations_made", on_delete=models.CASCADE)
nominee = models.ForeignKey(
Nominee,
related_name="nominations",
null=True,
on_delete=models.CASCADE,
blank=True,
)
accepted = models.BooleanField(null=False, default=False)
approved = models.BooleanField(null=False, default=False)
def __str__(self):
"""Return the nominee name and email."""
return f"{self.name} <{self.email}>"
def get_absolute_url(self):
"""Return the URL for the nomination detail page."""
return reverse(
"nominations:nomination_detail",
kwargs={"election": self.election.slug, "pk": self.pk},
)
def get_edit_url(self):
"""Return the URL for editing this nomination."""
return reverse(
"nominations:nomination_edit",
kwargs={"election": self.election.slug, "pk": self.pk},
)
def get_accept_url(self):
"""Return the URL for accepting this nomination."""
return reverse(
"nominations:nomination_accept",
kwargs={"election": self.election.slug, "pk": self.pk},
)
def editable(self, user=None):
"""Return True if the given user can edit this nomination."""
if self.nominee and user == self.nominee.user and self.election.nominations_open:
return True
return bool(user == self.nominator and not (self.accepted or self.approved) and self.election.nominations_open)
def visible(self, user=None):
"""Return True if the nomination is visible to the given user."""
if self.accepted and self.approved and not self.election.nominations_open_at:
return True
if user is None:
return False
if user.is_staff:
return True
if user == self.nominator:
return True
return bool(self.nominee and user == self.nominee.user)
@receiver(post_save, sender=Nomination)
def purge_nomination_pages(sender, instance, created, **kwargs):
"""Purge pages that contain the rendered markup."""
# Skip in fixtures
if kwargs.get("raw", False):
return
# Purge the nomination page itself
purge_url(instance.get_absolute_url())
if instance.nominee:
# Purge the nominee page
purge_url(instance.nominee.get_absolute_url())
if instance.election:
# Purge the election page
purge_url(reverse("nominations:nominees_list", kwargs={"election": instance.election.slug}))