models.py
1 |
|
2 |
from joeni import constants |
3 |
from django.utils.translation import ugettext_lazy as _ |
4 |
|
5 |
def validate_hex_color(value): |
6 |
pass # TODO |
7 |
|
8 |
class Course(models.Model): |
9 |
""" Represents a course that is taught at the university. """ |
10 |
number = models.PositiveSmallIntegerField( |
11 |
primary_key=True, |
12 |
blank=False, |
13 |
help_text=_("The number associated with this course. A leading '0' will be added if the number is smaller than 1000."), |
14 |
) |
15 |
name = models.CharField( |
16 |
max_length=64, |
17 |
blank=False, |
18 |
help_text=_("The name of this course, in the language that it is taught. Translations are for the appropriate template."), |
19 |
) |
20 |
color = models.CharField( |
21 |
max_length=6, |
22 |
blank=False, |
23 |
default=constants.COLORS['UHasselt default'], |
24 |
help_text=_("The color for this course. Must be an hexadecimal code. " |
25 |
"Some standard colors if you don't know what to pick: " |
26 |
"<ul><li>0076BE: Faculty of Sciences / Blue</li>" |
27 |
"<li>C0D633: Faculty of Transportation Sciences / Green</li>" |
28 |
"<li>F4802D: Faculty of Architecture and Arts / Orange</li>" |
29 |
"<li>00ACEE: Faculty of Business Economics / Turquoise</li>" |
30 |
"<li>9C3591: Faculty of Medicine and Life Sciences / Purple</li>" |
31 |
"<li>5BC4BA: Faculty of Engineering Technology / Light blue</li>" |
32 |
"<li>E41F3A: Faculty of Law / Red</li></ul>"), |
33 |
#validators=['validate_hex_color'], # TODO |
34 |
) |
35 |
slug_name = models.SlugField( |
36 |
blank=False, |
37 |
allow_unicode=True, |
38 |
unique=True, |
39 |
help_text=_("A so-called 'slug name' for this course."), |
40 |
) |
41 |
# TODO: Add a potential thingy magicky to auto fill the slug name on the course name |
42 |
contact_person = models.ForeignKey( |
43 |
"administration.User", |
44 |
on_delete=models.PROTECT, # A course must have a contact person |
45 |
limit_choices_to={'is_staff': True}, |
46 |
null=False, |
47 |
help_text=_("The person to contact regarding this course."), |
48 |
related_name="contact_person", |
49 |
) |
50 |
coordinator = models.ForeignKey( |
51 |
"administration.User", |
52 |
on_delete=models.PROTECT, # A course must have a coordinator |
53 |
limit_choices_to={'is_staff': True}, |
54 |
null=False, |
55 |
help_text=_("The person whom's the coordinator of this course."), |
56 |
related_name="coordinator", |
57 |
) |
58 |
co_owners = models.ManyToManyField( |
59 |
"administration.User", |
60 |
limit_choices_to={'is_staff': True}, |
61 |
blank=True, # Allows empty in form validation, and M->M implies null=True |
62 |
help_text=_("If applicable: The co-owners of this course."), |
63 |
related_name="co_owners", |
64 |
) |
65 |
|
66 |
educating_team = models.ManyToManyField( |
67 |
"administration.User", |
68 |
# No on_delete, since M->M cannot be required at database level |
69 |
limit_choices_to={'is_staff': True}, |
70 |
blank=True, |
71 |
help_text=_("The remaining team members of this course."), |
72 |
related_name="educating_team", |
73 |
) |
74 |
language = models.CharField( |
75 |
max_length=64, |
76 |
choices = ( |
77 |
('NL', _("Dutch")), |
78 |
('EN', _("English")), |
79 |
('FR', _("French")), |
80 |
), |
81 |
null=False, |
82 |
help_text=_("The language in which this course is given."), |
83 |
) |
84 |
|
85 |
def course_team(self): |
86 |
""" Returns a set of all Users that are part of the team of this course. """ |
87 |
return set().union( |
88 |
{self.contact_person, |
89 |
self.coordinator,}, |
90 |
self.educating_team.iterator(), |
91 |
self.co_owners.iterator()) |
92 |
|
93 |
def __str__(self): |
94 |
number = str(self.number) |
95 |
for i in [10,100,1000]: |
96 |
if self.number < i: |
97 |
number = "0" + number |
98 |
return "(" + number + ") " + self.name |
99 |
|
100 |
def students(self): |
101 |
"""Returns a list of all students that are following this course.""" |
102 |
stud_in_course = list() |
103 |
for student in administration.models.User.objects.all(): |
104 |
if self in student.current_courses: |
105 |
stud_in_course.append(student) |
106 |
return stud_in_course |
107 |
|
108 |
|
109 |
|
110 |
class Prerequisite(models.Model): |
111 |
""" Represents a collection of prerequisites a student must have obtained |
112 |
before being allowed to partake in this course. |
113 |
It's possible that, if a student has obtained credits in a certain set of |
114 |
courses, a certain part of the prerequisites do not have to be obtained. |
115 |
Because of this, make a different record for each different set. In other |
116 |
words: If one set of prerequisites is obtained, and another one isn't, BUT |
117 |
they point to the same course, the student is allowed to partake. """ |
118 |
course = models.ForeignKey( |
119 |
"Course", |
120 |
on_delete=models.CASCADE, |
121 |
null=False, |
122 |
help_text=_("The course that these prerequisites are for."), |
123 |
related_name="prerequisite_course", |
124 |
) |
125 |
name = models.CharField( |
126 |
max_length=64, |
127 |
blank=True, |
128 |
help_text=_("To specify a name for this set, if necessary."), |
129 |
) |
130 |
sequentialities = models.ManyToManyField( |
131 |
"Course", |
132 |
help_text=_("All courses for which a credit must've been received in order to follow the course."), |
133 |
blank=True, |
134 |
related_name="sequentialities", |
135 |
) |
136 |
in_curriculum = models.ManyToManyField( |
137 |
"Course", |
138 |
help_text=_("All courses that have to be in the curriculum to follow this. If a credit was achieved, that course can be omitted."), |
139 |
blank=True, |
140 |
related_name="in_curriculum", |
141 |
) |
142 |
required_study = models.ForeignKey( |
143 |
"Study", |
144 |
on_delete=models.CASCADE, |
145 |
blank=True, |
146 |
null=True, |
147 |
help_text=_("If one must have a certain amount of obtained ECTS points for a particular course, state that course here."), |
148 |
) |
149 |
ECTS_for_required_study = models.PositiveSmallIntegerField( |
150 |
blank=True, |
151 |
null=True, |
152 |
help_text=_("The amount of obtained ECTS points for the required course, if any."), |
153 |
) |
154 |
|
155 |
def __str__(self): |
156 |
if self.name == "": |
157 |
return _("Prerequisites for %(course)s") % {'course': str(self.course)} |
158 |
else: |
159 |
return self.name + " | " + str(self.course) |
160 |
|
161 |
|
162 |
class CourseProgramme(models.Model): |
163 |
""" It's possible that a course is taught in multiple degree programmes; For |
164 |
example: Calculus can easily be taught to physics and mathematics students |
165 |
alike. In this table, these relations are set up, and the related properties |
166 |
are defined as well. """ |
167 |
study = models.ForeignKey( |
168 |
"Study", |
169 |
on_delete=models.CASCADE, |
170 |
null=False, |
171 |
help_text=_("The study in which the course is taught."), |
172 |
) |
173 |
course = models.ForeignKey( |
174 |
"Course", |
175 |
on_delete=models.CASCADE, |
176 |
null=False, |
177 |
help_text=_("The course that this programme is for."), |
178 |
) |
179 |
study_programme = models.ForeignKey( |
180 |
"StudyProgramme", |
181 |
on_delete=models.CASCADE, |
182 |
null=False, |
183 |
help_text=_("The study programme that this course belongs to."), |
184 |
) |
185 |
programme_type = models.CharField( |
186 |
max_length=1, |
187 |
blank=False, |
188 |
choices = ( |
189 |
('C', _("Compulsory")), |
190 |
('O', _("Optional")), |
191 |
), |
192 |
help_text=_("Type of this course for this study."), |
193 |
) |
194 |
study_hours = models.PositiveSmallIntegerField( |
195 |
blank=False, |
196 |
help_text=_("The required amount of hours to study this course."), |
197 |
) |
198 |
ECTS = models.PositiveSmallIntegerField( |
199 |
blank=False, |
200 |
help_text=_("The amount of ECTS points attached to this course."), |
201 |
) |
202 |
semester = models.PositiveSmallIntegerField( |
203 |
blank=False, |
204 |
choices = ( |
205 |
(1, _("First semester")), |
206 |
(2, _("Second semester")), |
207 |
(3, _("Full year course")), |
208 |
(4, _("Taught in first quarter")), |
209 |
(5, _("Taught in second quarter")), |
210 |
(6, _("Taught in third quarter")), |
211 |
(7, _("Taught in fourth quarter")), |
212 |
), |
213 |
help_text=_("The period in which this course is being taught in this study."), |
214 |
) |
215 |
year = models.PositiveSmallIntegerField( |
216 |
blank=False, |
217 |
help_text=_("The year in which this course is taught for this study."), |
218 |
) |
219 |
second_chance = models.BooleanField( |
220 |
default=True, |
221 |
help_text=_("Defines if a second chance exam is planned for this course."), |
222 |
) |
223 |
tolerable = models.BooleanField( |
224 |
default=True, |
225 |
help_text=_("Defines if a failed result can be tolerated."), |
226 |
) |
227 |
scoring = models.CharField( |
228 |
max_length=2, |
229 |
choices = ( |
230 |
('N', _("Numerical")), |
231 |
('FP', _("Fail/Pass")), |
232 |
), |
233 |
default='N', |
234 |
blank=False, |
235 |
help_text=_("How the obtained score for this course is given."), |
236 |
) |
237 |
|
238 |
def __str__(self): |
239 |
return str(self.study) + " - " + str(self.course) |
240 |
|
241 |
class Study(models.Model): |
242 |
""" Defines a certain study that can be followed at the university. |
243 |
This also includes abridged study programmes, like transition programmes. |
244 |
Other information, such as descriptions, are kept in the template file |
245 |
of this study, which can be manually edited. Joeni searches for a file |
246 |
with the exact name as the study + ".html". So if the study is called |
247 |
"Bachelor of Informatics", it will search for "Bachelor of Informatics.html". |
248 |
""" |
249 |
# Degree types |
250 |
BSc = _("Bachelor of Science") |
251 |
MSc = _("Master of Science") |
252 |
LLB = _("Bachelor of Laws") |
253 |
LLM = _("Master of Laws") |
254 |
BA = _("Bachelor of Arts") |
255 |
MA = _("Master of Arts") |
256 |
ir = _("Engineer") |
257 |
ing = _("Technical Engineer") |
258 |
# Faculties |
259 |
FoMaLS = _("Faculty of Medicine and Life Sciences") |
260 |
FoS = _("Faculty of Sciences") |
261 |
FoTS = _("Faculty of Transportation Sciences") |
262 |
FoAaA = _("Faculty of Architecture and Arts") |
263 |
FoBE = _("Faculty of Business Economics") |
264 |
FoET = _("Faculty of Engineering Technology") |
265 |
FoL = _("Faculty of Law") |
266 |
|
267 |
name = models.CharField( |
268 |
max_length=128, |
269 |
blank=False, |
270 |
unique=True, |
271 |
help_text=_("The full name of this study, in the language it's taught in."), |
272 |
) |
273 |
degree_type = models.CharField( |
274 |
max_length=64, |
275 |
choices = ( |
276 |
('BSc', BSc), |
277 |
('MSc', MSc), |
278 |
('LL.B', LLB), |
279 |
('LL.M', LLM), |
280 |
('ir.', ir ), |
281 |
('ing.',ing), |
282 |
('BA', BA), |
283 |
('MA', MA), |
284 |
), |
285 |
blank=False, |
286 |
help_text=_("The type of degree one obtains upon passing this study."), |
287 |
) |
288 |
language = models.CharField( |
289 |
max_length=64, |
290 |
choices = ( |
291 |
('NL', _("Dutch")), |
292 |
('EN', _("English")), |
293 |
('FR', _("French")), |
294 |
), |
295 |
null=False, |
296 |
help_text=_("The language in which this study is given."), |
297 |
) |
298 |
# Information about exam committee |
299 |
chairman = models.ForeignKey( |
300 |
"administration.User", |
301 |
on_delete=models.PROTECT, |
302 |
null=False, |
303 |
limit_choices_to={'is_staff': True}, |
304 |
help_text=_("The chairman of this study."), |
305 |
related_name="chairman", |
306 |
) |
307 |
vice_chairman = models.ForeignKey( |
308 |
"administration.User", |
309 |
on_delete=models.PROTECT, |
310 |
null=False, |
311 |
help_text=_("The vice-chairman of this study."), |
312 |
limit_choices_to={'is_staff': True}, |
313 |
related_name="vice_chairman", |
314 |
) |
315 |
secretary = models.ForeignKey( |
316 |
"administration.User", |
317 |
on_delete=models.PROTECT, |
318 |
null=False, |
319 |
help_text=_("The secretary of this study."), |
320 |
limit_choices_to={'is_staff': True}, |
321 |
related_name="secretary", |
322 |
) |
323 |
ombuds = models.ForeignKey( |
324 |
"administration.User", |
325 |
on_delete=models.PROTECT, |
326 |
null=False, |
327 |
help_text=_("The ombuds person of this study."), |
328 |
limit_choices_to={'is_staff': True}, |
329 |
related_name="ombuds", |
330 |
) |
331 |
vice_ombuds = models.ForeignKey( |
332 |
"administration.User", |
333 |
on_delete=models.PROTECT, |
334 |
null=False, |
335 |
help_text=_("The (replacing) ombuds person of this study."), |
336 |
limit_choices_to={'is_staff': True}, |
337 |
related_name="vice_ombuds", |
338 |
) |
339 |
additional_members = models.ManyToManyField( |
340 |
"administration.User", |
341 |
help_text=_("All the other members of the exam committee."), |
342 |
limit_choices_to={'is_staff': True}, |
343 |
related_name="additional_members", |
344 |
) |
345 |
faculty = models.CharField( |
346 |
max_length=6, |
347 |
choices = ( |
348 |
('FoS', FoS), |
349 |
('FoTS', FoTS), |
350 |
('FoAaA', FoAaA), |
351 |
('FoBE', FoBE), |
352 |
('FoMaLS', FoMaLS), |
353 |
('FoET', FoET), |
354 |
('FoL', FoL), |
355 |
), |
356 |
blank=False, |
357 |
help_text=_("The faculty where this study belongs to."), |
358 |
) |
359 |
|
360 |
#def study_points(self): |
361 |
""" Returns the amount of study points for this year. |
362 |
This value is inferred based on the study programme information |
363 |
records that lists this study as their foreign key. """ |
364 |
#total_ECTS = 0 |
365 |
#for course in CourseProgramme.objects.filter(study=self): |
366 |
#total_ECTS += course.ECTS |
367 |
#return total_ECTS |
368 |
# XXX: Commented because this is actually something for the StudyProgramme |
369 |
def years(self): |
370 |
""" Returns the amount of years this study takes. |
371 |
This value is inferred based on the study programme information |
372 |
records that lists this study as their foreign key. """ |
373 |
highest_year = 0 |
374 |
for course in CourseProgramme.objects.filter(study=self): |
375 |
highest_year = max(highest_year, course.year) |
376 |
return highest_year |
377 |
|
378 |
def students(self): |
379 |
""" Cross references the information stored in the database, and |
380 |
returns all the students that are following this study in this |
381 |
academic year. """ |
382 |
return 0 # TODO |
383 |
|
384 |
|
385 |
def __str__(self): |
386 |
return self.name |
387 |
|
388 |
class StudyProgramme(models.Model): |
389 |
""" Represents a programme within a certain study. |
390 |
A good example for this is the different specializations, minors, majors, ... |
391 |
one can follow within the same study. Nevertheless, they're all made of |
392 |
a certain set of courses. This table collects all these, and allows one to name |
393 |
them, so they're distinct from one another. """ |
394 |
name = models.CharField( |
395 |
max_length=64, |
396 |
blank=False, |
397 |
help_text=_("The name of this programme."), |
398 |
) |
399 |
|
400 |
def courses(self): |
401 |
""" All courses that are part of this study programme. """ |
402 |
programmes = CourseProgramme.objects.filter(study_programme=self) |
403 |
courses = {} |
404 |
for program in programmes: |
405 |
courses.add(program.course) |
406 |
return courses |
407 |
|
408 |
def study_points(self, year=None): |
409 |
""" Returns the amount of study points this programme contains. |
410 |
Accepts year as an optional argument. If not given, the study points |
411 |
of all years are returned. """ |
412 |
programmes = CourseProgramme.objects.filter(study_programme=self) |
413 |
ECTS = 0 |
414 |
for program in programmes: |
415 |
if year is None or program.year == year: |
416 |
# XXX: This only works if the used implementation does lazy |
417 |
# evaluation, otherwise this is a type error! |
418 |
ECTS += program.ECTS |
419 |
return ECTS |
420 |
|
421 |
def __str__(self): |
422 |
return self.name |
423 |
|
424 |
# Tables about things related to the courses: |
425 |
|
426 |
class Assignment(models.Model): |
427 |
""" For courses, it's possible to set up tasks. These tasks are recorded |
428 |
here. """ |
429 |
# TODO: Require that only the course team can create assignments for a team. |
430 |
course = models.ForeignKey( |
431 |
"Course", |
432 |
on_delete=models.CASCADE, |
433 |
null=False, |
434 |
#editable=False, |
435 |
db_index=True, |
436 |
help_text=_("The course for which this task is assigned."), |
437 |
) |
438 |
title = models.CharField( |
439 |
max_length=32, |
440 |
blank=False, |
441 |
help_text=_("The title of this assignment."), |
442 |
) |
443 |
information = models.TextField( |
444 |
help_text=_("Any additional information regarding the assignment. Orgmode syntax available."), |
445 |
) |
446 |
deadline = models.DateTimeField( |
447 |
null=False, |
448 |
help_text=_("The date and time this task is due."), |
449 |
) |
450 |
posted = models.DateField(auto_now_add=True) |
451 |
digital_task = models.BooleanField( |
452 |
default=True, |
453 |
help_text=_("This determines whether this assignment requires handing " |
454 |
"in a digital file."), |
455 |
) |
456 |
|
457 |
def __str__(self): |
458 |
return str(self.course) +" | "+ str(self.posted) |
459 |
|
460 |
class Announcement(models.Model): |
461 |
""" Courses sometimes have to make announcements for the students. """ |
462 |
course = models.ForeignKey( |
463 |
"Course", |
464 |
on_delete=models.CASCADE, |
465 |
null=False, |
466 |
#editable=False, |
467 |
db_index=True, |
468 |
help_text=_("The course for which this announcement is made."), |
469 |
) |
470 |
title = models.CharField( |
471 |
max_length=20, # Keep It Short & Simple® |
472 |
help_text=_("A quick title for what this is about."), |
473 |
) |
474 |
text = models.TextField( |
475 |
blank=False, |
476 |
help_text=_("The announcement itself. Orgmode syntax available."), |
477 |
) |
478 |
posted = models.DateTimeField(auto_now_add=True) |
479 |
|
480 |
def __str__(self): |
481 |
return str(self.course) +" | "+ self.posted.strftime("%m/%d") |
482 |
|
483 |
class Upload(models.Model): |
484 |
""" For certain assignments, digital hand-ins may be required. These hand |
485 |
ins are recorded per student in this table. """ |
486 |
course = models.ForeignKey( |
487 |
"Course", |
488 |
on_delete=models.CASCADE, |
489 |
null=False, |
490 |
db_index=True, |
491 |
) |
492 |
assignment = models.ForeignKey( |
493 |
"Assignment", |
494 |
on_delete=models.CASCADE, |
495 |
null=False, |
496 |
#editable=False, |
497 |
db_index=True, |
498 |
limit_choices_to={"digital_task": True}, |
499 |
help_text=_("For which assignment this upload is."), |
500 |
) |
501 |
# TODO: Try to find a way to require that, if the upload is made, |
502 |
# only students that have this course in their curriculum can upload. |
503 |
student = models.ForeignKey( |
504 |
"administration.User", |
505 |
on_delete=models.CASCADE, |
506 |
null=False, |
507 |
#editable=False, |
508 |
limit_choices_to={"is_student": True}, |
509 |
help_text=_("The student who handed this in."), |
510 |
) |
511 |
upload_time = models.DateTimeField(auto_now_add=True) |
512 |
comment = models.TextField( |
513 |
blank=True, |
514 |
help_text=_("If you wish to add an additional comment, state it here."), |
515 |
) |
516 |
file = models.FileField( |
517 |
upload_to="assignments/uploads/%Y/%m/", |
518 |
null=False, |
519 |
#editable=False, |
520 |
help_text=_("The file you want to upload for this assignment."), |
521 |
) |
522 |
|
523 |
|
524 |
def __str__(self): |
525 |
deadline = self.assignment.deadline |
526 |
if deadline < self.upload_time: |
527 |
return str(self.assignment.course) +" | "+ str(self.student.number) + _("(OVERDUE)") |
528 |
else: |
529 |
return str(self.assignment.course) +" | "+ str(self.student.number) |
530 |
|
531 |
def item_upload_directory(instance, filename): |
532 |
return "courses/" + instance.course.slug_name + "/" + filename |
533 |
class CourseItem(models.Model): |
534 |
""" Reprensents study material for a course that is being shared by the |
535 |
course's education team. """ |
536 |
course = models.ForeignKey( |
537 |
Course, |
538 |
on_delete=models.CASCADE, |
539 |
null=False, |
540 |
#editable=False, |
541 |
) |
542 |
file = models.FileField( |
543 |
upload_to=item_upload_directory, |
544 |
null=False, |
545 |
#editable=False, |
546 |
help_text=_("The file you wish to upload."), |
547 |
) |
548 |
def canonical(self): |
549 |
return str(self.file.name).split("/")[-1] |
550 |
timestamp = models.DateTimeField(auto_now_add=True) |
551 |
note = models.TextField( |
552 |
blank=True, |
553 |
help_text=_("If you want to state some additional information about " |
554 |
"this upload, state it here."), |
555 |
) |
556 |
|
557 |
class StudyGroup(models.Model): |
558 |
""" It may be necessary to make study groups regarding a course. These |
559 |
are recorded here, and blend in seamlessly with the Groups from Agora. |
560 |
Groups that are recorded as a StudyGroup, are given official course status, |
561 |
and thus, cannot be removed until the status of StudyGroup is lifted. """ |
562 |
course = models.ForeignKey( |
563 |
"Course", |
564 |
on_delete=models.CASCADE, |
565 |
null=False, |
566 |
#editable=False, |
567 |
db_index=True, |
568 |
help_text=_("The course for which this group is."), |
569 |
) |
570 |
group = models.ForeignKey( |
571 |
"agora.Group", |
572 |
on_delete=models.PROTECT, # See class documentation |
573 |
null=False, |
574 |
#editable=False, # Keep the same group |
575 |
help_text=_("The group that will be seen as the study group."), |
576 |
) |
577 |
|
578 |
def __str__(self): |
579 |
return str(self.course) +" | "+ str(self.group) |
580 |
|
581 |
class CourseGroup(models.Model): |
582 |
"""Because of size, some studies may use multiple groups for the different |
583 |
students, so it's possible to facilitate all of them. These groups must be |
584 |
registered here.""" |
585 |
study = models.ForeignKey( |
586 |
"Study", |
587 |
on_delete=models.CASCADE, |
588 |
null=False, |
589 |
) |
590 |
# TODO: How to attach students to certain groups? The curriculum or what? |
591 |