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