joeni

models.py

1
from django.db import models
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