Change collection of course results to be error proof
It's possible a course result does not exist (yet). Until now, that would cause an error when requesting the results, but now it just returns None.
- Author
- Maarten 'Vngngdn' Vangeneugden
- Date
- April 15, 2018, 7:16 p.m.
- Hash
- cb27d1718c59fdf3dd92bb0d423059778fdf6588
- Parent
- 56eb93dad8d6d31a2bf667cf2960a4cbc080c858
- Modified file
- administration/models.py
administration/models.py ¶
5 additions and 2 deletions.
View changes Hide changes
1 |
1 |
from django.core.exceptions import ValidationError |
2 |
2 |
from django.core.validators import MaxValueValidator |
3 |
3 |
from django.utils.translation import ugettext_lazy as _ |
4 |
4 |
from django.utils.text import slugify |
5 |
5 |
from django.contrib.auth.models import AbstractUser |
6 |
6 |
import datetime |
7 |
7 |
import os |
8 |
8 |
import uuid |
9 |
9 |
#from . import roster |
10 |
10 |
|
11 |
11 |
def validate_IBAN(value): |
12 |
12 |
""" Validates if the given value qualifies as a valid IBAN number. |
13 |
13 |
This validator checks if the structure is valid, and calculates the control |
14 |
14 |
number if the structure is correct. If the control number fails, or the |
15 |
15 |
structure is invalid, a ValidationError will be raised. In that case, |
16 |
16 |
the Error will specify whether the structure is incorrect, or the control |
17 |
17 |
number is not valid. |
18 |
18 |
""" |
19 |
19 |
# FIXME: This function is not complete. When there's time, implement |
20 |
20 |
# as specified at https://nl.wikipedia.org/wiki/International_Bank_Account_Number#Structuur |
21 |
21 |
if False: |
22 |
22 |
raise ValidationError( |
23 |
23 |
_('%(value)s is not a valid IBAN number.'), |
24 |
24 |
params={'value': value},) |
25 |
25 |
def validate_BIC(value): |
26 |
26 |
""" Same functionality as validate_IBAN, but for BIC-codes. """ |
27 |
27 |
# FIXME: This function is not complete. When there's time, implement |
28 |
28 |
# as specified at https://nl.wikipedia.org/wiki/Business_Identifier_Code |
29 |
29 |
pass |
30 |
30 |
|
31 |
31 |
class User(AbstractUser): |
32 |
32 |
""" Replacement for the standard Django User model. """ |
33 |
33 |
number = models.AutoField( |
34 |
34 |
primary_key=True, |
35 |
35 |
help_text=_("The number assigned to this user."), |
36 |
36 |
) |
37 |
37 |
created = models.DateField(auto_now_add=True) |
38 |
38 |
|
39 |
39 |
def __str__(self): |
40 |
40 |
user_data = UserData.objects.filter(user=self) |
41 |
41 |
if len(user_data) == 0: |
42 |
42 |
return self.username |
43 |
43 |
else: |
44 |
44 |
user_data = user_data[0] |
45 |
45 |
name = user_data.first_name +" "+ user_data.last_name |
46 |
46 |
titles = user_data.title.split() |
47 |
47 |
if len(titles) == 0: |
48 |
48 |
return name |
49 |
49 |
else: |
50 |
50 |
prefix_titles = "" |
51 |
51 |
suffix_titles = "" |
52 |
52 |
for title in titles: |
53 |
53 |
if title in ["lic.", "prof.", "dr.", "drs.", "bc.", "bacc.", "ing.", "ir." "cand.", "mr.", "dr.h.c.mult.", "dr.h.c.", "dr.mult.", "em. prof.", "prof. em.", "lec."]: |
54 |
54 |
prefix_titles += title + " " |
55 |
55 |
elif title in ["MSc", "BSc", "MA", "BA", "LLM", "LLB", "PhD", "MD"]: |
56 |
56 |
suffix_titles += title + " " |
57 |
57 |
return prefix_titles + name + " " + suffix_titles.strip() |
58 |
58 |
|
59 |
59 |
class UserData(models.Model): |
60 |
60 |
user = models.OneToOneField("User", on_delete=models.CASCADE, related_name="user_data") |
61 |
61 |
first_name = models.CharField(max_length=64, blank=False) |
62 |
62 |
last_name = models.CharField(max_length=64, blank=False) |
63 |
63 |
title = models.CharField( |
64 |
64 |
max_length=64, |
65 |
65 |
blank=True, |
66 |
66 |
help_text=_("The academic title of this user, if applicable."), |
67 |
67 |
) |
68 |
68 |
DOB = models.DateField( |
69 |
69 |
blank=False, |
70 |
70 |
#editable=False, # For testing purposes, decomment in deployment! |
71 |
71 |
help_text=_("The date of birth of this user."), |
72 |
72 |
) |
73 |
73 |
POB = models.CharField( |
74 |
74 |
max_length=64, |
75 |
75 |
blank=False, |
76 |
76 |
#editable=False, # For testing purposes, decomment in deployment! |
77 |
77 |
help_text=_("The place of birth of this user."), |
78 |
78 |
) |
79 |
79 |
nationality = models.CharField( |
80 |
80 |
max_length=64, |
81 |
81 |
blank=False, |
82 |
82 |
help_text=_("The current nationality of this user."), |
83 |
83 |
default="Belg", |
84 |
84 |
) |
85 |
85 |
# XXX: What if this starts with zeros? |
86 |
86 |
national_registry_number = models.BigIntegerField( |
87 |
87 |
blank=True, # Only possible if Belgian |
88 |
88 |
# TODO Validator! |
89 |
89 |
#editable=False, |
90 |
90 |
help_text=_("The assigned national registry number of this user."), |
91 |
91 |
) |
92 |
92 |
civil_status = models.CharField( |
93 |
93 |
max_length=32, |
94 |
94 |
choices = ( |
95 |
95 |
("Single", _("Single")), |
96 |
96 |
("Married", _("Married")), |
97 |
97 |
("Divorced", _("Divorced")), |
98 |
98 |
("Widowed", _("Widowed")), |
99 |
99 |
("Partnership", _("Partnership")), |
100 |
100 |
), |
101 |
101 |
blank=False, |
102 |
102 |
# There may be more; consult http://www.aantrekkingskracht.com/trefwoord/burgerlijke-staat |
103 |
103 |
# for more information. |
104 |
104 |
help_text=_("The civil/marital status of the user."), |
105 |
105 |
) |
106 |
106 |
|
107 |
107 |
is_staff = models.BooleanField( |
108 |
108 |
default=False, |
109 |
109 |
help_text=_("Determines if this user is part of the university's staff."), |
110 |
110 |
) |
111 |
111 |
is_student = models.BooleanField( |
112 |
112 |
default=True, |
113 |
113 |
help_text=_("Indicates if this user is a student at the university."), |
114 |
114 |
) |
115 |
115 |
|
116 |
116 |
def slug_name(self): |
117 |
117 |
"""Returns a slug name for this user which can be used to reference in |
118 |
118 |
URLs.""" |
119 |
119 |
same_names = UserData.objects.filter(first_name=self.first_name).filter(last_name=self.last_name) |
120 |
120 |
if len(same_names) == 1 and same_names[0] == self: |
121 |
121 |
return slugify(self.first_name +"-"+ self.last_name, allow_unicode=True) |
122 |
122 |
else: |
123 |
123 |
number = self.user.number |
124 |
124 |
return slugify(self.first_name +"-"+ self.last_name +"-"+ str(number), allow_unicode=True) |
125 |
125 |
|
126 |
126 |
# Home address |
127 |
127 |
home_street = models.CharField(max_length=64, blank=False) |
128 |
128 |
home_number = models.PositiveSmallIntegerField(blank=False) |
129 |
129 |
home_bus = models.CharField(max_length=10, null=True, blank=True) |
130 |
130 |
home_postal_code = models.PositiveIntegerField(blank=False) |
131 |
131 |
home_city = models.CharField(max_length=64, blank=False) |
132 |
132 |
home_country = models.CharField(max_length=64, blank=False, default="België") |
133 |
133 |
home_telephone = models.CharField( |
134 |
134 |
max_length=64, |
135 |
135 |
help_text=_("The telephone number for the house address. Prefix 0 can be presented with the national call code in the system (\"32\" for Belgium)."), |
136 |
136 |
) |
137 |
137 |
# Study address |
138 |
138 |
study_street = models.CharField(max_length=64, blank=True, null=True) |
139 |
139 |
study_number = models.PositiveSmallIntegerField(blank=True, null=True) |
140 |
140 |
study_bus = models.CharField(max_length=10, null=True, blank=True) |
141 |
141 |
study_postal_code = models.PositiveSmallIntegerField(blank=True, null=True) |
142 |
142 |
study_country = models.CharField(max_length=64, blank=True, null=True) |
143 |
143 |
study_telephone = models.CharField( |
144 |
144 |
blank=True, null=True, |
145 |
145 |
max_length=64, |
146 |
146 |
help_text=_("The telephone number for the study address. Prefix 0 can be presented with the national call code in the system."), |
147 |
147 |
) |
148 |
148 |
study_cellphone = models.CharField( |
149 |
149 |
max_length=64, null=True, blank=True, |
150 |
150 |
help_text=_("The cellphone number of the person. Prefix 0 can be presented with then national call code in the system."), |
151 |
151 |
) |
152 |
152 |
# Titularis address |
153 |
153 |
# XXX: These fields are only required if this differs from the user itself. |
154 |
154 |
titularis_street = models.CharField(max_length=64, null=True, blank=True) |
155 |
155 |
titularis_number = models.PositiveSmallIntegerField(null=True, blank=True) |
156 |
156 |
titularis_bus = models.CharField(max_length=10, null=True, blank=True) |
157 |
157 |
titularis_postal_code = models.PositiveSmallIntegerField(null=True, blank=True) |
158 |
158 |
titularis_country = models.CharField(max_length=64, null=True, blank=True) |
159 |
159 |
titularis_telephone = models.CharField( |
160 |
160 |
max_length=64, |
161 |
161 |
help_text=_("The telephone number of the titularis. Prefix 0 can be presented with the national call code in the system."), |
162 |
162 |
null=True, |
163 |
163 |
blank=True, |
164 |
164 |
) |
165 |
165 |
|
166 |
166 |
# Financial details |
167 |
167 |
bank_account_number = models.CharField( |
168 |
168 |
max_length=34, # Max length of all IBAN account numbers |
169 |
169 |
validators=[validate_IBAN], |
170 |
170 |
help_text=_("The IBAN of this user. No spaces!"), |
171 |
171 |
) |
172 |
172 |
BIC = models.CharField( |
173 |
173 |
max_length=11, |
174 |
174 |
validators=[validate_BIC], |
175 |
175 |
help_text=_("The BIC of this user's bank."), |
176 |
176 |
) |
177 |
177 |
|
178 |
178 |
""" NOTE: What about all the other features that should be in the administration? |
179 |
179 |
While there are a lot of things to cover, as of now, I have no way to know which |
180 |
180 |
ones are still valid, which are deprecated, and so on... |
181 |
181 |
Additionally, every feature may have a different set of requirements, data, |
182 |
182 |
and it's very likely making an abstract class won't do any good. Thus I have |
183 |
183 |
decided to postpone making additional tables and forms for these features until |
184 |
184 |
I have clearance about certain aspects. """ |
185 |
185 |
|
186 |
186 |
class Curriculum(models.Model): |
187 |
187 |
""" The curriculum of a particular student. |
188 |
188 |
Every academic year, a student has to hand in a curriculum (s)he wishes to |
189 |
189 |
follow. This is then reviewed by a committee. A curriculum exists of all the |
190 |
190 |
courses one wants to partake in in a certain year. """ |
191 |
191 |
student = models.ForeignKey( |
192 |
192 |
"User", |
193 |
193 |
on_delete=models.CASCADE, |
194 |
194 |
limit_choices_to={'groups': 1}, # 1 = Students group ID |
195 |
195 |
null=False, |
196 |
196 |
#editable=False, |
197 |
197 |
#unique_for_year="year", # Only 1 curriculum per year # FIXME to work with integer! |
198 |
198 |
) |
199 |
199 |
year = models.PositiveIntegerField( |
200 |
200 |
null=False, |
201 |
201 |
default=datetime.date.today().year, |
202 |
202 |
help_text=_("The academic year for which this curriculum is. " |
203 |
203 |
"If this field is equal to 2008, then that means " |
204 |
204 |
"this curriculum is for the academic year " |
205 |
205 |
"2008-2009."), |
206 |
206 |
) |
207 |
207 |
# TODO: Validate changes: A curriculum cannot undergo another change if the |
208 |
208 |
# academic year it was made in is history. |
209 |
209 |
last_modified = models.DateTimeField( |
210 |
210 |
auto_now=True, |
211 |
211 |
help_text=_("The last timestamp that this was updated."), |
212 |
212 |
) |
213 |
213 |
course_programmes = models.ManyToManyField( |
214 |
214 |
"courses.CourseProgramme", |
215 |
215 |
blank=False, # An empty curriculum makes no sense |
216 |
216 |
help_text=_("All the course programmes included in this curriculum."), |
217 |
217 |
) |
218 |
218 |
approved = models.NullBooleanField( |
219 |
219 |
default=None, |
220 |
220 |
help_text=_("Indicates if this curriculum has been approved. If true, " |
221 |
221 |
"that means the responsible committee has reviewed and " |
222 |
222 |
"approved the student for this curriculum. False otherwise. " |
223 |
223 |
"If review is still pending, the value is NULL. Modifying " |
224 |
224 |
"the curriculum implies this setting is set to NULL again."), |
225 |
225 |
) |
226 |
226 |
note = models.TextField( |
227 |
227 |
blank=True, |
228 |
228 |
help_text=_("Additional notes regarding this curriculum. This has " |
229 |
229 |
"multiple uses. For the student, it is used to clarify " |
230 |
230 |
"any questions, or to motivate why (s)he wants to take a " |
231 |
231 |
"course for which the requirements were not met. " |
232 |
232 |
"The reviewing committee can use this field to argument " |
233 |
233 |
"their decision, especially for when the curriculum is " |
234 |
234 |
"denied."), |
235 |
235 |
) |
236 |
236 |
|
237 |
237 |
def course_programmes_results(self): |
238 |
238 |
""" Returns a dictionary, where the keys are the course_programmes |
239 |
239 |
in this curriculum, and the values are the course_results associated |
240 |
240 |
with them.""" |
241 |
241 |
join_dict = dict() |
242 |
242 |
for course_program in self.course_programmes.all(): |
243 |
243 |
result = CourseResult.objects.filter( |
244 |
244 |
student=self.student).filter( |
245 |
245 |
course_programme=course_program).filter( |
246 |
246 |
year=self.year)[0] |
247 |
- | join_dict[course_program] = result |
248 |
- | return join_dict |
+ |
247 |
if len(result) == 0: |
+ |
248 |
join_dict[course_program] = None |
+ |
249 |
else: |
+ |
250 |
join_dict[course_program] = result[0] |
+ |
251 |
return join_dict |
249 |
252 |
|
250 |
253 |
def courses(self): |
251 |
254 |
""" Returns a set of all the courses that are in this curriculum. |
252 |
255 |
This is not the same as CourseProgrammes, as these can differ depending |
253 |
256 |
on which study one follows. """ |
254 |
257 |
course_set = set() |
255 |
258 |
for course_programme in self.course_programmes.all(): |
256 |
259 |
course_set.add(course_programme.course) |
257 |
260 |
return course_set |
258 |
261 |
|
259 |
262 |
def curriculum_type(self): |
260 |
263 |
""" Returns the type of this curriculum. At the moment, this is |
261 |
264 |
either a standard programme, or an individualized programme. """ |
262 |
265 |
# Currently: A standard programme means: All courses are from the |
263 |
266 |
# same study, ánd from the same year. Additionally, all courses |
264 |
267 |
# from that year must've been taken. |
265 |
268 |
# FIXME: Need a way to determine what is the standard programme. |
266 |
269 |
# If not possible, make this a charfield with options or something |
267 |
270 |
pass |
268 |
271 |
|
269 |
272 |
def __str__(self): |
270 |
273 |
return str(self.student) +" | "+ str(self.year) +"-"+ str(self.year+1) |
271 |
274 |
|
272 |
275 |
|
273 |
276 |
class CourseResult(models.Model): |
274 |
277 |
""" A student has to obtain a certain course result. These are stored here, |
275 |
278 |
together with all the appropriate information. """ |
276 |
279 |
# TODO: Validate that a course programme for a student can only be made once per year for each course, if possible. |
277 |
280 |
CRED = _("Credit acquired") |
278 |
281 |
FAIL = _("Credit not acquired") |
279 |
282 |
TLRD = _("Tolerated") |
280 |
283 |
ITLR = _("Tolerance used") |
281 |
284 |
BDRG = _("Fraud committed") |
282 |
285 |
VRST = _("Exemption") |
283 |
286 |
STOP = _("Course cancelled") |
284 |
287 |
# Possible to add more in the future |
285 |
288 |
|
286 |
289 |
student = models.ForeignKey( |
287 |
290 |
"User", |
288 |
291 |
on_delete=models.CASCADE, |
289 |
292 |
#limit_choices_to={'is_student': True}, |
290 |
293 |
null=False, |
291 |
294 |
db_index=True, |
292 |
295 |
) |
293 |
296 |
course_programme = models.ForeignKey( |
294 |
297 |
"courses.CourseProgramme", |
295 |
298 |
on_delete=models.PROTECT, |
296 |
299 |
null=False, |
297 |
300 |
) |
298 |
301 |
year = models.PositiveIntegerField( |
299 |
302 |
null=False, |
300 |
303 |
default=datetime.date.today().year, |
301 |
304 |
help_text=_("The academic year this course took place in. If 2018 is entered, " |
302 |
305 |
"then that means academic year '2018-2019'."), |
303 |
306 |
) |
304 |
307 |
released = models.DateField( |
305 |
308 |
auto_now=True, |
306 |
309 |
help_text=_("The date that this result was last updated."), |
307 |
310 |
) |
308 |
311 |
first_score = models.PositiveSmallIntegerField( |
309 |
312 |
null=True, # It's possible a score does not exist. |
310 |
313 |
blank=True, |
311 |
314 |
validators=[MaxValueValidator( |
312 |
315 |
20, |
313 |
316 |
_("The score mustn't be higher than 20."), |
314 |
317 |
)], |
315 |
318 |
) |
316 |
319 |
second_score = models.PositiveSmallIntegerField( |
317 |
320 |
null=True, |
318 |
321 |
blank=True, |
319 |
322 |
validators=[MaxValueValidator( |
320 |
323 |
20, |
321 |
324 |
_("The score mustn't be higher than 20."), |
322 |
325 |
)], |
323 |
326 |
) |
324 |
327 |
result = models.CharField( |
325 |
328 |
max_length=10, |
326 |
329 |
choices = ( |
327 |
330 |
("CRED", CRED), |
328 |
331 |
("FAIL", FAIL), |
329 |
332 |
("TLRD", TLRD), |
330 |
333 |
("ITLR", ITLR), |
331 |
334 |
("BDRG", BDRG), |
332 |
335 |
("VRST", VRST), |
333 |
336 |
("STOP", STOP), |
334 |
337 |
), |
335 |
338 |
blank=False, |
336 |
339 |
help_text=_("The final result this record constitutes."), |
337 |
340 |
) |
338 |
341 |
|
339 |
342 |
def __str__(self): |
340 |
343 |
stdnum = str(self.student.number) |
341 |
344 |
result = self.result |
342 |
345 |
if result == "CRED": |
343 |
346 |
if self.first_score < 10: |
344 |
347 |
result = "C" + str(self.first_score) + "1" |
345 |
348 |
else: |
346 |
349 |
result = "C" + str(self.second_score) + "2" |
347 |
350 |
course = str(self.course_programme.course) |
348 |
351 |
return stdnum +" ("+ result +") | "+ course |
349 |
352 |
|
350 |
353 |
class PreRegistration(models.Model): |
351 |
354 |
""" At the beginning of the new academic year, students can register |
352 |
355 |
themselves at the university. Online, they can do a preregistration already. |
353 |
356 |
These records are stored here and can later be retrieved for the actual |
354 |
357 |
registration process. |
355 |
358 |
Note: The current system in use at Hasselt University provides a password system. |
356 |
359 |
That will be eliminated here. Just make sure that the entered details are correct. |
357 |
360 |
Should there be an error, and the same email address is used to update something, |
358 |
361 |
a mail will be sent to that address to verify this was a genuine update.""" |
359 |
362 |
created = models.DateField(auto_now_add=True) |
360 |
363 |
first_name = models.CharField(max_length=64, blank=False, help_text=_("Your first name.")) |
361 |
364 |
last_name = models.CharField(max_length=64, blank=False, help_text=_("Your last name.")) |
362 |
365 |
additional_names = models.CharField(max_length=64, blank=True, help_text=_("Any additional names.")) |
363 |
366 |
title = models.CharField( |
364 |
367 |
max_length=64, |
365 |
368 |
blank=True, |
366 |
369 |
help_text=_("Any additional titles, prefixes, ..."), |
367 |
370 |
) |
368 |
371 |
DOB = models.DateField( |
369 |
372 |
blank=False, |
370 |
373 |
#editable=False, |
371 |
374 |
help_text=_("Your date of birth."), |
372 |
375 |
) |
373 |
376 |
POB = models.CharField( |
374 |
377 |
max_length=64, |
375 |
378 |
blank=False, |
376 |
379 |
#editable=False, |
377 |
380 |
help_text=_("The place you were born."), |
378 |
381 |
) |
379 |
382 |
nationality = models.CharField( |
380 |
383 |
max_length=64, |
381 |
384 |
blank=False, |
382 |
385 |
help_text=_("Your current nationality."), |
383 |
386 |
) |
384 |
387 |
national_registry_number = models.BigIntegerField( |
385 |
388 |
null=True, |
386 |
389 |
help_text=_("If you have one, your national registry number."), |
387 |
390 |
) |
388 |
391 |
civil_status = models.CharField( |
389 |
392 |
max_length=32, |
390 |
393 |
choices = ( |
391 |
394 |
("Single", _("Single")), |
392 |
395 |
("Married", _("Married")), |
393 |
396 |
("Divorced", _("Divorced")), |
394 |
397 |
("Widowed", _("Widowed")), |
395 |
398 |
("Partnership", _("Partnership")), |
396 |
399 |
), |
397 |
400 |
blank=False, |
398 |
401 |
# There may be more; consult http://www.aantrekkingskracht.com/trefwoord/burgerlijke-staat |
399 |
402 |
# for more information. |
400 |
403 |
help_text=_("Your civil/marital status."), |
401 |
404 |
) |
402 |
405 |
email = models.EmailField( |
403 |
406 |
blank=False, |
404 |
407 |
unique=True, |
405 |
408 |
help_text=_("The e-mail address we will use to communicate until your actual registration."), |
406 |
409 |
) |
407 |
410 |
study = models.ForeignKey( |
408 |
411 |
"courses.Study", |
409 |
412 |
on_delete=models.PROTECT, |
410 |
413 |
null=False, |
411 |
414 |
help_text=_("The study you wish to follow. Be sure to provide all legal" |
412 |
415 |
"documents that are required for this study with this " |
413 |
416 |
"application, or bring them with you to the final registration."), |
414 |
417 |
) |
415 |
418 |
study_type = models.CharField( |
416 |
419 |
max_length=32, |
417 |
420 |
choices = ( |
418 |
421 |
("Diplom contract", _("Diplom contract")), |
419 |
422 |
("Exam contract", _("Exam contract")), |
420 |
423 |
("Credit contract", _("Credit contract")), |
421 |
424 |
), |
422 |
425 |
blank=False, |
423 |
426 |
help_text=_("The type of study contract you wish to follow."), |
424 |
427 |
) |
425 |
428 |
document = models.FileField( |
426 |
429 |
upload_to="pre-enrollment/%Y", |
427 |
430 |
help_text=_("Any legal documents regarding your enrollment."), |
428 |
431 |
) |
429 |
432 |
# XXX: If the database in production is PostgreSQL, comment document, and |
430 |
433 |
# uncomment the next column. |
431 |
434 |
"""documents = models.ArrayField( |
432 |
435 |
models.FileField(upload_to="pre-enrollment/%Y"), |
433 |
436 |
help_text=_("Any legal documents regarding your enrollment."), |
434 |
437 |
)""" |
435 |
438 |
|
436 |
439 |
def __str__(self): |
437 |
440 |
name = self.last_name +" "+ self.first_name |
438 |
441 |
dob = self.DOB.strftime("%d/%m/%Y") |
439 |
442 |
return name +" | "+ dob |
440 |
443 |
|
441 |
444 |
|
442 |
445 |
# Planning and organization related tables |
443 |
446 |
class Room(models.Model): |
444 |
447 |
""" Represents a room in the university. |
445 |
448 |
Rooms can have a number of properties, which are stored in the database. |
446 |
449 |
""" |
447 |
450 |
# Types of rooms |
448 |
451 |
LABORATORY = _("Laboratory") # Chemistry/Physics equipped rooms |
449 |
452 |
CLASS_ROOM = _("Class room") # Simple class rooms |
450 |
453 |
AUDITORIUM = _("Auditorium") # Large rooms with ample seating and equipment for lectures |
451 |
454 |
PC_ROOM = _("PC room" ) # Rooms equipped for executing PC related tasks |
452 |
455 |
PUBLIC_ROOM= _("Public room") # Restaurants, restrooms, ... general public spaces |
453 |
456 |
OFFICE = _("Office" ) # Private offices for staff |
454 |
457 |
PRIVATE_ROOM = _("Private room") # Rooms accessible for a limited public; cleaning cupboards, kitchens, ... |
455 |
458 |
WORKSHOP = _("Workshop" ) # Rooms with hardware equipment to build and work on materials |
456 |
459 |
OTHER = _("Other" ) # Rooms that do not fit in any other category |
457 |
460 |
|
458 |
461 |
|
459 |
462 |
name = models.CharField( |
460 |
463 |
max_length=20, |
461 |
464 |
primary_key=True, |
462 |
465 |
blank=False, |
463 |
466 |
help_text=_("The name of this room. If more appropriate, this can be the colloquial name."), |
464 |
467 |
) |
465 |
468 |
seats = models.PositiveSmallIntegerField( |
466 |
469 |
help_text=_("The amount of available seats in this room."), |
467 |
470 |
) |
468 |
471 |
wheelchair_accessible = models.BooleanField(default=True) |
469 |
472 |
exams_equipped = models.BooleanField( |
470 |
473 |
default=True, |
471 |
474 |
help_text=_("Indicates if exams can reasonably be held in this room."), |
472 |
475 |
) |
473 |
476 |
loose_tables = models.BooleanField( |
474 |
477 |
default=True, |
475 |
478 |
help_text=_("If true, the tables in this room can be moved freely. " |
476 |
479 |
"If false, they're bolted down in their positions."), |
477 |
480 |
) |
478 |
481 |
electrical_plugs = models.PositiveSmallIntegerField( |
479 |
482 |
help_text=_("The amount of electrical plugs that are available to the " |
480 |
483 |
"people for free use. Electrical plugs that are more or " |
481 |
484 |
"less constantly occupied by permanent equipment (such as " |
482 |
485 |
"computers, beamers, ...) are excluded from counting."), |
483 |
486 |
) |
484 |
487 |
exterior_window = models.BooleanField( |
485 |
488 |
default=True, |
486 |
489 |
help_text=_("Indicates if this room has a window to the outside."), |
487 |
490 |
) |
488 |
491 |
software_available = models.TextField( |
489 |
492 |
blank=True, |
490 |
493 |
help_text=_("Some software used at the university is proprietary, and " |
491 |
494 |
"thus not available at every system. If certain " |
492 |
495 |
"software is installed on the computers in this room that " |
493 |
496 |
"cannot be found on other computers, list them here."), |
494 |
497 |
) |
495 |
498 |
computers_available = models.PositiveSmallIntegerField( |
496 |
499 |
default=0, |
497 |
500 |
help_text=_("Indicates how many computers are available in this room."), |
498 |
501 |
) |
499 |
502 |
projector_available = models.BooleanField( |
500 |
503 |
default=False, |
501 |
504 |
help_text=_("Indicates if a projector is available at this room."), |
502 |
505 |
) |
503 |
506 |
blackboards_available = models.PositiveSmallIntegerField( |
504 |
507 |
help_text=_("The amount of blackboards available in this room."), |
505 |
508 |
) |
506 |
509 |
whiteboards_available = models.PositiveSmallIntegerField( |
507 |
510 |
help_text=_("The amount of whiteboards available in this room."), |
508 |
511 |
) |
509 |
512 |
category = models.CharField( |
510 |
513 |
max_length=16, |
511 |
514 |
blank=False, |
512 |
515 |
choices = ( |
513 |
516 |
("LABORATORY", LABORATORY), |
514 |
517 |
("CLASS_ROOM", CLASS_ROOM), |
515 |
518 |
("AUDITORIUM", AUDITORIUM), |
516 |
519 |
("PC_ROOM", PC_ROOM), |
517 |
520 |
("PUBLIC_ROOM", PUBLIC_ROOM), |
518 |
521 |
("OFFICE", OFFICE), |
519 |
522 |
("PRIVATE_ROOM", PRIVATE_ROOM), |
520 |
523 |
("WORKSHOP", WORKSHOP), |
521 |
524 |
("OTHER", OTHER), |
522 |
525 |
), |
523 |
526 |
help_text=_("The category that best suits the character of this room."), |
524 |
527 |
) |
525 |
528 |
reservable = models.BooleanField( |
526 |
529 |
default=True, |
527 |
530 |
help_text=_("Indicates if this room can be reserved for something."), |
528 |
531 |
) |
529 |
532 |
note = models.TextField( |
530 |
533 |
blank=True, |
531 |
534 |
help_text=_("If some additional info is required for this room, like a " |
532 |
535 |
"characteristic property (e.g. 'Usually occupied by 2BACH " |
533 |
536 |
"informatics'), state it here."), |
534 |
537 |
) |
535 |
538 |
# TODO: Add a campus/building field or not? |
536 |
539 |
|
537 |
540 |
def next_reservation(self, time): |
538 |
541 |
""" Returns the next reservation starting from the given time, or, if |
539 |
542 |
the next reservation starts on the given time, that reservation. |
540 |
543 |
Returns None if there is no reservation from this moment on.""" |
541 |
544 |
reservations = RoomReservation.objects.filter(room=self).filter(begin_time__gte=time).order_by('begin_time') |
542 |
545 |
if len(reservations) == 0: |
543 |
546 |
return None |
544 |
547 |
else: |
545 |
548 |
return reservations[0] |
546 |
549 |
def next_event(self, time): |
547 |
550 |
""" Returns the next event starting from the given time, or, if |
548 |
551 |
the next event starts on the given time, that event. |
549 |
552 |
Returns None if there is no event from this moment on.""" |
550 |
553 |
events = CourseEvent.objects.filter(room=self).filter(begin_time__gte=time).order_by('begin_time') |
551 |
554 |
if len(events) == 0: |
552 |
555 |
return None |
553 |
556 |
else: |
554 |
557 |
return events[0] |
555 |
558 |
|
556 |
559 |
|
557 |
560 |
def reservation_possible(self, begin, end, seats=None): |
558 |
561 |
# TODO: Include events in the check for possibilities! |
559 |
562 |
""" Returns a boolean indicating if reservating during the given time |
560 |
563 |
is possible. If the begin overlaps with a reservation's end or vice versa, |
561 |
564 |
this is regarded as possible. |
562 |
565 |
Takes seats as optional argument. If not specified, it is assumed the entire |
563 |
566 |
room has to be reserved. """ |
564 |
567 |
if self.reservable is False: |
565 |
568 |
return False |
566 |
569 |
if seats is not None and seats < 0: raise ValueError(_("seats ∈ ℕ")) |
567 |
570 |
|
568 |
571 |
reservations = RoomReservation.objects.filter(room=self) |
569 |
572 |
for reservation in reservations: |
570 |
573 |
if reservation.end_time <= begin or reservation.begin_time >= end: |
571 |
574 |
continue # Can be trivially skipped, no overlap here |
572 |
575 |
elif seats is None or reservation.seats is None: |
573 |
576 |
return False # The whole room cannot be reserved -> False |
574 |
577 |
elif seats + reservation.seats > self.seats: |
575 |
578 |
return False # Total amount of seats exceeds the available amount -> False |
576 |
579 |
return True # No overlappings found -> True |
577 |
580 |
|
578 |
581 |
def __str__(self): |
579 |
582 |
return self.name |
580 |
583 |
|
581 |
584 |
|
582 |
585 |
# Validators that will be used for RoomReservations and Events |
583 |
586 |
def validate_event_time(time): |
584 |
587 |
"""Checks if the time is a quarter of an hour (0, 15, 30, or 45).""" |
585 |
588 |
if time.minute not in [0, 15, 30, 45] or time.second != 0: |
586 |
589 |
raise ValidationError( |
587 |
590 |
_('%(time)s is not in the quarter of an hour.'), |
588 |
591 |
params={'time': time.strftime("%H:%M")}) |
589 |
592 |
def validate_university_hours(value): |
590 |
593 |
"""Checks if the datetime value given takes place during the opening hours |
591 |
594 |
of the university (08:00 - 20:00).""" |
592 |
595 |
if value.hour < 8 or (value.hour == 22 and value.minute != 0) or value.hour >= 23: |
593 |
596 |
raise ValidationError( |
594 |
597 |
_("All events and reservations must begin and end between 08:00 " |
595 |
598 |
"and 22:00.")) |
596 |
599 |
def overlaps(begin_a, end_a, begin_b, end_b): |
597 |
600 |
"""Checks if timespan a and b overlap with each other. If one of them ends at |
598 |
601 |
the same time the other one begins, it does not count as an overlap. |
599 |
602 |
This function assumes the end takes place strictly /after/ the begin.""" |
600 |
603 |
if end_a <= begin_b or end_b <= begin_a: |
601 |
604 |
return False |
602 |
605 |
if ( |
603 |
606 |
begin_a < begin_b <= end_a or |
604 |
607 |
begin_b < begin_a <= end_b or |
605 |
608 |
begin_a <= end_b < end_a or |
606 |
609 |
begin_b <= end_a < end_b): |
607 |
610 |
return True |
608 |
611 |
else: |
609 |
612 |
return False |
610 |
613 |
|
611 |
614 |
|
612 |
615 |
def general_reservation_validator(self): |
613 |
616 |
# Check for overlapping reservations |
614 |
617 |
# TODO: Try to make it possible to link to the reservator, |
615 |
618 |
# to display the reason, to show the available times that a |
616 |
619 |
# reservation can be made for that room, and so on... Make it |
617 |
620 |
# a bit more interactive. |
618 |
621 |
for reservation in RoomReservation.objects.filter(room=self.room): |
619 |
622 |
if overlaps(self.begin_time, |
620 |
623 |
self.end_time, |
621 |
624 |
reservation.begin_time, |
622 |
625 |
reservation.end_time): |
623 |
626 |
if isinstance(self, RoomReservation): |
624 |
627 |
if self.room.reservation_possible(self.begin_time, self.end_time, self.seats): |
625 |
628 |
continue # Both reservations can take place in the same room |
626 |
629 |
raise ValidationError( |
627 |
630 |
_("It is not possible to plan this event/reservation in " |
628 |
631 |
"%(room)s from %(self_begin)s to %(end_begin)s on %(day)s. " |
629 |
632 |
"%(reservator)s has already " |
630 |
633 |
"reserved it from %(res_begin)s to %(res_end)s."), |
631 |
634 |
params={'room': str(self.room), |
632 |
635 |
'self_begin': self.begin_time.strftime("%H:%M"), |
633 |
636 |
'self_end': self.end_time.strftime("%H:%M"), |
634 |
637 |
'day': self.begin_time.strftime("%A (%d/%m)"), |
635 |
638 |
'reservator': str(reservation.reservator), |
636 |
639 |
'res_begin': reservation.begin_time.strftime("%H:%M"), |
637 |
640 |
'res_end': reservation.end_time.strftime("%H:%M"), |
638 |
641 |
}) |
639 |
642 |
for course_event in CourseEvent.objects.filter(room=self.room): |
640 |
643 |
if overlaps(self.begin_time, |
641 |
644 |
self.end_time, |
642 |
645 |
course_event.begin_time, |
643 |
646 |
course_event.end_time): |
644 |
647 |
raise ValidationError( |
645 |
648 |
_("%(docent)s has organized a %(subject)s in %(room)s from " |
646 |
649 |
"%(res_begin)s to %(res_end)s on %(day)s, so you cannot " |
647 |
650 |
"place a reservation there from %(self_begin)s to " |
648 |
651 |
"%(self_end)s."), |
649 |
652 |
params={'room': str(self.room), |
650 |
653 |
'self_begin': self.begin_time.strftime("%H:%M"), |
651 |
654 |
'self_end': self.end_time.strftime("%H:%M"), |
652 |
655 |
'day': self.begin_time.strftime("%A (%d/%m)"), |
653 |
656 |
'docent': str(course_event.docent), |
654 |
657 |
'subject': course_event.subject, |
655 |
658 |
'res_begin': course_event.begin_time.strftime("%H:%M"), |
656 |
659 |
'res_end': course_event.end_time.strftime("%H:%M"),}) |
657 |
660 |
|
658 |
661 |
# Checking for correct timings: |
659 |
662 |
if self.begin_time >= self.end_time: |
660 |
663 |
raise ValidationError( |
661 |
664 |
_("The begin time (%(begin)) must take place <em>before</em> " |
662 |
665 |
"the end time (%(end))."), |
663 |
666 |
params={'begin': self.begin_time.strftime("%H:%M"), |
664 |
667 |
'end': self.end_time.strftime("%H:%M"),}) |
665 |
668 |
"""if not roster.same_day(self.begin_time, self.end_time): |
666 |
669 |
raise ValidationError( |
667 |
670 |
_("The event/reservation must begin and end on the same day."))""" |
668 |
671 |
|
669 |
672 |
|
670 |
673 |
class RoomReservation(models.Model): |
671 |
674 |
""" Rooms are to be reserved from time to time. They can be reserved |
672 |
675 |
by externals, for something else, and whatnot. That is stored in this table. |
673 |
676 |
""" |
674 |
677 |
room = models.ForeignKey( |
675 |
678 |
"Room", |
676 |
679 |
on_delete=models.CASCADE, |
677 |
680 |
null=False, |
678 |
681 |
#editable=False, |
679 |
682 |
db_index=True, |
680 |
683 |
limit_choices_to={"reservable": True}, |
681 |
684 |
help_text=_("The room that is being reserved at this point."), |
682 |
685 |
) |
683 |
686 |
reservator = models.ForeignKey( |
684 |
687 |
"User", |
685 |
688 |
on_delete=models.CASCADE, |
686 |
689 |
null=False, |
687 |
690 |
#editable=False, |
688 |
691 |
help_text=_("The person that made the reservation (and thus responsible)."), |
689 |
692 |
) |
690 |
693 |
timestamp = models.DateTimeField(auto_now_add=True) |
691 |
694 |
begin_time = models.DateTimeField( |
692 |
695 |
null=False, |
693 |
696 |
help_text=_("The time that this reservation begin."), |
694 |
697 |
validators=[validate_event_time,validate_university_hours], |
695 |
698 |
) |
696 |
699 |
end_time = models.DateTimeField( |
697 |
700 |
null=False, |
698 |
701 |
help_text=_("The time that this reservation ends."), |
699 |
702 |
validators=[validate_event_time,validate_university_hours], |
700 |
703 |
) |
701 |
704 |
seats = models.PositiveSmallIntegerField( |
702 |
705 |
null=True, |
703 |
706 |
blank=True, |
704 |
707 |
help_text=_("Indicates how many seats are required. If this is left empty, " |
705 |
708 |
"it is assumed the entire room has to be reserved."), |
706 |
709 |
) |
707 |
710 |
reason = models.CharField( |
708 |
711 |
max_length=64, |
709 |
712 |
blank=True, |
710 |
713 |
help_text=_("The reason for this reservation, if useful."), |
711 |
714 |
) |
712 |
715 |
note = models.TextField( |
713 |
716 |
blank=True, |
714 |
717 |
help_text=_("If some additional info is required for this reservation, " |
715 |
718 |
"state it here."), |
716 |
719 |
) |
717 |
720 |
|
718 |
721 |
def __str__(self): |
719 |
722 |
start = self.start_time.strftime("%H:%M") |
720 |
723 |
end = self.end_time.strftime("%H:%M") |
721 |
724 |
return str(self.room) +" | "+ start +"-"+ end |
722 |
725 |
|
723 |
726 |
def clean(self): |
724 |
727 |
general_reservation_validator(self) |
725 |
728 |
|
726 |
729 |
class Degree(models.Model): |
727 |
730 |
""" Contains all degrees that were achieved at this university. |
728 |
731 |
There are no foreign keys in this field. This allows system |
729 |
732 |
administrators to safely remove accounts from alumni, without |
730 |
733 |
the risk of breaking referential integrity or accidentally removing |
731 |
734 |
degrees. |
732 |
735 |
While keeping some fields editable that look like they shouldn't be |
733 |
736 |
(e.g. first_name), this makes it possible for alumni to have a name change |
734 |
737 |
later in their life, and still being able to get a copy of their degree. """ |
735 |
738 |
""" Reason for an ID field for every degree: |
736 |
739 |
This system allows for employers to verify that a certain applicant has indeed, |
737 |
740 |
achieved the degrees (s)he proclaims to have. Because of privacy concerns, |
738 |
741 |
a university cannot disclose information about alumni. |
739 |
742 |
That's where the degree ID comes in. This ID can be printed on all future |
740 |
743 |
degrees. The employer can then visit the university's website, and simply |
741 |
744 |
enter the ID. The website will then simply print what study is attached to |
742 |
745 |
this degree, but not disclose names or anything identifiable. This strikes |
743 |
746 |
thé perfect balance between (easy and digital) degree verification for employers, and maintaining |
744 |
747 |
alumni privacy to the highest extent possible. """ |
745 |
748 |
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) |
746 |
749 |
first_name = models.CharField( |
747 |
750 |
max_length=64, |
748 |
751 |
blank=False, |
749 |
752 |
) |
750 |
753 |
last_name = models.CharField( |
751 |
754 |
max_length=64, |
752 |
755 |
blank=False, |
753 |
756 |
) |
754 |
757 |
additional_names = models.CharField( |
755 |
758 |
max_length=64, |
756 |
759 |
blank=True, |
757 |
760 |
) |
758 |
761 |
DOB = models.DateField(null=False)#editable=False, null=False) # This can't be changed, of course |
759 |
762 |
POB = models.CharField( |
760 |
763 |
max_length=64, |
761 |
764 |
blank=False, |
762 |
765 |
#editable=False, |
763 |
766 |
) |
764 |
767 |
# The study also has to be a charfield, because if a study is removed, |
765 |
768 |
# The information will be lost. |
766 |
769 |
study = models.CharField( |
767 |
770 |
max_length=64, |
768 |
771 |
blank=False, |
769 |
772 |
#editable=False, |
770 |
773 |
) |
771 |
774 |
achieved = models.DateField(null=False)#editable=False, null=False) |
772 |
775 |
user = models.ForeignKey( |
773 |
776 |
"User", |
774 |
777 |
on_delete=models.SET_NULL, |
775 |
778 |
null=True, |
776 |
779 |
help_text=_("The person that achieved this degree, if (s)he still has " |
777 |
780 |
"an account at this university. If the account is deleted " |
778 |
781 |
"at a later date, this field will be set to NULL, but the " |
779 |
782 |
"other fields will be retained."), |
780 |
783 |
) |
781 |
784 |
|
782 |
785 |
def __str__(self): |
783 |
786 |
return self.first_name +" "+ self.last_name +" | "+ self.study |
784 |
787 |
|
785 |
788 |
|
786 |
789 |
# Classes regarding roster items |
787 |
790 |
|
788 |
791 |
|
789 |
792 |
class Event(models.Model): |
790 |
793 |
"""An event that will show up in the roster of accounts that need to be |
791 |
794 |
aware of this event. This can be a multitude of things, like colleges |
792 |
795 |
for certain courses, meetings like blood donations, and so on. There are |
793 |
796 |
specialized classes for certain types of events that take place.""" |
794 |
797 |
begin_time = models.DateTimeField( |
795 |
798 |
null=False, |
796 |
799 |
help_text=_("The begin date and time that this event takes place. " |
797 |
800 |
"This value must be a quarter of an hour (0, 15, 30, 45), " |
798 |
801 |
"and take place <em>before</em> this event's end time."), |
799 |
802 |
verbose_name=_("begin time"), |
800 |
803 |
validators=[validate_event_time, validate_university_hours], |
801 |
804 |
) |
802 |
805 |
end_time = models.DateTimeField( |
803 |
806 |
null=False, |
804 |
807 |
help_text=_("The end date and time that this event takes place. " |
805 |
808 |
"This value must be a quarter of an hour (0, 15, 30, 45), " |
806 |
809 |
"and take place <em>after</em> this event's begin time, " |
807 |
810 |
"but it must end on the same day as it begins!"), |
808 |
811 |
verbose_name=_("end time"), |
809 |
812 |
validators=[validate_event_time, validate_university_hours], |
810 |
813 |
) |
811 |
814 |
note = models.TextField( |
812 |
815 |
blank=True, |
813 |
816 |
help_text=_("Optional. If necessary, this field allows for additional " |
814 |
817 |
"information that can be shown to the people for whom this " |
815 |
818 |
"event is."), |
816 |
819 |
) |
817 |
820 |
created = models.DateTimeField( |
818 |
821 |
auto_now_add=True, |
819 |
822 |
) |
820 |
823 |
last_update = models.DateTimeField( |
821 |
824 |
auto_now=True, |
822 |
825 |
) |
823 |
826 |
|
824 |
827 |
def recently_created(self): |
825 |
828 |
"""Indicates if this event was created in the last 5 days.""" |
826 |
829 |
return (datetime.datetime.now(datetime.timezone.utc) - self.created).days <= 5 |
827 |
830 |
def recently_updated(self): |
828 |
831 |
"""Indicates if this event was updated in the last 5 days.""" |
829 |
832 |
return (datetime.datetime.now(datetime.timezone.utc) - self.last_update).days <= 5 |
830 |
833 |
|
831 |
834 |
class CourseEvent(Event): |
832 |
835 |
"""An event related to a particular course. This includes a location, |
833 |
836 |
a group (if applicable), and other data.""" |
834 |
837 |
course = models.ForeignKey( |
835 |
838 |
"courses.CourseProgramme", |
836 |
839 |
on_delete=models.CASCADE, |
837 |
840 |
null=False, |
838 |
841 |
) |
839 |
842 |
docent = models.ForeignKey( |
840 |
843 |
"User", |
841 |
844 |
on_delete=models.PROTECT, |
842 |
845 |
null=False, |
843 |
846 |
limit_choices_to={'is_staff': True}, |
844 |
847 |
help_text=_("The person who will be the main overseer of this event."), |
845 |
848 |
) |
846 |
849 |
room = models.ForeignKey( |
847 |
850 |
"Room", |
848 |
851 |
on_delete=models.PROTECT, |
849 |
852 |
limit_choices_to={'reservable': True}, |
850 |
853 |
null=False, |
851 |
854 |
help_text=_("The room in which this event will be held."), |
852 |
855 |
) |
853 |
856 |
subject = models.CharField( |
854 |
857 |
max_length=32, |
855 |
858 |
blank=False, |
856 |
859 |
help_text=_("The subject of this event. Examples are 'Hoorcollege', " |
857 |
860 |
"'Zelfstudie', ..."), |
858 |
861 |
) |
859 |
862 |
group = models.ForeignKey( |
860 |
863 |
"courses.CourseGroup", |
861 |
864 |
on_delete = models.CASCADE, |
862 |
865 |
null=True, |
863 |
866 |
blank=True, |
864 |
867 |
help_text=_("Some courses have multiple groups. If that's the case, " |
865 |
868 |
"and this event is only for a specific group, then that " |
866 |
869 |
"group must be referenced here."), |
867 |
870 |
) |
868 |
871 |
|
869 |
872 |
def clean(self): |
870 |
873 |
general_reservation_validator(self) |
871 |
874 |
|
872 |
875 |
|
873 |
876 |
class UniversityEvent(Event): |
874 |
877 |
"""University wide events. These include events like blood donations for the |
875 |
878 |
Red Cross, for example.""" |
876 |
879 |
pass |
877 |
880 |
|
878 |
881 |
class StudyEvent(Event): |
879 |
882 |
"""An event that is linked to a particular study, like lectures from guest |
880 |
883 |
speakers about a certain subject, the Flemish Programming Contest, ...""" |
881 |
884 |
pass |
882 |
885 |
|
883 |
886 |
class ExamCommissionDecision(models.Model): |
884 |
887 |
"""The Exam commission can make certain decisions regarding individual |
885 |
888 |
students. Every decision on its own is stored in this table, and is linked |
886 |
889 |
to the recipient's account.""" |
887 |
890 |
user = models.ForeignKey( |
888 |
891 |
User, |
889 |
892 |
on_delete=models.CASCADE, |
890 |
893 |
null=False, |
891 |
894 |
help_text=_("The recipient of this decision."), |
892 |
895 |
) |
893 |
896 |
date = models.DateField(auto_now_add=True) |
894 |
897 |
text = models.TextField( |
895 |
898 |
blank=False, |
896 |
899 |
help_text=_("The text describing the decision. Org syntax available.") |
897 |
900 |
) |
898 |
901 |
def __str__(self): |
899 |
902 |
return str(self.user) + " | " + str(self.date) |
900 |
903 |
|
901 |
904 |
class Meta: |
902 |
905 |
verbose_name = _("Decision of the exam commission") |
903 |
906 |
verbose_name_plural = _("Decisions of the exam commission") |
904 |
907 |
|
905 |
908 |
class EducationDepartmentMessages(models.Model): |
906 |
909 |
"""The department of education can issue messages that are to be shown to |
907 |
910 |
all students. Their contents are stored here.""" |
908 |
911 |
date = models.DateField(auto_now_add=True) |
909 |
912 |
title = models.CharField( |
910 |
913 |
max_length=64, |
911 |
914 |
blank=False, |
912 |
915 |
help_text=_("A short, well-describing title for this message."), |
913 |
916 |
) |
914 |
917 |
text = models.TextField( |
915 |
918 |
blank=False, |
916 |
919 |
help_text=_("The message text. Org syntax available.") |
917 |
920 |
) |
918 |
921 |
def __str__(self): |
919 |
922 |
return str(self.date) + " | " + str(self.title) |
920 |
923 |
|
921 |
924 |
class Meta: |
922 |
925 |
verbose_name = _("Message of the education department") |
923 |
926 |
verbose_name_plural = _("Messages of the education department") |
924 |
927 |