Adding changes to roster branch
- Author
- Maarten 'Vngngdn' Vangeneugden
- Date
- April 11, 2018, 6:07 p.m.
- Hash
- 975dae00e067e87580e76f559ddc403b45c1d8f4
- Parent
- a2bf82891efae1e751a01e38860f8a1a11646db7
- Modified files
- administration/models.py
- administration/templates/administration/nav.djhtml
- administration/urls.py
- administration/views.py
- courses/models.py
- courses/urls.py
- courses/views.py
- joeni/templates/joeni/header.djhtml
- static/css/header.css
administration/models.py ¶
49 additions and 14 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 |
198 |
- | ) |
+ |
198 |
) |
199 |
199 |
year = models.DateField( |
200 |
- | auto_now_add=True, |
201 |
- | db_index=True, |
202 |
- | help_text=_("The academic year for which this curriculum is. " |
+ |
200 |
null=False, |
+ |
201 |
default=datetime.date.today().year, |
+ |
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 courses(self): |
+ |
238 |
""" Returns a dictionary, where the keys are the course_programmes |
+ |
239 |
in this curriculum, and the values are the course_results associated |
+ |
240 |
with them.""" |
+ |
241 |
join_dict = dict() |
+ |
242 |
for course_program in self.course_programmes.all(): |
+ |
243 |
result = CourseResult.objects.filter( |
+ |
244 |
student=self.student).filter( |
+ |
245 |
course_programme=course_program).filter( |
+ |
246 |
year=self.year)[0] |
+ |
247 |
join_dict[course_program] = result |
+ |
248 |
return join_dict |
+ |
249 |
|
+ |
250 |
def courses(self): |
238 |
251 |
""" Returns a set of all the courses that are in this curriculum. |
239 |
252 |
This is not the same as CourseProgrammes, as these can differ depending |
240 |
253 |
on which study one follows. """ |
241 |
254 |
course_set = set() |
242 |
255 |
for course_programme in self.course_programmes.all(): |
243 |
256 |
course_set.add(course_programme.course) |
244 |
257 |
return course_set |
245 |
258 |
|
246 |
259 |
def curriculum_type(self): |
247 |
260 |
""" Returns the type of this curriculum. At the moment, this is |
248 |
261 |
either a standard programme, or an individualized programme. """ |
249 |
262 |
# Currently: A standard programme means: All courses are from the |
250 |
263 |
# same study, ánd from the same year. Additionally, all courses |
251 |
264 |
# from that year must've been taken. |
252 |
265 |
# FIXME: Need a way to determine what is the standard programme. |
253 |
266 |
# If not possible, make this a charfield with options or something |
254 |
267 |
pass |
255 |
268 |
|
256 |
269 |
def __str__(self): |
257 |
270 |
year = self.year.year |
258 |
- | if self.year.month < 7: |
259 |
- | return str(self.student) +" | "+ str(year-1) +"-"+ str(year) |
260 |
- | else: |
261 |
- | return str(self.student) +" | "+ str(year) +"-"+ str(year+1) |
262 |
- | |
+ |
271 |
|
263 |
272 |
|
264 |
273 |
class CourseResult(models.Model): |
265 |
274 |
""" A student has to obtain a certain course result. These are stored here, |
266 |
275 |
together with all the appropriate information. """ |
267 |
276 |
# TODO: Validate that a course programme for a student can only be made once per year for each course, if possible. |
268 |
277 |
CRED = _("Credit acquired") |
269 |
278 |
FAIL = _("Credit not acquired") |
270 |
279 |
TLRD = _("Tolerated") |
271 |
280 |
ITLD = _("Tolerance used") |
272 |
- | BDRG = _("Fraud committed") |
+ |
281 |
BDRG = _("Fraud committed") |
273 |
282 |
VRST = _("Exemption") |
274 |
283 |
STOP = _("Course cancelled") |
275 |
284 |
# Possible to add more in the future |
276 |
285 |
|
277 |
286 |
student = models.ForeignKey( |
278 |
287 |
"User", |
279 |
288 |
on_delete=models.CASCADE, |
280 |
289 |
limit_choices_to={'is_student': True}, |
281 |
- | null=False, |
+ |
290 |
null=False, |
282 |
291 |
db_index=True, |
283 |
292 |
) |
284 |
293 |
course_programme = models.ForeignKey( |
285 |
294 |
"courses.CourseProgramme", |
286 |
295 |
on_delete=models.PROTECT, |
287 |
296 |
null=False, |
288 |
297 |
) |
289 |
298 |
year = models.PositiveIntegerField( |
290 |
299 |
null=False, |
291 |
300 |
default=datetime.date.today().year, |
292 |
301 |
help_text=_("The academic year this course took place in. If 2018 is entered, " |
293 |
302 |
"then that means academic year '2018-2019'."), |
294 |
303 |
) |
295 |
304 |
released = models.DateField( |
296 |
305 |
auto_now=True, |
297 |
306 |
help_text=_("The date that this result was last updated."), |
298 |
307 |
) |
299 |
308 |
first_score = models.PositiveSmallIntegerField( |
300 |
309 |
null=True, # It's possible a score does not exist. |
301 |
310 |
validators=[MaxValueValidator( |
+ |
311 |
validators=[MaxValueValidator( |
302 |
312 |
20, |
303 |
313 |
_("The score mustn't be higher than 20."), |
304 |
314 |
)], |
305 |
315 |
) |
306 |
316 |
second_score = models.PositiveSmallIntegerField( |
307 |
317 |
null=True, |
308 |
318 |
validators=[MaxValueValidator( |
+ |
319 |
validators=[MaxValueValidator( |
309 |
320 |
20, |
310 |
321 |
_("The score mustn't be higher than 20."), |
311 |
322 |
)], |
312 |
323 |
) |
313 |
324 |
result = models.CharField( |
314 |
325 |
max_length=10, |
315 |
326 |
choices = ( |
316 |
327 |
("CRED", CRED), |
317 |
328 |
("FAIL", FAIL), |
318 |
329 |
("TLRD", TLRD), |
319 |
330 |
("ITLD", ITLD), |
320 |
- | ), |
+ |
331 |
("BDRG", BDRG), |
+ |
332 |
("VRST", VRST), |
+ |
333 |
("STOP", STOP), |
+ |
334 |
), |
321 |
335 |
blank=False, |
322 |
336 |
help_text=_("The final result this record constitutes."), |
323 |
337 |
) |
324 |
338 |
|
325 |
339 |
def __str__(self): |
326 |
340 |
stdnum = str(self.student.number) |
327 |
341 |
result = self.result |
328 |
342 |
if result == "CRED": |
329 |
343 |
if self.first_score < 10: |
330 |
344 |
result = "C" + self.first_score + "1" |
331 |
- | else: |
+ |
345 |
else: |
332 |
346 |
result = "C" + self.second_score + "2" |
333 |
- | course = str(self.course_programme.course) |
+ |
347 |
course = str(self.course_programme.course) |
334 |
348 |
return stdnum +" ("+ result +") | "+ course |
335 |
349 |
|
336 |
350 |
class PreRegistration(models.Model): |
337 |
351 |
""" At the beginning of the new academic year, students can register |
338 |
352 |
themselves at the university. Online, they can do a preregistration already. |
339 |
353 |
These records are stored here and can later be retrieved for the actual |
340 |
354 |
registration process. |
341 |
355 |
Note: The current system in use at Hasselt University provides a password system. |
342 |
356 |
That will be eliminated here. Just make sure that the entered details are correct. |
343 |
357 |
Should there be an error, and the same email address is used to update something, |
344 |
358 |
a mail will be sent to that address to verify this was a genuine update.""" |
345 |
359 |
created = models.DateField(auto_now_add=True) |
346 |
360 |
first_name = models.CharField(max_length=64, blank=False, help_text=_("Your first name.")) |
347 |
361 |
last_name = models.CharField(max_length=64, blank=False, help_text=_("Your last name.")) |
348 |
362 |
additional_names = models.CharField(max_length=64, blank=True, help_text=_("Any additional names.")) |
349 |
363 |
title = models.CharField( |
350 |
364 |
max_length=64, |
351 |
365 |
blank=True, |
352 |
366 |
help_text=_("Any additional titles, prefixes, ..."), |
353 |
367 |
) |
354 |
368 |
DOB = models.DateField( |
355 |
369 |
blank=False, |
356 |
370 |
#editable=False, |
357 |
371 |
help_text=_("Your date of birth."), |
358 |
372 |
) |
359 |
373 |
POB = models.CharField( |
360 |
374 |
max_length=64, |
361 |
375 |
blank=False, |
362 |
376 |
#editable=False, |
363 |
377 |
help_text=_("The place you were born."), |
364 |
378 |
) |
365 |
379 |
nationality = models.CharField( |
366 |
380 |
max_length=64, |
367 |
381 |
blank=False, |
368 |
382 |
help_text=_("Your current nationality."), |
369 |
383 |
) |
370 |
384 |
national_registry_number = models.BigIntegerField( |
371 |
385 |
null=True, |
372 |
386 |
help_text=_("If you have one, your national registry number."), |
373 |
387 |
) |
374 |
388 |
civil_status = models.CharField( |
375 |
389 |
max_length=32, |
376 |
390 |
choices = ( |
377 |
391 |
("Single", _("Single")), |
378 |
392 |
("Married", _("Married")), |
379 |
393 |
("Divorced", _("Divorced")), |
380 |
394 |
("Widowed", _("Widowed")), |
381 |
395 |
("Partnership", _("Partnership")), |
382 |
396 |
), |
383 |
397 |
blank=False, |
384 |
398 |
# There may be more; consult http://www.aantrekkingskracht.com/trefwoord/burgerlijke-staat |
385 |
399 |
# for more information. |
386 |
400 |
help_text=_("Your civil/marital status."), |
387 |
401 |
) |
388 |
402 |
email = models.EmailField( |
389 |
403 |
blank=False, |
390 |
404 |
unique=True, |
391 |
405 |
help_text=_("The e-mail address we will use to communicate until your actual registration."), |
392 |
406 |
) |
393 |
407 |
study = models.ForeignKey( |
394 |
408 |
"courses.Study", |
395 |
409 |
on_delete=models.PROTECT, |
396 |
410 |
null=False, |
397 |
411 |
help_text=_("The study you wish to follow. Be sure to provide all legal" |
398 |
412 |
"documents that are required for this study with this " |
399 |
413 |
"application, or bring them with you to the final registration."), |
400 |
414 |
) |
401 |
415 |
study_type = models.CharField( |
402 |
416 |
max_length=32, |
403 |
417 |
choices = ( |
404 |
418 |
("Diplom contract", _("Diplom contract")), |
405 |
419 |
("Exam contract", _("Exam contract")), |
406 |
420 |
("Credit contract", _("Credit contract")), |
407 |
421 |
), |
408 |
422 |
blank=False, |
409 |
423 |
help_text=_("The type of study contract you wish to follow."), |
410 |
424 |
) |
411 |
425 |
document = models.FileField( |
412 |
426 |
upload_to="pre-enrollment/%Y", |
413 |
427 |
help_text=_("Any legal documents regarding your enrollment."), |
414 |
428 |
) |
415 |
429 |
# XXX: If the database in production is PostgreSQL, comment document, and |
416 |
430 |
# uncomment the next column. |
417 |
431 |
"""documents = models.ArrayField( |
418 |
432 |
models.FileField(upload_to="pre-enrollment/%Y"), |
419 |
433 |
help_text=_("Any legal documents regarding your enrollment."), |
420 |
434 |
)""" |
421 |
435 |
|
422 |
436 |
def __str__(self): |
423 |
437 |
name = self.last_name +" "+ self.first_name |
424 |
438 |
dob = self.DOB.strftime("%d/%m/%Y") |
425 |
439 |
return name +" | "+ dob |
426 |
440 |
|
427 |
441 |
|
428 |
442 |
# Planning and organization related tables |
429 |
443 |
class Room(models.Model): |
430 |
444 |
""" Represents a room in the university. |
431 |
445 |
Rooms can have a number of properties, which are stored in the database. |
432 |
446 |
""" |
433 |
447 |
# Types of rooms |
434 |
448 |
LABORATORY = _("Laboratory") # Chemistry/Physics equipped rooms |
435 |
449 |
CLASS_ROOM = _("Class room") # Simple class rooms |
436 |
450 |
AUDITORIUM = _("Auditorium") # Large rooms with ample seating and equipment for lectures |
437 |
451 |
PC_ROOM = _("PC room" ) # Rooms equipped for executing PC related tasks |
438 |
452 |
PUBLIC_ROOM= _("Public room") # Restaurants, restrooms, ... general public spaces |
439 |
453 |
OFFICE = _("Office" ) # Private offices for staff |
440 |
454 |
PRIVATE_ROOM = _("Private room") # Rooms accessible for a limited public; cleaning cupboards, kitchens, ... |
441 |
455 |
WORKSHOP = _("Workshop" ) # Rooms with hardware equipment to build and work on materials |
442 |
456 |
OTHER = _("Other" ) # Rooms that do not fit in any other category |
443 |
457 |
|
444 |
458 |
|
445 |
459 |
name = models.CharField( |
446 |
460 |
max_length=20, |
447 |
461 |
primary_key=True, |
448 |
462 |
blank=False, |
449 |
463 |
help_text=_("The name of this room. If more appropriate, this can be the colloquial name."), |
450 |
464 |
) |
451 |
465 |
seats = models.PositiveSmallIntegerField( |
452 |
466 |
help_text=_("The amount of available seats in this room."), |
453 |
467 |
) |
454 |
468 |
wheelchair_accessible = models.BooleanField(default=True) |
455 |
469 |
exams_equipped = models.BooleanField( |
456 |
470 |
default=True, |
457 |
471 |
help_text=_("Indicates if exams can reasonably be held in this room."), |
458 |
472 |
) |
459 |
473 |
loose_tables = models.BooleanField( |
460 |
474 |
default=True, |
461 |
475 |
help_text=_("If true, the tables in this room can be moved freely. " |
462 |
476 |
"If false, they're bolted down in their positions."), |
463 |
477 |
) |
464 |
478 |
electrical_plugs = models.PositiveSmallIntegerField( |
465 |
479 |
help_text=_("The amount of electrical plugs that are available to the " |
466 |
480 |
"people for free use. Electrical plugs that are more or " |
467 |
481 |
"less constantly occupied by permanent equipment (such as " |
468 |
482 |
"computers, beamers, ...) are excluded from counting."), |
469 |
483 |
) |
470 |
484 |
exterior_window = models.BooleanField( |
471 |
485 |
default=True, |
472 |
486 |
help_text=_("Indicates if this room has a window to the outside."), |
473 |
487 |
) |
474 |
488 |
software_available = models.TextField( |
475 |
489 |
blank=True, |
476 |
490 |
help_text=_("Some software used at the university is proprietary, and " |
477 |
491 |
"thus not available at every system. If certain " |
478 |
492 |
"software is installed on the computers in this room that " |
479 |
493 |
"cannot be found on other computers, list them here."), |
480 |
494 |
) |
481 |
495 |
computers_available = models.PositiveSmallIntegerField( |
482 |
496 |
default=0, |
483 |
497 |
help_text=_("Indicates how many computers are available in this room."), |
484 |
498 |
) |
485 |
499 |
projector_available = models.BooleanField( |
486 |
500 |
default=False, |
487 |
501 |
help_text=_("Indicates if a projector is available at this room."), |
488 |
502 |
) |
489 |
503 |
blackboards_available = models.PositiveSmallIntegerField( |
490 |
504 |
help_text=_("The amount of blackboards available in this room."), |
491 |
505 |
) |
492 |
506 |
whiteboards_available = models.PositiveSmallIntegerField( |
493 |
507 |
help_text=_("The amount of whiteboards available in this room."), |
494 |
508 |
) |
495 |
509 |
category = models.CharField( |
496 |
510 |
max_length=16, |
497 |
511 |
blank=False, |
498 |
512 |
choices = ( |
499 |
513 |
("LABORATORY", LABORATORY), |
500 |
514 |
("CLASS_ROOM", CLASS_ROOM), |
501 |
515 |
("AUDITORIUM", AUDITORIUM), |
502 |
516 |
("PC_ROOM", PC_ROOM), |
503 |
517 |
("PUBLIC_ROOM", PUBLIC_ROOM), |
504 |
518 |
("OFFICE", OFFICE), |
505 |
519 |
("PRIVATE_ROOM", PRIVATE_ROOM), |
506 |
520 |
("WORKSHOP", WORKSHOP), |
507 |
521 |
("OTHER", OTHER), |
508 |
522 |
), |
509 |
523 |
help_text=_("The category that best suits the character of this room."), |
510 |
524 |
) |
511 |
525 |
reservable = models.BooleanField( |
512 |
526 |
default=True, |
513 |
527 |
help_text=_("Indicates if this room can be reserved for something."), |
514 |
528 |
) |
515 |
529 |
note = models.TextField( |
516 |
530 |
blank=True, |
517 |
531 |
help_text=_("If some additional info is required for this room, like a " |
518 |
532 |
"characteristic property (e.g. 'Usually occupied by 2BACH " |
519 |
533 |
"informatics'), state it here."), |
520 |
534 |
) |
521 |
535 |
# TODO: Add a campus/building field or not? |
522 |
536 |
|
523 |
537 |
def reservation_possible(self, begin, end, seats=None): |
+ |
538 |
""" Returns the next reservation starting from the given time, or, if |
+ |
539 |
the next reservation starts on the given time, that reservation. |
+ |
540 |
Returns None if there is no reservation from this moment on.""" |
+ |
541 |
reservations = RoomReservation.objects.filter(room=self).filter(begin_time__gte=time).order_by('begin_time') |
+ |
542 |
if len(reservations) == 0: |
+ |
543 |
return None |
+ |
544 |
else: |
+ |
545 |
return reservations[0] |
+ |
546 |
def next_event(self, time): |
+ |
547 |
""" Returns the next event starting from the given time, or, if |
+ |
548 |
the next event starts on the given time, that event. |
+ |
549 |
Returns None if there is no event from this moment on.""" |
+ |
550 |
events = CourseEvent.objects.filter(room=self).filter(begin_time__gte=time).order_by('begin_time') |
+ |
551 |
if len(events) == 0: |
+ |
552 |
return None |
+ |
553 |
else: |
+ |
554 |
return events[0] |
+ |
555 |
|
+ |
556 |
|
+ |
557 |
def reservation_possible(self, begin, end, seats=None): |
524 |
558 |
""" Returns a boolean indicating if reservating during the given time |
+ |
559 |
""" Returns a boolean indicating if reservating during the given time |
525 |
560 |
is possible. If the begin overlaps with a reservation's end or vice versa, |
526 |
561 |
this is regarded as possible. |
527 |
562 |
Takes seats as optional argument. If not specified, it is assumed the entire |
528 |
563 |
room has to be reserved. """ |
529 |
564 |
if self.reservable is False: |
530 |
565 |
return False |
531 |
566 |
if seats is not None and seats < 0: raise ValueError(_("seats ∈ ℕ")) |
532 |
567 |
|
533 |
568 |
reservations = RoomReservation.objects.filter(room=self) |
534 |
569 |
for reservation in reservations: |
535 |
570 |
if reservation.end_time <= begin or reservation.begin_time >= end: |
536 |
571 |
continue # Can be trivially skipped, no overlap here |
537 |
572 |
elif seats is None or reservation.seats is None: |
538 |
573 |
return False # The whole room cannot be reserved -> False |
539 |
574 |
elif seats + reservation.seats > self.seats: |
540 |
575 |
return False # Total amount of seats exceeds the available amount -> False |
541 |
576 |
return True # No overlappings found -> True |
542 |
577 |
|
543 |
578 |
def __str__(self): |
544 |
579 |
return self.name |
545 |
580 |
|
546 |
581 |
|
547 |
582 |
# Validators that will be used for RoomReservations and Events |
548 |
583 |
def validate_event_time(time): |
549 |
584 |
"""Checks if the time is a quarter of an hour (0, 15, 30, or 45).""" |
550 |
585 |
if time.minute not in [0, 15, 30, 45] or time.second != 0: |
551 |
586 |
raise ValidationError( |
552 |
587 |
_('%(time)s is not in the quarter of an hour.'), |
553 |
588 |
params={'time': time.strftime("%H:%M")}) |
554 |
589 |
def validate_university_hours(value): |
555 |
590 |
"""Checks if the datetime value given takes place during the opening hours |
556 |
591 |
of the university (08:00 - 20:00).""" |
557 |
592 |
if value.hour < 8 or (value.hour == 22 and value.minute != 0) or value.hour >= 23: |
558 |
593 |
raise ValidationError( |
559 |
594 |
_("All events and reservations must begin and end between 08:00 " |
560 |
595 |
"and 22:00.")) |
561 |
596 |
def overlaps(begin_a, end_a, begin_b, end_b): |
562 |
597 |
"""Checks if timespan a and b overlap with each other. If one of them ends at |
563 |
598 |
the same time the other one begins, it does not count as an overlap. |
564 |
599 |
This function assumes the end takes place strictly /after/ the begin.""" |
565 |
600 |
if end_a <= begin_b or end_b <= begin_a: |
566 |
601 |
return False |
567 |
602 |
if ( |
568 |
603 |
begin_a < begin_b <= end_a or |
569 |
604 |
begin_b < begin_a <= end_b or |
570 |
605 |
begin_a <= end_b < end_a or |
571 |
606 |
begin_b <= end_a < end_b): |
572 |
607 |
return True |
573 |
608 |
else: |
574 |
609 |
return False |
575 |
610 |
|
576 |
611 |
|
577 |
612 |
def general_reservation_validator(self): |
578 |
613 |
# Check for overlapping reservations |
579 |
614 |
# TODO: Try to make it possible to link to the reservator, |
580 |
615 |
# to display the reason, to show the available times that a |
581 |
616 |
# reservation can be made for that room, and so on... Make it |
582 |
617 |
# a bit more interactive. |
583 |
618 |
for reservation in RoomReservation.objects.filter(room=self.room): |
584 |
619 |
if overlaps(self.begin_time, |
585 |
620 |
self.end_time, |
586 |
621 |
reservation.begin_time, |
587 |
622 |
reservation.end_time): |
588 |
623 |
if isinstance(self, RoomReservation): |
589 |
624 |
if self.room.reservation_possible(self.begin_time, self.end_time, self.seats): |
590 |
625 |
continue # Both reservations can take place in the same room |
591 |
626 |
raise ValidationError( |
592 |
627 |
_("It is not possible to plan this event/reservation in " |
593 |
628 |
"%(room)s from %(self_begin)s to %(end_begin)s on %(day)s. " |
594 |
629 |
"%(reservator)s has already " |
595 |
630 |
"reserved it from %(res_begin)s to %(res_end)s."), |
596 |
631 |
params={'room': str(self.room), |
597 |
632 |
'self_begin': self.begin_time.strftime("%H:%M"), |
598 |
633 |
'self_end': self.end_time.strftime("%H:%M"), |
599 |
634 |
'day': self.begin_time.strftime("%A (%d/%m)"), |
600 |
635 |
'reservator': str(reservation.reservator), |
601 |
636 |
'res_begin': reservation.begin_time.strftime("%H:%M"), |
602 |
637 |
'res_end': reservation.end_time.strftime("%H:%M"), |
603 |
638 |
}) |
604 |
639 |
for course_event in CourseEvent.objects.filter(room=self.room): |
605 |
640 |
if overlaps(self.begin_time, |
606 |
641 |
self.end_time, |
607 |
642 |
course_event.begin_time, |
608 |
643 |
course_event.end_time): |
609 |
644 |
raise ValidationError( |
610 |
645 |
_("%(docent)s has organized a %(subject)s in %(room)s from " |
611 |
646 |
"%(res_begin)s to %(res_end)s on %(day)s, so you cannot " |
612 |
647 |
"place a reservation there from %(self_begin)s to " |
613 |
648 |
"%(self_end)s."), |
614 |
649 |
params={'room': str(self.room), |
615 |
650 |
'self_begin': self.begin_time.strftime("%H:%M"), |
616 |
651 |
'self_end': self.end_time.strftime("%H:%M"), |
617 |
652 |
'day': self.begin_time.strftime("%A (%d/%m)"), |
618 |
653 |
'docent': str(course_event.docent), |
619 |
654 |
'subject': course_event.subject, |
620 |
655 |
'res_begin': course_event.begin_time.strftime("%H:%M"), |
621 |
656 |
'res_end': course_event.end_time.strftime("%H:%M"),}) |
622 |
657 |
|
623 |
658 |
# Checking for correct timings: |
624 |
659 |
if self.begin_time >= self.end_time: |
625 |
660 |
raise ValidationError( |
626 |
661 |
_("The begin time (%(begin)) must take place <em>before</em> " |
627 |
662 |
"the end time (%(end))."), |
628 |
663 |
params={'begin': self.begin_time.strftime("%H:%M"), |
629 |
664 |
'end': self.end_time.strftime("%H:%M"),}) |
630 |
665 |
"""if not roster.same_day(self.begin_time, self.end_time): |
631 |
666 |
raise ValidationError( |
632 |
667 |
_("The event/reservation must begin and end on the same day."))""" |
633 |
668 |
|
634 |
669 |
|
635 |
670 |
class RoomReservation(models.Model): |
636 |
671 |
""" Rooms are to be reserved from time to time. They can be reserved |
637 |
672 |
by externals, for something else, and whatnot. That is stored in this table. |
638 |
673 |
""" |
639 |
674 |
room = models.ForeignKey( |
640 |
675 |
"Room", |
641 |
676 |
on_delete=models.CASCADE, |
642 |
677 |
null=False, |
643 |
678 |
#editable=False, |
644 |
679 |
db_index=True, |
645 |
680 |
limit_choices_to={"reservable": True}, |
646 |
681 |
help_text=_("The room that is being reserved at this point."), |
647 |
682 |
) |
648 |
683 |
reservator = models.ForeignKey( |
649 |
684 |
"User", |
650 |
685 |
on_delete=models.CASCADE, |
651 |
686 |
null=False, |
652 |
687 |
#editable=False, |
653 |
688 |
help_text=_("The person that made the reservation (and thus responsible)."), |
654 |
689 |
) |
655 |
690 |
timestamp = models.DateTimeField(auto_now_add=True) |
656 |
691 |
begin_time = models.DateTimeField( |
657 |
692 |
null=False, |
658 |
693 |
help_text=_("The time that this reservation begin."), |
659 |
694 |
validators=[validate_event_time,validate_university_hours], |
660 |
695 |
) |
661 |
696 |
end_time = models.DateTimeField( |
662 |
697 |
null=False, |
663 |
698 |
help_text=_("The time that this reservation ends."), |
664 |
699 |
validators=[validate_event_time,validate_university_hours], |
665 |
700 |
) |
666 |
701 |
seats = models.PositiveSmallIntegerField( |
667 |
702 |
null=True, |
668 |
703 |
blank=True, |
669 |
704 |
help_text=_("Indicates how many seats are required. If this is left empty, " |
670 |
705 |
"it is assumed the entire room has to be reserved."), |
671 |
706 |
) |
672 |
707 |
reason = models.CharField( |
673 |
708 |
max_length=64, |
674 |
709 |
blank=True, |
675 |
710 |
help_text=_("The reason for this reservation, if useful."), |
676 |
711 |
) |
677 |
712 |
note = models.TextField( |
678 |
713 |
blank=True, |
679 |
714 |
help_text=_("If some additional info is required for this reservation, " |
680 |
715 |
"state it here."), |
681 |
716 |
) |
682 |
717 |
|
683 |
718 |
def __str__(self): |
684 |
719 |
start = self.start_time.strftime("%H:%M") |
685 |
720 |
end = self.end_time.strftime("%H:%M") |
686 |
721 |
return str(self.room) +" | "+ start +"-"+ end |
687 |
722 |
|
688 |
723 |
def clean(self): |
689 |
724 |
general_reservation_validator(self) |
690 |
725 |
|
691 |
726 |
class Degree(models.Model): |
692 |
727 |
""" Contains all degrees that were achieved at this university. |
693 |
728 |
There are no foreign keys in this field. This allows system |
694 |
729 |
administrators to safely remove accounts from alumni, without |
695 |
730 |
the risk of breaking referential integrity or accidentally removing |
696 |
731 |
degrees. |
697 |
732 |
While keeping some fields editable that look like they shouldn't be |
698 |
733 |
(e.g. first_name), this makes it possible for alumni to have a name change |
699 |
734 |
later in their life, and still being able to get a copy of their degree. """ |
700 |
735 |
""" Reason for an ID field for every degree: |
701 |
736 |
This system allows for employers to verify that a certain applicant has indeed, |
702 |
737 |
achieved the degrees (s)he proclaims to have. Because of privacy concerns, |
703 |
738 |
a university cannot disclose information about alumni. |
704 |
739 |
That's where the degree ID comes in. This ID can be printed on all future |
705 |
740 |
degrees. The employer can then visit the university's website, and simply |
706 |
741 |
enter the ID. The website will then simply print what study is attached to |
707 |
742 |
this degree, but not disclose names or anything identifiable. This strikes |
708 |
743 |
thé perfect balance between (easy and digital) degree verification for employers, and maintaining |
709 |
744 |
alumni privacy to the highest extent possible. """ |
710 |
745 |
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) |
711 |
746 |
first_name = models.CharField( |
712 |
747 |
max_length=64, |
713 |
748 |
blank=False, |
714 |
749 |
) |
715 |
750 |
last_name = models.CharField( |
716 |
751 |
max_length=64, |
717 |
752 |
blank=False, |
718 |
753 |
) |
719 |
754 |
additional_names = models.CharField( |
720 |
755 |
max_length=64, |
721 |
756 |
blank=True, |
722 |
757 |
) |
723 |
758 |
DOB = models.DateField(null=False)#editable=False, null=False) # This can't be changed, of course |
724 |
759 |
POB = models.CharField( |
725 |
760 |
max_length=64, |
726 |
761 |
blank=False, |
727 |
762 |
#editable=False, |
728 |
763 |
) |
729 |
764 |
# The study also has to be a charfield, because if a study is removed, |
730 |
765 |
# The information will be lost. |
731 |
766 |
study = models.CharField( |
732 |
767 |
max_length=64, |
733 |
768 |
blank=False, |
734 |
769 |
#editable=False, |
735 |
770 |
) |
736 |
771 |
achieved = models.DateField(null=False)#editable=False, null=False) |
737 |
772 |
user = models.ForeignKey( |
738 |
773 |
"User", |
739 |
774 |
on_delete=models.SET_NULL, |
740 |
775 |
null=True, |
741 |
776 |
help_text=_("The person that achieved this degree, if (s)he still has " |
742 |
777 |
"an account at this university. If the account is deleted " |
743 |
778 |
"at a later date, this field will be set to NULL, but the " |
744 |
779 |
"other fields will be retained."), |
745 |
780 |
) |
746 |
781 |
|
747 |
782 |
def __str__(self): |
748 |
783 |
return self.first_name +" "+ self.last_name +" | "+ self.study |
749 |
784 |
|
750 |
785 |
|
751 |
786 |
# Classes regarding roster items |
752 |
787 |
|
753 |
788 |
|
754 |
789 |
class Event(models.Model): |
755 |
790 |
"""An event that will show up in the roster of accounts that need to be |
756 |
791 |
aware of this event. This can be a multitude of things, like colleges |
757 |
792 |
for certain courses, meetings like blood donations, and so on. There are |
758 |
793 |
specialized classes for certain types of events that take place.""" |
759 |
794 |
begin_time = models.DateTimeField( |
760 |
795 |
null=False, |
761 |
796 |
help_text=_("The begin date and time that this event takes place. " |
762 |
797 |
"This value must be a quarter of an hour (0, 15, 30, 45), " |
763 |
798 |
"and take place <em>before</em> this event's end time."), |
764 |
799 |
verbose_name=_("begin time"), |
765 |
800 |
validators=[validate_event_time, validate_university_hours], |
766 |
801 |
) |
767 |
802 |
end_time = models.DateTimeField( |
768 |
803 |
null=False, |
769 |
804 |
help_text=_("The end date and time that this event takes place. " |
770 |
805 |
"This value must be a quarter of an hour (0, 15, 30, 45), " |
771 |
806 |
"and take place <em>after</em> this event's begin time, " |
772 |
807 |
"but it must end on the same day as it begins!"), |
773 |
808 |
verbose_name=_("end time"), |
774 |
809 |
validators=[validate_event_time, validate_university_hours], |
775 |
810 |
) |
776 |
811 |
note = models.TextField( |
777 |
812 |
blank=True, |
778 |
813 |
help_text=_("Optional. If necessary, this field allows for additional " |
779 |
814 |
"information that can be shown to the people for whom this " |
780 |
815 |
"event is."), |
781 |
816 |
) |
782 |
817 |
created = models.DateTimeField( |
783 |
818 |
auto_now_add=True, |
784 |
819 |
) |
785 |
820 |
last_update = models.DateTimeField( |
786 |
821 |
auto_now=True, |
787 |
822 |
) |
788 |
823 |
|
789 |
824 |
def recently_created(self): |
790 |
825 |
"""Indicates if this event was created in the last 5 days.""" |
791 |
826 |
return (datetime.datetime.now(datetime.timezone.utc) - self.created).days <= 5 |
792 |
827 |
def recently_updated(self): |
793 |
828 |
"""Indicates if this event was updated in the last 5 days.""" |
794 |
829 |
return (datetime.datetime.now(datetime.timezone.utc) - self.last_update).days <= 5 |
795 |
830 |
|
796 |
831 |
class CourseEvent(Event): |
797 |
832 |
"""An event related to a particular course. This includes a location, |
798 |
833 |
a group (if applicable), and other data.""" |
799 |
834 |
course = models.ForeignKey( |
800 |
835 |
"courses.CourseProgramme", |
801 |
836 |
on_delete=models.CASCADE, |
802 |
837 |
null=False, |
803 |
838 |
) |
804 |
839 |
docent = models.ForeignKey( |
805 |
840 |
"User", |
806 |
841 |
on_delete=models.PROTECT, |
807 |
842 |
null=False, |
808 |
843 |
limit_choices_to={'is_staff': True}, |
809 |
844 |
help_text=_("The person who will be the main overseer of this event."), |
810 |
845 |
) |
811 |
846 |
room = models.ForeignKey( |
812 |
847 |
"Room", |
813 |
848 |
on_delete=models.PROTECT, |
814 |
849 |
limit_choices_to={'reservable': True}, |
815 |
850 |
null=False, |
816 |
851 |
help_text=_("The room in which this event will be held."), |
817 |
852 |
) |
818 |
853 |
subject = models.CharField( |
819 |
854 |
max_length=32, |
820 |
855 |
blank=False, |
821 |
856 |
help_text=_("The subject of this event. Examples are 'Hoorcollege', " |
822 |
857 |
"'Zelfstudie', ..."), |
823 |
858 |
) |
824 |
859 |
group = models.ForeignKey( |
825 |
860 |
"courses.CourseGroup", |
826 |
861 |
on_delete = models.CASCADE, |
827 |
862 |
null=True, |
828 |
863 |
blank=True, |
829 |
864 |
help_text=_("Some courses have multiple groups. If that's the case, " |
830 |
865 |
"and this event is only for a specific group, then that " |
831 |
866 |
"group must be referenced here."), |
832 |
867 |
) |
833 |
868 |
|
834 |
869 |
def clean(self): |
835 |
870 |
general_reservation_validator(self) |
836 |
871 |
|
837 |
872 |
|
838 |
873 |
class UniversityEvent(Event): |
839 |
874 |
"""University wide events. These include events like blood donations for the |
840 |
875 |
Red Cross, for example.""" |
841 |
876 |
pass |
842 |
877 |
|
843 |
878 |
class StudyEvent(Event): |
844 |
879 |
"""An event that is linked to a particular study, like lectures from guest |
845 |
880 |
speakers about a certain subject, the Flemish Programming Contest, ...""" |
846 |
881 |
pass |
847 |
882 |
|
848 |
883 |
class ExamCommissionDecision(models.Model): |
849 |
884 |
"""The Exam commission can make certain decisions regarding individual |
850 |
885 |
students. Every decision on its own is stored in this table, and is linked |
851 |
886 |
to the recipient's account.""" |
852 |
887 |
user = models.ForeignKey( |
853 |
888 |
User, |
854 |
889 |
on_delete=models.CASCADE, |
855 |
890 |
null=False, |
856 |
891 |
help_text=_("The recipient of this decision."), |
857 |
892 |
) |
858 |
893 |
date = models.DateField(auto_now_add=True) |
859 |
894 |
text = models.TextField( |
860 |
895 |
blank=False, |
861 |
896 |
help_text=_("The text describing the decision. Org syntax available.") |
862 |
897 |
) |
863 |
898 |
def __str__(self): |
864 |
899 |
return str(self.user) + " | " + str(self.date) |
865 |
900 |
|
866 |
901 |
class Meta: |
867 |
902 |
verbose_name = _("Decision of the exam commission") |
868 |
903 |
verbose_name_plural = _("Decisions of the exam commission") |
869 |
904 |
|
870 |
905 |
class EducationDepartmentMessages(models.Model): |
871 |
906 |
"""The department of education can issue messages that are to be shown to |
872 |
907 |
all students. Their contents are stored here.""" |
873 |
908 |
date = models.DateField(auto_now_add=True) |
874 |
909 |
title = models.CharField( |
875 |
910 |
max_length=64, |
876 |
911 |
blank=False, |
877 |
912 |
help_text=_("A short, well-describing title for this message."), |
878 |
913 |
) |
879 |
914 |
text = models.TextField( |
880 |
915 |
blank=False, |
881 |
916 |
help_text=_("The message text. Org syntax available.") |
882 |
917 |
) |
883 |
918 |
def __str__(self): |
884 |
919 |
return str(self.date) + " | " + str(self.title) |
885 |
920 |
|
886 |
921 |
class Meta: |
887 |
922 |
verbose_name = _("Message of the education department") |
888 |
923 |
verbose_name_plural = _("Messages of the education department") |
889 |
924 |
administration/templates/administration/nav.djhtml ¶
1 addition and 1 deletion.
View changes Hide changes
1 |
1 |
{% load i18n %} |
2 |
2 |
<nav> |
3 |
3 |
<a href="{% url 'administration-settings' %}">{% trans "Personal settings" %}</a> |
4 |
4 |
<!--<a href="{% url 'administration-curriculum' %}">{% trans "Curricula" %}</a>--> |
5 |
- | <a href="{% url 'administration-forms' %}">{% trans "Forms" %}</a> |
+ |
5 |
<a href="{% url 'administration-forms' %}">{% trans "Forms" %}</a> |
6 |
6 |
<a href="{% url 'administration-rooms' %}">{% trans "Rooms" %}</a> |
7 |
7 |
<a href="{% url 'administration-jobs' %}">{% trans "Jobs" %}</a> |
8 |
8 |
<a href="{% url 'administration-roster' %}">{% trans "Personal Roster" %}</a> |
9 |
9 |
<a href="{% url 'administration-bulletin-board' %}">{% trans "Bulletin board" %}</a> |
10 |
10 |
PingPing: €{{ money }} |
11 |
11 |
</nav> |
12 |
12 |
administration/urls.py ¶
13 additions and 13 deletions.
View changes Hide changes
1 |
1 |
from django.contrib.auth import views as auth_views |
2 |
2 |
from . import views |
3 |
3 |
from django.utils.translation import gettext_lazy as _ |
4 |
4 |
|
5 |
5 |
urlpatterns = ([ |
6 |
6 |
path('', views.index, name='administration-index'), |
7 |
7 |
path(_('pre-registration'), views.pre_registration, name='administration-pre-registration'), |
8 |
- | path(_('settings'), views.settings, name='administration-settings'), |
+ |
8 |
path(_('settings'), views.settings, name='administration-settings'), |
9 |
9 |
path(_('curriculum'), views.curriculum, name='administration-curriculum'), |
10 |
- | # Commented because they might very well be merged with curriculum |
+ |
10 |
# Commented because they might very well be merged with curriculum |
11 |
11 |
#path(_('results'), views.results, name='administration-results'), |
12 |
12 |
#path(_('results/<slug:course>'), views.result, name='administration-results'), |
13 |
13 |
#path(_('results/<int:student_id>'), views.result, name='administration-results'), |
14 |
14 |
path(_('forms'), views.forms, name='administration-forms'), # In Dutch: "Attesten" |
15 |
- | path(_('forms/<str:form>'), views.forms, name='administration-forms'), |
16 |
- | path(_('rooms'), views.rooms, name='administration-rooms'), |
17 |
- | path(_('rooms/<str:room>'), views.room_detail, name='administration-room-detail'), |
18 |
- | #path(_('rooms/reservate'), views.room_reservate, name='administration-room-reservate'), |
+ |
15 |
path(_('forms/<str:form>'), views.forms, name='administration-forms'), # HOLD |
+ |
16 |
path(_('rooms'), views.rooms, name='administration-rooms'), # TODO |
+ |
17 |
path(_('rooms/<str:room>'), views.room_detail, name='administration-room-detail'), # TODO |
+ |
18 |
#path(_('rooms/reservate'), views.room_reservate, name='administration-room-reservate'), |
19 |
19 |
path(_('roster'), views.roster, name='administration-roster'), |
20 |
- | re_path(_('roster/(?P<begin>[0-9]{2}-[0-9]{2}-[0-9]{4})/(?P<end>[0-9]{2}-[0-9]{2}-[0-9]{4})'), views.roster, name='administration-roster'), |
21 |
- | path(_('jobs'), views.jobs, name='administration-jobs'), |
22 |
- | path(_('bulletin-board'), views.bulletin_board, name='administration-bulletin-board'), |
23 |
- | path(_('user/<slug:slug_name>'), views.user, name='administration-user'), |
24 |
- | path(_('roster/<slug:user_slug>.ics'), views.roster_ics, name='administration-roster-ics'), |
25 |
- | |
+ |
20 |
re_path(_('roster/(?P<begin>[0-9]{2}-[0-9]{2}-[0-9]{4})/(?P<end>[0-9]{2}-[0-9]{2}-[0-9]{4})'), views.roster, name='administration-roster'), # TODO |
+ |
21 |
path(_('jobs'), views.jobs, name='administration-jobs'), # HOLD |
+ |
22 |
path(_('bulletin-board'), views.bulletin_board, name='administration-bulletin-board'), # TODO |
+ |
23 |
path(_('user/<slug:slug_name>'), views.user, name='administration-user'), # TODO |
+ |
24 |
path(_('roster/<slug:user_slug>.ics'), views.roster_ics, name='administration-roster-ics'), # TODO |
+ |
25 |
|
26 |
26 |
path('login', views.login, name='administration-login'), |
27 |
- | ]) |
+ |
27 |
]) |
28 |
28 |
administration/views.py ¶
23 additions and 2 deletions.
View changes Hide changes
1 |
1 |
from collections import OrderedDict |
2 |
2 |
from django.http import HttpResponseRedirect |
3 |
3 |
import datetime |
4 |
4 |
from django.urls import reverse # Why? |
5 |
5 |
from django.utils.translation import gettext as _ |
6 |
6 |
from .models import * |
7 |
7 |
from .forms import UserDataForm |
8 |
8 |
from .new_roster import create_roster_rows |
9 |
9 |
import administration |
10 |
10 |
from django.contrib.auth.decorators import login_required |
11 |
11 |
from django.contrib.auth import authenticate |
12 |
12 |
|
13 |
13 |
@login_required |
14 |
14 |
def roster(request, begin=None, end=None): |
15 |
15 |
"""Collects and renders the data that has to be displayed in the roster. |
16 |
16 |
|
17 |
17 |
The begin and end date can be specified. Only roster points in that range |
18 |
18 |
will be included in the response. If no begin and end are specified, it will |
19 |
19 |
take the current week as begin and end point. If it's |
20 |
20 |
weekend, it will take next week.""" |
21 |
21 |
|
22 |
22 |
# TODO Handle given begin and end |
23 |
23 |
context = dict() |
24 |
24 |
#context = {'money' : update_balance(None)} |
25 |
25 |
template = "administration/roster.djhtml" |
26 |
26 |
|
27 |
27 |
if begin is None or end is None: |
28 |
28 |
today = datetime.date.today() |
29 |
29 |
if today.isoweekday() in {6,7}: # Weekend |
30 |
30 |
begin = today + datetime.timedelta(days=8-today.isoweekday()) |
31 |
31 |
end = today + datetime.timedelta(days=13-today.isoweekday()) |
32 |
32 |
else: # Same week |
33 |
33 |
begin = today - datetime.timedelta(days=today.weekday()) |
34 |
34 |
end = today + datetime.timedelta(days=5-today.isoweekday()) |
35 |
35 |
else: # Changing regexes to date objects |
36 |
36 |
b = begin.split("-") |
37 |
37 |
e = end.split("-") |
38 |
38 |
begin = datetime.datetime(int(b[2]),int(b[1]),int(b[0])) |
39 |
39 |
end = datetime.datetime(int(e[2]),int(e[1]),int(e[0])) |
40 |
40 |
|
41 |
41 |
context['begin'] = begin |
42 |
42 |
context['end'] = end |
43 |
43 |
|
44 |
44 |
context['prev_begin'] = (begin - datetime.timedelta(days=7)).strftime("%d-%m-%Y") |
45 |
45 |
context['prev_end'] = (begin - datetime.timedelta(days=2)).strftime("%d-%m-%Y") |
46 |
46 |
context['next_begin'] = (end + datetime.timedelta(days=2)).strftime("%d-%m-%Y") |
47 |
47 |
context['next_end'] = (end + datetime.timedelta(days=7)).strftime("%d-%m-%Y") |
48 |
48 |
|
49 |
49 |
days = [begin] |
50 |
50 |
while (end-days[-1]).days != 0: |
51 |
51 |
# Human translation: Keep adding days until the last day in the array of |
52 |
52 |
# days is the same day as the last day the user wants to see the roster for. |
53 |
53 |
days.append(days[-1] + datetime.timedelta(days=1)) |
54 |
54 |
context['days'] = days |
55 |
55 |
|
56 |
56 |
# Collecting events |
57 |
57 |
course_events = CourseEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end).order_by("begin_time") |
58 |
58 |
#university_events = UniversityEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end) |
59 |
59 |
#study_events = StudyEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end) |
60 |
60 |
#events = Event.objects.filter(begin_time__gte=begin).filter(end_time__lte=end) |
61 |
61 |
conflicts, table_code = create_roster_rows(course_events) |
62 |
62 |
|
63 |
63 |
context['time_blocks'] = table_code |
64 |
64 |
context['conflicts'] = conflicts |
65 |
65 |
#print(time_blocks) |
66 |
66 |
return render(request, template, context) |
67 |
67 |
# TODO Finish! |
68 |
68 |
|
69 |
69 |
def roster_ics(request, user_slug): |
70 |
70 |
template = "administration/roster.ics" |
71 |
71 |
context = dict() |
72 |
72 |
context['events'] = CourseEvent.objects.all() # FIXME: Filter to personal calendar items! |
73 |
73 |
return render(request, template, context) |
74 |
74 |
|
75 |
75 |
def index(request): |
76 |
76 |
template = "administration/index.djhtml" |
77 |
77 |
#context = {'money': update_balance(None)} |
78 |
78 |
return render(request, template, context) |
79 |
79 |
|
80 |
80 |
pass |
81 |
81 |
|
82 |
82 |
def pre_registration(request): |
83 |
83 |
user_data_form = UserDataForm() |
84 |
84 |
template = "administration/pre_registration.djhtml" |
85 |
85 |
context = dict() |
86 |
86 |
|
87 |
87 |
if request.method == 'POST': |
88 |
88 |
user_data_form = UserDataForm(request.POST) |
89 |
89 |
context['user_data_form'] = user_data_form |
90 |
90 |
if user_data_form.is_valid(): |
91 |
91 |
user_data_form.save() |
92 |
92 |
context['messsage'] = _("Your registration has been completed. You will receive an e-mail shortly.") |
93 |
93 |
else: |
94 |
94 |
context['messsage'] = _("The data you supplied had errors. Please review your submission.") |
95 |
95 |
else: |
96 |
96 |
context['user_data_form'] = UserDataForm(instance = user_data_form) |
97 |
97 |
|
98 |
98 |
return render(request, template, context) |
99 |
99 |
pass |
100 |
100 |
|
101 |
101 |
@login_required |
102 |
102 |
def settings(request): |
103 |
103 |
user_data = UserData.objects.get(user=request.user) |
104 |
104 |
user_data_form = UserDataForm(instance = user_data) |
105 |
105 |
template = "administration/settings.djhtml" |
106 |
106 |
#context = {'money' : update_balance(None)} |
+ |
107 |
#context = {'money' : update_balance(None)} |
107 |
108 |
|
108 |
109 |
if request.method == 'POST': |
109 |
110 |
user_data_form = UserDataForm(request.POST, instance = user_data) |
110 |
111 |
context['user_data_form'] = user_data_form |
111 |
112 |
if user_data_form.is_valid(): |
112 |
113 |
user_data_form.save() |
113 |
114 |
context['messsage'] = _("Your settings were successfully updated.") |
114 |
115 |
else: |
115 |
116 |
context['messsage'] = _("The data you supplied had errors. Please review your submission.") |
116 |
117 |
else: |
117 |
118 |
context['user_data_form'] = UserDataForm(instance = user_data) |
118 |
119 |
|
119 |
120 |
return render(request, template, context) |
120 |
121 |
|
121 |
122 |
@login_required |
122 |
123 |
def bulletin_board(request): |
123 |
124 |
context = dict() |
124 |
125 |
#context = {'money' : update_balance(None)} |
125 |
126 |
context['exam_commission_decisions'] = ExamCommissionDecision.objects.filter(user=request.user) |
126 |
127 |
context['education_department_messages'] = EducationDepartmentMessages.objects.all() |
127 |
128 |
for item in context['education_department_messages']: |
128 |
129 |
print(item.text) |
129 |
130 |
template = "administration/bulletin_board.djhtml" |
130 |
131 |
return render(request, template, context) |
131 |
132 |
|
132 |
133 |
def jobs(request): |
133 |
134 |
context = dict() |
134 |
135 |
#context = {'money' : update_balance(None)} |
135 |
136 |
template = "administration/jobs.djhtml" |
136 |
137 |
#@context['decisions'] = ExamCommissionDecision.objects.filter(user=request.user) |
137 |
138 |
return render(request, template, context) |
138 |
139 |
|
139 |
140 |
|
140 |
141 |
@login_required |
141 |
142 |
def curriculum(request): |
142 |
143 |
context = dict() |
143 |
144 |
#context = {'money' : update_balance(None)} |
144 |
145 |
template = "administration/curriculum.djhtml" |
145 |
146 |
context['curricula'] = Curriculum.objects.filter(student=request.user) |
146 |
147 |
context['cource_results'] = CourseResult.objects.filter(student=request.user) |
147 |
- | return render(request, template, context) |
+ |
148 |
for co in item.course_programmes_results(): |
+ |
149 |
print(co) |
+ |
150 |
return render(request, template, context) |
148 |
151 |
|
149 |
152 |
def result(request): |
150 |
153 |
return render(request, template, context) |
151 |
154 |
|
152 |
155 |
@login_required |
153 |
156 |
def results(request): |
154 |
157 |
results = CourseResult.objects.filter(student=request.user) |
155 |
158 |
template = "administration/results.djhtml" |
156 |
159 |
# TODO |
157 |
160 |
return render(request, template, context) |
158 |
161 |
|
159 |
162 |
def forms(request): |
160 |
163 |
context = dict() |
161 |
164 |
#context = {'money' : update_balance(None)} |
162 |
165 |
template = "administration/forms.djhtml" |
163 |
166 |
return render(request, template, context) |
164 |
167 |
|
165 |
168 |
def user(request, slug_name): |
166 |
169 |
pass |
167 |
170 |
|
168 |
171 |
def rooms(request): |
169 |
172 |
context = dict() |
170 |
173 |
#context = {'money' : update_balance(None)} |
171 |
174 |
context['rooms'] = Room.objects.all() |
172 |
175 |
context['room_reservations'] = RoomReservation.objects.all() |
173 |
176 |
context['course_events'] = CourseEvent.objects.all() |
174 |
177 |
context['blocks'] = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] |
175 |
178 |
|
176 |
179 |
|
177 |
- | |
+ |
180 |
now = datetime.datetime.now(datetime.timezone.utc) |
+ |
181 |
end = now + datetime.timedelta(hours=2) |
+ |
182 |
free_rooms = dict() |
+ |
183 |
for room in context['rooms']: |
+ |
184 |
if room.reservation_possible(now, end): |
+ |
185 |
event = room.next_event(end) |
+ |
186 |
reservation = room.next_reservation(end) |
+ |
187 |
if event is None and reservation is None: |
+ |
188 |
free_rooms[room] = None |
+ |
189 |
elif reservation is not None: |
+ |
190 |
free_rooms[room] = event.begin_time |
+ |
191 |
elif event is not None: |
+ |
192 |
free_rooms[room] = reservation.begin_time |
+ |
193 |
elif event.begin_time < reservation.begin_time: |
+ |
194 |
free_rooms[room] = event.begin_time |
+ |
195 |
else: |
+ |
196 |
free_rooms[room] = reservation.begin_time |
+ |
197 |
context['free_rooms'] = free_rooms |
+ |
198 |
|
178 |
199 |
template = "administration/rooms.djhtml" |
179 |
200 |
return render(request, template, context) |
180 |
201 |
|
181 |
202 |
def room_detail(request, room): |
182 |
203 |
template = "administration/room_detail.djhtml" |
183 |
204 |
context = dict() |
184 |
205 |
#context = {'money' : update_balance(None)} |
185 |
206 |
room = Room.objects.get(name=room) |
186 |
207 |
context['room'] = room |
187 |
208 |
context['reservations'] = RoomReservation.objects.filter(room=room).filter(begin_time__gte=datetime.datetime.now()) |
188 |
209 |
context['course_events'] = CourseEvent.objects.filter(room=room).filter(begin_time__gte=datetime.datetime.now()) |
189 |
210 |
# Building the room occupancy of today: |
190 |
211 |
today = datetime.date.today() |
191 |
212 |
if today.isoweekday() in {6,7}: # Weekend |
192 |
213 |
today = today + datetime.timedelta(days=8-today.isoweekday()) |
193 |
214 |
|
194 |
215 |
context['days'] = [today] |
195 |
216 |
|
196 |
217 |
# Collecting events |
197 |
218 |
course_events = CourseEvent.objects.filter(room=room).filter(begin_time__date=today) |
198 |
219 |
print(course_events) |
199 |
220 |
#university_events = UniversityEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end) |
200 |
221 |
#study_events = StudyEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end) |
201 |
222 |
#events = Event.objects.filter(begin_time__gte=begin).filter(end_time__lte=end) |
202 |
223 |
|
203 |
224 |
conflicts, table_code = create_roster_rows(course_events) |
204 |
225 |
context['time_blocks'] = table_code |
205 |
226 |
context['conflicts'] = conflicts |
206 |
227 |
print(context['time_blocks']) |
207 |
228 |
return render(request, template, context) |
208 |
229 |
|
209 |
230 |
def login(request): |
210 |
231 |
context = dict() |
211 |
232 |
#context = {'money' : update_balance(None)} |
212 |
233 |
if request.method == "POST": |
213 |
234 |
name = request.POST['name'] |
214 |
235 |
passphrase = request.POST['pass'] |
215 |
236 |
user = authenticate(username=name, password=passphrase) |
216 |
237 |
if user is not None: # The user was successfully authenticated |
217 |
238 |
print("YA") |
218 |
239 |
return HttpResponseRedirect(request.POST['next']) |
219 |
240 |
else: # User credentials were wrong |
220 |
241 |
context['next'] = request.POST['next'] |
221 |
242 |
context['message'] = _("The given credentials were not correct.") |
222 |
243 |
else: |
223 |
244 |
context['next'] = request.GET.get('next', None) |
224 |
245 |
if context['next'] is None: |
225 |
246 |
context['next'] = reverse('administration-index') |
226 |
247 |
|
227 |
248 |
template = 'administration/login.djhtml' |
228 |
249 |
|
229 |
250 |
return render(request, template, context) |
230 |
251 |
courses/models.py ¶
1 addition and 1 deletion.
View changes Hide changes
1 |
1 |
from joeni import constants |
2 |
2 |
from django.utils.translation import ugettext_lazy as _ |
3 |
3 |
|
4 |
4 |
def validate_hex_color(value): |
5 |
5 |
pass # TODO |
6 |
6 |
|
7 |
7 |
class Course(models.Model): |
8 |
8 |
""" Represents a course that is taught at the university. """ |
9 |
9 |
number = models.PositiveSmallIntegerField( |
10 |
10 |
primary_key=True, |
11 |
11 |
blank=False, |
12 |
12 |
help_text=_("The number associated with this course. A leading '0' will be added if the number is smaller than 1000."), |
13 |
13 |
) |
14 |
14 |
name = models.CharField( |
15 |
15 |
max_length=64, |
16 |
16 |
blank=False, |
17 |
17 |
help_text=_("The name of this course, in the language that it is taught. Translations are for the appropriate template."), |
18 |
18 |
) |
19 |
19 |
color = models.CharField( |
20 |
20 |
max_length=6, |
21 |
21 |
blank=False, |
22 |
22 |
default=constants.COLORS['UHasselt default'], |
23 |
23 |
help_text=_("The color for this course. Must be an hexadecimal code. " |
24 |
24 |
"Some standard colors if you don't know what to pick: " |
25 |
25 |
"<ul><li>0076BE: Faculty of Sciences / Blue</li>" |
26 |
26 |
"<li>C0D633: Faculty of Transportation Sciences / Green</li>" |
27 |
27 |
"<li>F4802D: Faculty of Architecture and Arts / Orange</li>" |
28 |
28 |
"<li>00ACEE: Faculty of Business Economics / Turquoise</li>" |
29 |
29 |
"<li>9C3591: Faculty of Medicine and Life Sciences / Purple</li>" |
30 |
30 |
"<li>5BC4BA: Faculty of Engineering Technology / Light blue</li>" |
31 |
31 |
"<li>E41F3A: Faculty of Law / Red</li></ul>"), |
32 |
32 |
#validators=['validate_hex_color'], # TODO |
33 |
33 |
) |
34 |
34 |
slug_name = models.SlugField( |
35 |
35 |
blank=False, |
36 |
36 |
allow_unicode=True, |
37 |
37 |
unique=True, |
38 |
38 |
help_text=_("A so-called 'slug name' for this course."), |
39 |
39 |
) |
40 |
40 |
# TODO: Add a potential thingy magicky to auto fill the slug name on the course name |
41 |
41 |
contact_person = models.ForeignKey( |
42 |
42 |
"administration.User", |
43 |
43 |
on_delete=models.PROTECT, # A course must have a contact person |
44 |
44 |
limit_choices_to={'is_staff': True}, |
45 |
45 |
null=False, |
46 |
46 |
help_text=_("The person to contact regarding this course."), |
47 |
47 |
related_name="contact_person", |
48 |
48 |
) |
49 |
49 |
coordinator = models.ForeignKey( |
50 |
50 |
"administration.User", |
51 |
51 |
on_delete=models.PROTECT, # A course must have a coordinator |
52 |
52 |
limit_choices_to={'is_staff': True}, |
53 |
53 |
null=False, |
54 |
54 |
help_text=_("The person whom's the coordinator of this course."), |
55 |
55 |
related_name="coordinator", |
56 |
56 |
) |
57 |
57 |
co_owners = models.ManyToManyField( |
58 |
58 |
"administration.User", |
59 |
59 |
limit_choices_to={'is_staff': True}, |
60 |
60 |
blank=True, # Allows empty in form validation, and M->M implies null=True |
61 |
61 |
help_text=_("If applicable: The co-owners of this course."), |
62 |
62 |
related_name="co_owners", |
63 |
63 |
) |
64 |
64 |
|
65 |
65 |
educating_team = models.ManyToManyField( |
66 |
66 |
"administration.User", |
67 |
67 |
# No on_delete, since M->M cannot be required at database level |
68 |
68 |
limit_choices_to={'is_staff': True}, |
69 |
69 |
blank=True, |
70 |
70 |
help_text=_("The remaining team members of this course."), |
71 |
71 |
related_name="educating_team", |
72 |
72 |
) |
73 |
73 |
language = models.CharField( |
74 |
74 |
max_length=64, |
75 |
75 |
choices = ( |
76 |
76 |
('NL', _("Dutch")), |
77 |
77 |
('EN', _("English")), |
78 |
78 |
('FR', _("French")), |
79 |
79 |
), |
80 |
80 |
null=False, |
81 |
81 |
help_text=_("The language in which this course is given."), |
82 |
82 |
) |
83 |
83 |
|
84 |
84 |
def course_team(self): |
85 |
85 |
""" Returns a set of all Users that are part of the team of this course. """ |
86 |
86 |
return set( |
87 |
87 |
self.contact_person, |
88 |
88 |
self.coordinator, |
89 |
89 |
self.educating_team, |
90 |
90 |
) |
91 |
91 |
|
92 |
92 |
def __str__(self): |
93 |
93 |
number = str(self.number) |
94 |
94 |
for i in [10,100,1000]: |
95 |
95 |
if self.number < i: |
96 |
96 |
number = "0" + number |
97 |
97 |
return "(" + number + ") " + self.name |
98 |
98 |
|
99 |
99 |
|
100 |
100 |
class Prerequisite(models.Model): |
101 |
101 |
""" Represents a collection of prerequisites a student must have obtained |
102 |
102 |
before being allowed to partake in this course. |
103 |
103 |
It's possible that, if a student has obtained credits in a certain set of |
104 |
104 |
courses, a certain part of the prerequisites do not have to be obtained. |
105 |
105 |
Because of this, make a different record for each different set. In other |
106 |
106 |
words: If one set of prerequisites is obtained, and another one isn't, BUT |
107 |
107 |
they point to the same course, the student is allowed to partake. """ |
108 |
108 |
course = models.ForeignKey( |
109 |
109 |
"Course", |
110 |
110 |
on_delete=models.CASCADE, |
111 |
111 |
null=False, |
112 |
112 |
help_text=_("The course that these prerequisites are for."), |
113 |
113 |
related_name="prerequisite_course", |
114 |
114 |
) |
115 |
115 |
name = models.CharField( |
116 |
116 |
max_length=64, |
117 |
117 |
blank=True, |
118 |
118 |
help_text=_("To specify a name for this set, if necessary."), |
119 |
119 |
) |
120 |
120 |
sequentialities = models.ManyToManyField( |
121 |
121 |
"Course", |
122 |
122 |
help_text=_("All courses for which a credit must've been received in order to follow the course."), |
123 |
123 |
blank=True, |
124 |
124 |
related_name="sequentialities", |
125 |
125 |
) |
126 |
126 |
in_curriculum = models.ManyToManyField( |
127 |
127 |
"Course", |
128 |
128 |
help_text=_("All courses that have to be in the curriculum to follow this. If a credit was achieved, that course can be omitted."), |
129 |
129 |
blank=True, |
130 |
130 |
related_name="in_curriculum", |
131 |
131 |
) |
132 |
132 |
required_study = models.ForeignKey( |
133 |
133 |
"Study", |
134 |
134 |
on_delete=models.CASCADE, |
135 |
135 |
blank=True, |
136 |
136 |
null=True, |
137 |
137 |
help_text=_("If one must have a certain amount of obtained ECTS points for a particular course, state that course here."), |
138 |
138 |
) |
139 |
139 |
ECTS_for_required_study = models.PositiveSmallIntegerField( |
140 |
140 |
blank=True, |
141 |
141 |
null=True, |
142 |
142 |
help_text=_("The amount of obtained ECTS points for the required course, if any."), |
143 |
143 |
) |
144 |
144 |
|
145 |
145 |
def __str__(self): |
146 |
146 |
if self.name == "": |
147 |
147 |
return _("Prerequisites for %(course)s") % {'course': str(self.course)} |
148 |
148 |
else: |
149 |
149 |
return self.name + " | " + str(self.course) |
150 |
150 |
|
151 |
151 |
|
152 |
152 |
class CourseProgramme(models.Model): |
153 |
153 |
""" It's possible that a course is taught in multiple degree programmes; For |
154 |
154 |
example: Calculus can easily be taught to physics and mathematics students |
155 |
155 |
alike. In this table, these relations are set up, and the related properties |
156 |
156 |
are defined as well. """ |
157 |
157 |
study = models.ForeignKey( |
158 |
158 |
"Study", |
159 |
159 |
on_delete=models.CASCADE, |
160 |
160 |
null=False, |
161 |
161 |
help_text=_("The study in which the course is taught."), |
162 |
162 |
) |
163 |
163 |
course = models.ForeignKey( |
164 |
164 |
"Course", |
165 |
165 |
on_delete=models.CASCADE, |
166 |
166 |
null=False, |
167 |
167 |
help_text=_("The course that this programme is for."), |
168 |
168 |
) |
169 |
169 |
study_programme = models.ForeignKey( |
170 |
170 |
"StudyProgramme", |
171 |
171 |
on_delete=models.CASCADE, |
172 |
172 |
null=False, |
173 |
173 |
help_text=_("The study programme that this course belongs to."), |
174 |
174 |
) |
175 |
175 |
programme_type = models.CharField( |
176 |
176 |
max_length=1, |
177 |
177 |
blank=False, |
178 |
178 |
choices = ( |
179 |
179 |
('C', _("Compulsory")), |
180 |
180 |
('O', _("Optional")), |
181 |
181 |
), |
182 |
182 |
help_text=_("Type of this course for this study."), |
183 |
183 |
) |
184 |
184 |
study_hours = models.PositiveSmallIntegerField( |
185 |
185 |
blank=False, |
186 |
186 |
help_text=_("The required amount of hours to study this course."), |
187 |
187 |
) |
188 |
188 |
ECTS = models.PositiveSmallIntegerField( |
189 |
189 |
blank=False, |
190 |
190 |
help_text=_("The amount of ECTS points attached to this course."), |
191 |
191 |
) |
192 |
192 |
semester = models.PositiveSmallIntegerField( |
193 |
193 |
blank=False, |
194 |
194 |
choices = ( |
195 |
195 |
(1, _("First semester")), |
196 |
196 |
(2, _("Second semester")), |
197 |
197 |
(3, _("Full year course")), |
198 |
198 |
(4, _("Taught in first quarter")), |
199 |
199 |
(5, _("Taught in second quarter")), |
200 |
200 |
(6, _("Taught in third quarter")), |
201 |
201 |
(7, _("Taught in fourth quarter")), |
202 |
202 |
), |
203 |
203 |
help_text=_("The period in which this course is being taught in this study."), |
204 |
204 |
) |
205 |
205 |
year = models.PositiveSmallIntegerField( |
206 |
206 |
blank=False, |
207 |
207 |
help_text=_("The year in which this course is taught for this study."), |
208 |
208 |
) |
209 |
209 |
second_chance = models.BooleanField( |
210 |
210 |
default=True, |
211 |
211 |
help_text=_("Defines if a second chance exam is planned for this course."), |
212 |
212 |
) |
213 |
213 |
tolerable = models.BooleanField( |
214 |
214 |
default=True, |
215 |
215 |
help_text=_("Defines if a failed result can be tolerated."), |
216 |
216 |
) |
217 |
217 |
scoring = models.CharField( |
218 |
218 |
max_length=2, |
219 |
219 |
choices = ( |
220 |
220 |
('N', _("Numerical")), |
221 |
221 |
('FP', _("Fail/Pass")), |
222 |
222 |
), |
223 |
223 |
default='N', |
224 |
224 |
blank=False, |
225 |
225 |
help_text=_("How the obtained score for this course is given."), |
226 |
226 |
) |
227 |
227 |
|
228 |
228 |
def __str__(self): |
229 |
229 |
return str(self.study) + " - " + str(self.course) |
230 |
230 |
|
231 |
231 |
class Study(models.Model): |
232 |
232 |
""" Defines a certain study that can be followed at the university. |
233 |
233 |
This also includes abridged study programmes, like transition programmes. |
234 |
234 |
Other information, such as descriptions, are kept in the template file |
235 |
235 |
of this study, which can be manually edited. Joeni searches for a file |
236 |
236 |
with the exact name as the study + ".html". So if the study is called |
237 |
237 |
"Bachelor of Informatics", it will search for "Bachelor of Informatics.html". |
238 |
238 |
""" |
239 |
239 |
# Degree types |
240 |
240 |
BSc = _("Bachelor of Science") |
241 |
241 |
MSc = _("Master of Science") |
242 |
242 |
LLB = _("Bachelor of Laws") |
243 |
243 |
LLM = _("Master of Laws") |
244 |
244 |
BA = _("Bachelor of Arts") |
245 |
245 |
MA = _("Master of Arts") |
246 |
246 |
ir = _("Engineer") |
247 |
247 |
ing = _("Technological Engineer") |
248 |
- | # Faculties |
+ |
248 |
# Faculties |
249 |
249 |
FoMaLS = _("Faculty of Medicine and Life Sciences") |
250 |
250 |
FoS = _("Faculty of Sciences") |
251 |
251 |
FoTS = _("Faculty of Transportation Sciences") |
252 |
252 |
FoAaA = _("Faculty of Architecture and Arts") |
253 |
253 |
FoBE = _("Faculty of Business Economics") |
254 |
254 |
FoET = _("Faculty of Engineering Technology") |
255 |
255 |
FoL = _("Faculty of Law") |
256 |
256 |
|
257 |
257 |
name = models.CharField( |
258 |
258 |
max_length=128, |
259 |
259 |
blank=False, |
260 |
260 |
unique=True, |
261 |
261 |
help_text=_("The full name of this study, in the language it's taught in."), |
262 |
262 |
) |
263 |
263 |
degree_type = models.CharField( |
264 |
264 |
max_length=64, |
265 |
265 |
choices = ( |
266 |
266 |
('BSc', BSc), |
267 |
267 |
('MSc', MSc), |
268 |
268 |
('LL.B', LLB), |
269 |
269 |
('LL.M', LLM), |
270 |
270 |
('ir.', ir ), |
271 |
271 |
('ing.',ing), |
272 |
272 |
('BA', BA), |
273 |
273 |
('MA', MA), |
274 |
274 |
), |
275 |
275 |
blank=False, |
276 |
276 |
help_text=_("The type of degree one obtains upon passing this study."), |
277 |
277 |
) |
278 |
278 |
language = models.CharField( |
279 |
279 |
max_length=64, |
280 |
280 |
choices = ( |
281 |
281 |
('NL', _("Dutch")), |
282 |
282 |
('EN', _("English")), |
283 |
283 |
('FR', _("French")), |
284 |
284 |
), |
285 |
285 |
null=False, |
286 |
286 |
help_text=_("The language in which this study is given."), |
287 |
287 |
) |
288 |
288 |
# Information about exam committee |
289 |
289 |
chairman = models.ForeignKey( |
290 |
290 |
"administration.User", |
291 |
291 |
on_delete=models.PROTECT, |
292 |
292 |
null=False, |
293 |
293 |
limit_choices_to={'is_staff': True}, |
294 |
294 |
help_text=_("The chairman of this study."), |
295 |
295 |
related_name="chairman", |
296 |
296 |
) |
297 |
297 |
vice_chairman = models.ForeignKey( |
298 |
298 |
"administration.User", |
299 |
299 |
on_delete=models.PROTECT, |
300 |
300 |
null=False, |
301 |
301 |
help_text=_("The vice-chairman of this study."), |
302 |
302 |
limit_choices_to={'is_staff': True}, |
303 |
303 |
related_name="vice_chairman", |
304 |
304 |
) |
305 |
305 |
secretary = models.ForeignKey( |
306 |
306 |
"administration.User", |
307 |
307 |
on_delete=models.PROTECT, |
308 |
308 |
null=False, |
309 |
309 |
help_text=_("The secretary of this study."), |
310 |
310 |
limit_choices_to={'is_staff': True}, |
311 |
311 |
related_name="secretary", |
312 |
312 |
) |
313 |
313 |
ombuds = models.ForeignKey( |
314 |
314 |
"administration.User", |
315 |
315 |
on_delete=models.PROTECT, |
316 |
316 |
null=False, |
317 |
317 |
help_text=_("The ombuds person of this study."), |
318 |
318 |
limit_choices_to={'is_staff': True}, |
319 |
319 |
related_name="ombuds", |
320 |
320 |
) |
321 |
321 |
vice_ombuds = models.ForeignKey( |
322 |
322 |
"administration.User", |
323 |
323 |
on_delete=models.PROTECT, |
324 |
324 |
null=False, |
325 |
325 |
help_text=_("The (replacing) ombuds person of this study."), |
326 |
326 |
limit_choices_to={'is_staff': True}, |
327 |
327 |
related_name="vice_ombuds", |
328 |
328 |
) |
329 |
329 |
additional_members = models.ManyToManyField( |
330 |
330 |
"administration.User", |
331 |
331 |
help_text=_("All the other members of the exam committee."), |
332 |
332 |
limit_choices_to={'is_staff': True}, |
333 |
333 |
related_name="additional_members", |
334 |
334 |
) |
335 |
335 |
faculty = models.CharField( |
336 |
336 |
max_length=6, |
337 |
337 |
choices = ( |
338 |
338 |
('FoS', FoS), |
339 |
339 |
('FoTS', FoTS), |
340 |
340 |
('FoAaA', FoAaA), |
341 |
341 |
('FoBE', FoBE), |
342 |
342 |
('FoMaLS', FoMaLS), |
343 |
343 |
('FoET', FoET), |
344 |
344 |
('FoL', FoL), |
345 |
345 |
), |
346 |
346 |
blank=False, |
347 |
347 |
help_text=_("The faculty where this study belongs to."), |
348 |
348 |
) |
349 |
349 |
|
350 |
350 |
#def study_points(self): |
351 |
351 |
""" Returns the amount of study points for this year. |
352 |
352 |
This value is inferred based on the study programme information |
353 |
353 |
records that lists this study as their foreign key. """ |
354 |
354 |
#total_ECTS = 0 |
355 |
355 |
#for course in CourseProgramme.objects.filter(study=self): |
356 |
356 |
#total_ECTS += course.ECTS |
357 |
357 |
#return total_ECTS |
358 |
358 |
# XXX: Commented because this is actually something for the StudyProgramme |
359 |
359 |
def years(self): |
360 |
360 |
""" Returns the amount of years this study takes. |
361 |
361 |
This value is inferred based on the study programme information |
362 |
362 |
records that lists this study as their foreign key. """ |
363 |
363 |
highest_year = 0 |
364 |
364 |
for course in CourseProgramme.objects.filter(study=self): |
365 |
365 |
highest_year = max(highest_year, course.year) |
366 |
366 |
return highest_year |
367 |
367 |
|
368 |
368 |
def students(self): |
369 |
369 |
""" Cross references the information stored in the database, and |
370 |
370 |
returns all the students that are following this study in this |
371 |
371 |
academic year. """ |
372 |
372 |
return 0 # TODO |
373 |
373 |
|
374 |
374 |
|
375 |
375 |
def __str__(self): |
376 |
376 |
return self.name |
377 |
377 |
|
378 |
378 |
class StudyProgramme(models.Model): |
379 |
379 |
""" Represents a programme within a certain study. |
380 |
380 |
A good example for this is the different specializations, minors, majors, ... |
381 |
381 |
one can follow within the same study. Nevertheless, they're all made of |
382 |
382 |
a certain set of courses. This table collects all these, and allows one to name |
383 |
383 |
them, so they're distinct from one another. """ |
384 |
384 |
name = models.CharField( |
385 |
385 |
max_length=64, |
386 |
386 |
blank=False, |
387 |
387 |
help_text=_("The name of this programme."), |
388 |
388 |
) |
389 |
389 |
|
390 |
390 |
def courses(self): |
391 |
391 |
""" All courses that are part of this study programme. """ |
392 |
392 |
programmes = CourseProgramme.objects.filter(study_programme=self) |
393 |
393 |
courses = {} |
394 |
394 |
for program in programmes: |
395 |
395 |
courses.add(program.course) |
396 |
396 |
return courses |
397 |
397 |
|
398 |
398 |
def study_points(self, year=None): |
399 |
399 |
""" Returns the amount of study points this programme contains. |
400 |
400 |
Accepts year as an optional argument. If not given, the study points |
401 |
401 |
of all years are returned. """ |
402 |
402 |
programmes = CourseProgramme.objects.filter(study_programme=self) |
403 |
403 |
ECTS = 0 |
404 |
404 |
for program in programmes: |
405 |
405 |
if year is None or program.year == year: |
406 |
406 |
# XXX: This only works if the used implementation does lazy |
407 |
407 |
# evaluation, otherwise this is a type error! |
408 |
408 |
ECTS += program.ECTS |
409 |
409 |
return ECTS |
410 |
410 |
|
411 |
411 |
def __str__(self): |
412 |
412 |
return self.name |
413 |
413 |
|
414 |
414 |
# Tables about things related to the courses: |
415 |
415 |
|
416 |
416 |
class Assignment(models.Model): |
417 |
417 |
""" For courses, it's possible to set up tasks. These tasks are recorded |
418 |
418 |
here. """ |
419 |
419 |
# TODO: Require that only the course team can create assignments for a team. |
420 |
420 |
course = models.ForeignKey( |
421 |
421 |
"Course", |
422 |
422 |
on_delete=models.CASCADE, |
423 |
423 |
null=False, |
424 |
424 |
#editable=False, |
425 |
425 |
db_index=True, |
426 |
426 |
help_text=_("The course for which this task is assigned."), |
427 |
427 |
) |
428 |
428 |
title = models.CharField( |
429 |
429 |
max_length=32, |
430 |
430 |
blank=False, |
431 |
431 |
help_text=_("The title of this assignment."), |
432 |
432 |
) |
433 |
433 |
information = models.TextField( |
434 |
434 |
help_text=_("Any additional information regarding the assignment. Orgmode syntax available."), |
435 |
435 |
) |
436 |
436 |
deadline = models.DateTimeField( |
437 |
437 |
null=False, |
438 |
438 |
help_text=_("The date and time this task is due."), |
439 |
439 |
) |
440 |
440 |
posted = models.DateField(auto_now_add=True) |
441 |
441 |
digital_task = models.BooleanField( |
442 |
442 |
default=True, |
443 |
443 |
help_text=_("This determines whether this assignment requires handing " |
444 |
444 |
"in a digital file."), |
445 |
445 |
) |
446 |
446 |
|
447 |
447 |
def __str__(self): |
448 |
448 |
return str(self.course) +" | "+ str(self.posted) |
449 |
449 |
|
450 |
450 |
class Announcement(models.Model): |
451 |
451 |
""" Courses sometimes have to make announcements for the students. """ |
452 |
452 |
course = models.ForeignKey( |
453 |
453 |
"Course", |
454 |
454 |
on_delete=models.CASCADE, |
455 |
455 |
null=False, |
456 |
456 |
#editable=False, |
457 |
457 |
db_index=True, |
458 |
458 |
help_text=_("The course for which this announcement is made."), |
459 |
459 |
) |
460 |
460 |
title = models.CharField( |
461 |
461 |
max_length=20, # Keep It Short & Simple® |
462 |
462 |
help_text=_("A quick title for what this is about."), |
463 |
463 |
) |
464 |
464 |
text = models.TextField( |
465 |
465 |
blank=False, |
466 |
466 |
help_text=_("The announcement itself. Orgmode syntax available."), |
467 |
467 |
) |
468 |
468 |
posted = models.DateTimeField(auto_now_add=True) |
469 |
469 |
|
470 |
470 |
def __str__(self): |
471 |
471 |
return str(self.course) +" | "+ self.posted.strftime("%m/%d") |
472 |
472 |
|
473 |
473 |
class Upload(models.Model): |
474 |
474 |
""" For certain assignments, digital hand-ins may be required. These hand |
475 |
475 |
ins are recorded per student in this table. """ |
476 |
476 |
course = models.ForeignKey( |
477 |
477 |
"Course", |
478 |
478 |
on_delete=models.CASCADE, |
479 |
479 |
null=False, |
480 |
480 |
db_index=True, |
481 |
481 |
) |
482 |
482 |
assignment = models.ForeignKey( |
483 |
483 |
"Assignment", |
484 |
484 |
on_delete=models.CASCADE, |
485 |
485 |
null=False, |
486 |
486 |
#editable=False, |
487 |
487 |
db_index=True, |
488 |
488 |
limit_choices_to={"digital_task": True}, |
489 |
489 |
help_text=_("For which assignment this upload is."), |
490 |
490 |
) |
491 |
491 |
# TODO: Try to find a way to require that, if the upload is made, |
492 |
492 |
# only students that have this course in their curriculum can upload. |
493 |
493 |
student = models.ForeignKey( |
494 |
494 |
"administration.User", |
495 |
495 |
on_delete=models.CASCADE, |
496 |
496 |
null=False, |
497 |
497 |
#editable=False, |
498 |
498 |
limit_choices_to={"is_student": True}, |
499 |
499 |
help_text=_("The student who handed this in."), |
500 |
500 |
) |
501 |
501 |
upload_time = models.DateTimeField(auto_now_add=True) |
502 |
502 |
comment = models.TextField( |
503 |
503 |
blank=True, |
504 |
504 |
help_text=_("If you wish to add an additional comment, state it here."), |
505 |
505 |
) |
506 |
506 |
file = models.FileField( |
507 |
507 |
upload_to="assignments/uploads/%Y/%m/", |
508 |
508 |
null=False, |
509 |
509 |
#editable=False, |
510 |
510 |
help_text=_("The file you want to upload for this assignment."), |
511 |
511 |
) |
512 |
512 |
|
513 |
513 |
|
514 |
514 |
def __str__(self): |
515 |
515 |
deadline = self.assignment.deadline |
516 |
516 |
if deadline < self.upload_time: |
517 |
517 |
return str(self.assignment.course) +" | "+ str(self.student.number) + _("(OVERDUE)") |
518 |
518 |
else: |
519 |
519 |
return str(self.assignment.course) +" | "+ str(self.student.number) |
520 |
520 |
|
521 |
521 |
def item_upload_directory(instance, filename): |
522 |
522 |
return "courses/" + instance.course.slug_name + "/" |
523 |
523 |
class CourseItem(models.Model): |
524 |
524 |
""" Reprensents study material for a course that is being shared by the |
525 |
525 |
course's education team. """ |
526 |
526 |
course = models.ForeignKey( |
527 |
527 |
Course, |
528 |
528 |
on_delete=models.CASCADE, |
529 |
529 |
null=False, |
530 |
530 |
#editable=False, |
531 |
531 |
) |
532 |
532 |
file = models.FileField( |
533 |
533 |
upload_to=item_upload_directory, |
534 |
534 |
null=False, |
535 |
535 |
#editable=False, |
536 |
536 |
help_text=_("The file you wish to upload."), |
537 |
537 |
) |
538 |
538 |
timestamp = models.DateTimeField(auto_now_add=True) |
539 |
539 |
note = models.TextField( |
540 |
540 |
blank=True, |
541 |
541 |
help_text=_("If you want to state some additional information about " |
542 |
542 |
"this upload, state it here."), |
543 |
543 |
) |
544 |
544 |
|
545 |
545 |
class StudyGroup(models.Model): |
546 |
546 |
""" It may be necessary to make study groups regarding a course. These |
547 |
547 |
are recorded here, and blend in seamlessly with the Groups from Agora. |
548 |
548 |
Groups that are recorded as a StudyGroup, are given official course status, |
549 |
549 |
and thus, cannot be removed until the status of StudyGroup is lifted. """ |
550 |
550 |
course = models.ForeignKey( |
551 |
551 |
"Course", |
552 |
552 |
on_delete=models.CASCADE, |
553 |
553 |
null=False, |
554 |
554 |
#editable=False, |
555 |
555 |
db_index=True, |
556 |
556 |
help_text=_("The course for which this group is."), |
557 |
557 |
) |
558 |
558 |
group = models.ForeignKey( |
559 |
559 |
"agora.Group", |
560 |
560 |
on_delete=models.PROTECT, # See class documentation |
561 |
561 |
null=False, |
562 |
562 |
#editable=False, # Keep the same group |
563 |
563 |
help_text=_("The group that will be seen as the study group."), |
564 |
564 |
) |
565 |
565 |
|
566 |
566 |
def __str__(self): |
567 |
567 |
return str(self.course) +" | "+ str(self.group) |
568 |
568 |
|
569 |
569 |
class CourseGroup(models.Model): |
570 |
570 |
"""Because of size, some studies may use multiple groups for the different |
571 |
571 |
students, so it's possible to facilitate all of them. These groups must be |
572 |
572 |
registered here.""" |
573 |
573 |
study = models.ForeignKey( |
574 |
574 |
"Study", |
575 |
575 |
on_delete=models.CASCADE, |
576 |
576 |
null=False, |
577 |
577 |
) |
578 |
578 |
# TODO: How to attach students to certain groups? The curriculum or what? |
579 |
579 |
courses/urls.py ¶
1 addition and 0 deletions.
View changes Hide changes
1 |
1 |
from . import views |
2 |
2 |
from django.utils.translation import ugettext_lazy as _ |
3 |
3 |
|
4 |
4 |
urlpatterns = [ |
5 |
5 |
path('', views.index, name='courses-index'), |
6 |
6 |
path('<slug:course_slug>', views.course, name='courses-course-index'), |
7 |
7 |
path(_('<slug:course_slug>/<int:assignment_id>/upload'), views.upload, name='courses-upload'), |
8 |
8 |
path(_('<slug:course_slug>/new-item'), views.new_item, name='courses-new-item'), |
9 |
9 |
path(_('<slug:course_slug>/groups'), views.groups, name='courses-groups'), |
10 |
10 |
] |
+ |
11 |
] |
11 |
12 |
courses/views.py ¶
52 additions and 0 deletions.
View changes Hide changes
1 |
1 |
import datetime |
2 |
2 |
from django.urls import reverse # Why? |
3 |
3 |
from django.utils.translation import gettext as _ |
4 |
4 |
from .models import * |
5 |
5 |
from .forms import * |
6 |
6 |
import administration |
7 |
7 |
from django.contrib.auth.decorators import login_required |
8 |
8 |
|
9 |
9 |
def current_academic_year(): |
10 |
10 |
""" Returns the current academic year. The year is determined as follows: |
11 |
11 |
- If today is before September 15 of the current year, the returned value |
12 |
12 |
is the current year - 1. |
13 |
13 |
- If today is after September 15 of the current year, but before January 1 |
14 |
14 |
of the next year, it returns the current year as is. |
15 |
15 |
""" |
16 |
16 |
today = datetime.datetime.now() |
17 |
17 |
switch = datetime.datetime(datetime.datetime.now().year, 9, 15) |
18 |
18 |
if today < switch: |
19 |
19 |
return today.year - 1 |
20 |
20 |
else: |
21 |
21 |
return today.year |
22 |
22 |
|
23 |
23 |
@login_required |
24 |
24 |
def index(request): |
25 |
25 |
""" Starting page regarding the courses. This serves two specific groups: |
26 |
26 |
- Students: Displays all courses that this student has in his/her curriculum |
27 |
27 |
for this academic year. Requires the curriculum to be accepted. |
28 |
28 |
- Staff: Displays all courses in which the staff member is part of the |
29 |
29 |
educating team, or is otherwise related to the course. |
30 |
30 |
Users who are not logged in will be sent to the login page. |
31 |
31 |
""" |
32 |
32 |
template = "courses/index.djhtml" |
33 |
33 |
courses = set() |
34 |
34 |
if request.user.user_data.is_student: |
35 |
35 |
curricula = administration.models.Curriculum.objects.filter(student=request.user) |
36 |
36 |
#current_curriculum = curricula.filter(year__year=current_academic_year()) |
37 |
37 |
#courses = current_curriculum.courses |
38 |
38 |
courses = curricula.first().courses |
39 |
39 |
elif request.user.user_data.is_staff: |
40 |
40 |
courses += adminstration.models.Course.filter(course_team__contains=request.user) |
41 |
41 |
else: |
42 |
42 |
raise django.exceptions.FieldError("User "+request.user.number+" is neither staff nor student") |
43 |
43 |
|
44 |
44 |
context = { |
45 |
45 |
'courses': courses, |
46 |
46 |
} |
47 |
47 |
|
48 |
48 |
return render(request, template, context) |
49 |
49 |
|
50 |
50 |
@login_required |
51 |
51 |
def course(request, course_slug): |
52 |
52 |
template = "courses/course.djhtml" |
53 |
53 |
course = Course.objects.get(slug_name=course_slug) |
54 |
54 |
|
55 |
55 |
# Check if user can see this page |
56 |
56 |
if request.user.user_data.is_student: |
57 |
57 |
curricula = administration.models.Curriculum.objects.filter(student=request.user) |
58 |
58 |
#current_curriculum = curricula.filter(year__year=current_academic_year()) |
59 |
59 |
current_curriculum = curricula.first() |
60 |
60 |
if course not in current_curriculum.courses(): |
61 |
61 |
""" I'm currently just redirecting to the index page, but maybe it's |
62 |
62 |
just as good to make an announcement that this course cannot be |
63 |
63 |
used by this user. """ |
64 |
64 |
return index(request) |
65 |
65 |
|
66 |
66 |
context = { |
67 |
67 |
'course': course, |
68 |
68 |
'announcements': Announcement.objects.filter(course=course), |
69 |
69 |
'assignments': Assignment.objects.filter(course=course), |
70 |
70 |
'course-items': CourseItem.objects.filter(course=course), |
71 |
71 |
'study-groups': StudyGroup.objects.filter(course=course), |
72 |
72 |
'uploads': Upload.objects.filter(course=course).filter(student=request.user) |
73 |
73 |
} |
74 |
74 |
if request.user.user_data.is_student: |
75 |
75 |
context['upload_form'] = UploadForm() |
76 |
76 |
|
77 |
77 |
return render(request, template, context) |
78 |
78 |
|
79 |
79 |
# TODO: Find a way to see if it's possible to require some permissions and to |
80 |
80 |
# put them in a decorator |
81 |
81 |
#@permission_required |
82 |
82 |
@login_required |
83 |
83 |
def new_item(request, course_slug): |
84 |
84 |
template = "courses/new_item.djhtml" |
85 |
85 |
course = Course.objects.get(slug_name=course_slug) |
86 |
86 |
|
87 |
87 |
if request.user.user_data.is_student or request.user not in course.course_team: |
88 |
88 |
# Students can't add new items. Redirect to index |
89 |
89 |
# Also redirect people who are not part of the course team |
90 |
90 |
redirect('courses-index') |
91 |
91 |
# Now able to assume user is allowed to add items to this course |
92 |
92 |
|
93 |
93 |
context = { |
94 |
94 |
'course': course, |
95 |
95 |
'announcements': Announcement.objects.filter(course=course), |
96 |
96 |
'assignments': Assignment.objects.filter(course=course), |
97 |
97 |
'course-items': CourseItem.objects.filter(course=course), |
98 |
98 |
'study-groups': StudyGroup.objects.filter(course=course), |
99 |
99 |
'uploads': Upload.objects.filter(course=course) |
100 |
100 |
} |
101 |
101 |
|
102 |
102 |
return render(request, template, context) |
103 |
103 |
|
104 |
104 |
@login_required |
+ |
105 |
def edit_course_items(request, course_slug): |
+ |
106 |
# TODO Only allow people on the course team to this page! |
+ |
107 |
template = "courses/edit_course_items.djhtml" |
+ |
108 |
context = dict() |
+ |
109 |
course_ = Course.objects.get(slug_name=course_slug) |
+ |
110 |
if request.method == 'POST': |
+ |
111 |
assignments = AssignmentFormSet(request.POST, prefix='assignments') |
+ |
112 |
announcements = AnnouncementFormSet(request.POST, prefix='announcements') |
+ |
113 |
course_items = CourseItemFormSet(request.POST, request.FILES, prefix='course_items') |
+ |
114 |
if assignments.is_valid() and announcements.is_valid() and course_items.is_valid(): |
+ |
115 |
assignments.save(commit=False) |
+ |
116 |
announcements.save(commit=False) |
+ |
117 |
course_items.save(commit=False) |
+ |
118 |
for new_assignment in assignments.new_objects: |
+ |
119 |
new_assignment.course = course_ |
+ |
120 |
for new_announcement in announcements.new_objects: |
+ |
121 |
new_announcement.course = course_ |
+ |
122 |
for new_course_item in course_items.new_objects: |
+ |
123 |
new_coutse_item.course = course_ |
+ |
124 |
assignments.save() |
+ |
125 |
announcements.save() |
+ |
126 |
course_items.save() |
+ |
127 |
return course(request, course_slug) |
+ |
128 |
else: |
+ |
129 |
assignments = AssignmentFormSet( |
+ |
130 |
queryset=Assignment.objects.filter(course=course_), |
+ |
131 |
prefix="assignments", |
+ |
132 |
) |
+ |
133 |
announcements = AnnouncementFormSet( |
+ |
134 |
queryset=Announcement.objects.filter(course=course_), |
+ |
135 |
prefix="announcements", |
+ |
136 |
) |
+ |
137 |
course_items = CourseItemFormSet( |
+ |
138 |
queryset=CourseItem.objects.filter(course=course_), |
+ |
139 |
prefix="course_items", |
+ |
140 |
) |
+ |
141 |
context['assignments'] = assignments |
+ |
142 |
context['announcements'] = announcements |
+ |
143 |
context['course_items'] = course_items |
+ |
144 |
return render(request, template, context) |
+ |
145 |
|
+ |
146 |
|
+ |
147 |
@login_required |
105 |
148 |
def remove(request, type, id): |
106 |
149 |
pass |
107 |
150 |
|
108 |
151 |
@login_required |
109 |
152 |
def upload(request): |
110 |
153 |
pass |
111 |
154 |
|
112 |
155 |
@login_required |
113 |
156 |
def groups(request): |
114 |
157 |
pass |
115 |
158 |
|
+ |
159 |
def fiche(request, course_slug): |
+ |
160 |
"""Displays the fiche for the given course. Includes information about all |
+ |
161 |
course programs.""" |
+ |
162 |
template = "courses/fiche.djhtml" |
+ |
163 |
context = dict() |
+ |
164 |
course = Course.objects.get(slug_name=course_slug) |
+ |
165 |
|
+ |
166 |
|
+ |
167 |
joeni/templates/joeni/header.djhtml ¶
26 additions and 11 deletions.
View changes Hide changes
1 |
1 |
{% load static %} |
2 |
2 |
{% get_media_prefix as media %} |
3 |
3 |
<img width="200px" src="{% static "logos/uhasselt/simple_white.svg" %}" alt="◀ Joeni /▶" /> |
4 |
4 |
<ul> |
5 |
- | <li> |
6 |
- | <a href="{% url 'agora-index' %}">Agora</a> |
7 |
- | </li> |
8 |
- | <li> |
9 |
- | <a href="{% url 'administration-index' %}">{% trans "Administration" %}</a> |
10 |
- | </li> |
11 |
- | <li> |
12 |
- | <a href="{% url 'courses-index' %}">{% trans "Courses" %}</a> |
13 |
- | </li> |
14 |
- | </ul> |
15 |
- | |
+ |
5 |
<ul> |
+ |
6 |
<li><a href="{% url 'agora-index' %}">Agora</a> |
+ |
7 |
<ul> |
+ |
8 |
</ul> |
+ |
9 |
</li> |
+ |
10 |
|
+ |
11 |
<li><a href="{% url 'administration-index' %}">{% trans "Administration" %}</a> |
+ |
12 |
<ul> |
+ |
13 |
<li><a href="{% url 'administration-settings' %}">{% trans "Personal settings" %}</a></li> |
+ |
14 |
<li><a href="{% url 'administration-curriculum' %}">{% trans "Curricula" %}</a></li> |
+ |
15 |
<li><a href="{% url 'administration-forms' %}">{% trans "Forms" %}</a></li> |
+ |
16 |
<li><a href="{% url 'administration-rooms' %}">{% trans "Rooms" %}</a></li> |
+ |
17 |
<li><a href="{% url 'administration-jobs' %}">{% trans "Jobs" %}</a></li> |
+ |
18 |
<li><a href="{% url 'administration-roster' %}">{% trans "Personal Roster" %}</a></li> |
+ |
19 |
<li><a href="{% url 'administration-bulletin-board' %}">{% trans "Bulletin board" %}</a></li> |
+ |
20 |
</ul> |
+ |
21 |
</li> |
+ |
22 |
|
+ |
23 |
<li><a href="{% url 'courses-index' %}">{% trans "Courses" %}</a> |
+ |
24 |
<ul> |
+ |
25 |
</ul> |
+ |
26 |
</li> |
+ |
27 |
</ul> |
+ |
28 |
</nav> |
+ |
29 |
PingPing: €{{ money }} |
+ |
30 |
static/css/header.css ¶
0 additions and 12 deletions.