joeni

Big update to roster functionality

Today's been almost solely dedicated to roster updates. I've added a lot of functionality to make it possible to display events in the roster. I've also done some small updates to some of the models, because while some models allowed null values, it wasn't possible to /add/ null values, that required a blank. I've also added a validator for the events, so that they all take place in a quarter. I might lift this restriction in the future if I find a good and reliable way to make it work properly with HTML5 tables, but for now, this is pretty enough.

Author
Maarten 'Vngngdn' Vangeneugden
Date
Feb. 2, 2018, 11:16 p.m.
Hash
db2baced09f3dd36acf8ca5ebb352e88c7b30ed1
Parent
09579b7d3616c73134d6fe268f106bb32513d21b
Modified files
administration/models.py
administration/templates/administration/roster.djhtml
administration/views.py
courses/models.py

administration/models.py

16 additions and 2 deletions.

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

administration/templates/administration/roster.djhtml

39 additions and 0 deletions.

View changes Hide changes
1
1
{% cycle "hour" "first quarter" "half" "last quarter" as hour silent %}
2
2
{# "silent" blocks the cycle operator from printing the cycler, and in subsequent calls #}
3
3
{% load i18n %}
4
4
5
5
{% block title %}
6
6
    {% trans "Roster | ◀ Joeni /▶" %}
7
7
{% endblock %}
8
8
9
9
{% block main %}
10
10
    {% include "administration/nav.djhtml" %}
11
11
    <h1>{% trans "Personal timetable" %}</h1>
12
12
    <p>
+
13
    <p>
13
14
        {% trans "Personal roster from" %} {{ begin|date }} {% trans "to" %} {{ end|date }}
14
15
    </p>
15
16
    <table>
+
17
        {% blocktrans %}
+
18
            Some fields may have additional information that might be of interest
+
19
            to you. This information is shown in different ways with colour codes.
+
20
        {% endblocktrans %}
+
21
    </p>
+
22
    <p>
+
23
        {% blocktrans %}
+
24
            Most fields have a single colour, determined by the course. This makes
+
25
            it easier to differentiate on a glance what hours are for what courses.
+
26
        {% endblocktrans %}
+
27
    </p>
+
28
    <p>
+
29
        {% blocktrans %}
+
30
            Some fields have a
+
31
            <span style="background-color:yellow; color: red; border: medium dotted red;">
+
32
                bright yellow background, with red text and a red dotted border.</span>
+
33
            This indicates this event had one or more of its properties changed
+
34
            in the last five days. This can be the room, the hours, the subject, ...
+
35
            You're encouraged to take note of that.
+
36
        {% endblocktrans %}
+
37
    </p>
+
38
    <p>
+
39
        {% blocktrans %}
+
40
            Some fields are <span style="border: medium dashed black; background-color: white; color: black;">
+
41
            white with a dashed black border.</span> This indicates this event
+
42
            is new, and was added in the last five days.
+
43
        {% endblocktrans %}
+
44
    </p>
+
45
    <p>
+
46
        {% blocktrans %}
+
47
            Fields that flash <span style="color:orange; border: medium solid orange;">
+
48
            orange with the same coloured border</span> have a note attached to
+
49
            them by the docent/speaker. Hover over the event to display the note.
+
50
        {% endblocktrans %}
+
51
    </p>
+
52
+
53
    <h2>{% trans "Main hour roster" %}
+
54
    <table>
16
55
        <th>
17
56
            <td></td> {# Empty row for hours #}
18
57
            {% for day in days %}
19
58
                <td>{{ day|date:"l (d/m)" }}</td>
20
59
            {% endfor %}
21
60
        </th>
22
61
        {% for time, events in time_blocks %}
23
62
            <tr>
24
63
                {% if hour == "hour" %}
25
64
                    <td>{{ time }}</td>
26
65
                {% else %}
27
66
                    <td></td>
28
67
                {% endif %}
29
68
                {% cycle hour %}
30
69
                <td>{{ time }}</td>
31
70
                <!--<td rowspan="5">AI</td>
32
71
                <td>Dinsdag</td>
33
72
                <td>Dinsdag</td>
34
73
                <td>Woensdag</td>
35
74
                <td>Dondeddag</td>
36
75
                <td>Vdijdag</td>
37
76
                <td>Zaterdag</td>-->
38
77
            </tr>
39
78
        {% endfor %}
40
79
    </table>
41
80
{% endblock main %}
42
81

administration/views.py

99 additions and 19 deletions.

View changes Hide changes
1
1
from django.http import HttpResponseRedirect
+
2
from django.http import HttpResponseRedirect
2
3
import datetime
3
4
from django.urls import reverse # Why?
4
5
from django.utils.translation import gettext as _
5
6
from .models import *
6
7
from .forms import UserDataForm
7
8
import administration
8
9
from django.contrib.auth.decorators import login_required
9
10
from django.contrib.auth import authenticate
10
11
11
12
@login_required
+
13
def make_day_buckets(events):
+
14
    """Returns a dict with all days that the given events take place.
+
15
    Every day between the first and last event will get a bucket in the dict,
+
16
    with the keys in format "dd-mm-yyyy". Also days without events will be
+
17
    included, but will simply have empty buckets."""
+
18
    first_event = events[0]
+
19
    last_event = events[0]
+
20
    days = OrderedDict()  # This is the first time I ever use an OrderedDict and I intend it to be my last one as well.
+
21
    for event in events:
+
22
        if event < first_event:
+
23
            first_event = event
+
24
        if event > last_event:
+
25
            last_event = event
+
26
    days_count = (last_event - first_event).days
+
27
    event_counter = first_event
+
28
    for i in range(days_count):
+
29
        days[event_counter.strftime("%d-%m-%Y")] = list()
+
30
        event_counter += datetime.timedelta(days=1)
+
31
    for event in events:
+
32
        days[event.strftime("%d-%m-%Y")].append(event)
+
33
+
34
    return days  # Yay! ^.^
+
35
+
36
+
37
def create_roster_rows(events):
+
38
    """Creates the rows for use in the roster table.
+
39
    None of the times in the given events may overlap, and all must start and
+
40
    end at a quarter of the hour (so :00, :15, :30, or :45). If you think you're above this,
+
41
    I'll raise you a ValueError, kind sir.
+
42
    Events must be of administration.models.Event type."""
+
43
    for event in events:
+
44
        for other_event in events:
+
45
            if (
+
46
                    (event.begin_time > other_event.begin_time and event.begin_time > other_event.end_time)
+
47
                    or (event.end_time > other_event.begin_time and event.end_time > other_event.end_time)
+
48
                    ):
+
49
                raise ValueError("One of the events overlaps with another event.")
+
50
        if event.begin_time.minute not in [0, 15, 30, 45] or event.end_time.minute not in [0, 15, 30, 45]:
+
51
            raise ValueError("One of the events did not begin or end on a quarter.")
+
52
+
53
    # All events validated
+
54
    days = make_day_buckets(events)
+
55
+
56
    table_code = list()
+
57
    for hour in range(8, 20):
+
58
        for quarter in [0, 15, 30, 45]:
+
59
            quarter_line = "<tr><td>"
+
60
            if hour < 10:
+
61
                quarter_line += "0"
+
62
            quarter_line += str(hour) + ":"
+
63
            if quarter == 0:
+
64
                quarter_line += "0"
+
65
            quarter_line += str(quarter) + "</td>"
+
66
+
67
            for day in days:
+
68
                for event in day:
+
69
                    if event.begin_time.hour == hour and event.begin_time.minute == quarter:
+
70
                        quarters = (event.end_time - event.begin_time).minutes // 15
+
71
                        event_line = "<td "
+
72
                        if isinstance(event, administration.CourseEvent) and (datetime.datetime.today() - event.last_update).days > 5:
+
73
                            event_line += "style='backgrond-color: #"+event.course.color+"; color: white;' "
+
74
                        elif (datetime.datetime.today() - event.last_update).days <= 5:
+
75
                            event_line += "style='backgrond-color: yellow; color: red;' "
+
76
                        if event.note:
+
77
                            event_line +="title='" +event.note+ "' "
+
78
+
79
                        # FIXME: From here, I just assume the events are all CourseEvents, because
+
80
                        # that is the most important one to implement for the prototype.
+
81
                        if quarters > 1:
+
82
                            event_line += "rowspan='"+str(quarters)+"' "
+
83
+
84
                        event_line +=">"
+
85
                        event_line += str(
+
86
                            + "<a href=" + reverse("courses-course-index", args="")+"> "  # FIXME so that this links to the course's index page
+
87
                            + str(event.course)
+
88
                            + "<br />"
+
89
                            + str(event.docent)
+
90
                            + "<br />"
+
91
                            + str(event.room) + " (" + str(event.subject) + ")</a></td>")
+
92
                        quarter_line += event_line
+
93
                    else:
+
94
                        quarter_line += "<td></td>"
+
95
+
96
            quarter_line += "</tr>"
+
97
            code += quarter_line
+
98
    return code
+
99
+
100
+
101
+
102
+
103
+
104
+
105
+
106
@login_required
12
107
def roster(request, begin=None, end=None):
13
108
    """Collects and renders the data that has to be displayed in the roster.
14
109
15
110
    The begin and end date can be specified. Only roster points in that range
16
111
    will be included in the response. If no begin and end are specified, it will
17
112
    take the current week as begin and end point. If it's
18
113
    weekend, it will take next week."""
19
114
20
115
    # TODO Handle given begin and end
21
116
    context = dict()
22
117
    template = "administration/roster.djhtml"
23
118
24
119
    if begin is None or end is None:
25
120
        today = datetime.date.today()
26
121
        if today.isoweekday() in {6,7}:  # Weekend
27
122
            begin = today + datetime.timedelta(days=8-today.isoweekday())
28
123
            end = today + datetime.timedelta(days=13-today.isoweekday())
29
124
        else:  # Same week
30
125
            begin = today - datetime.timedelta(days=today.weekday())
31
126
            end = today + datetime.timedelta(days=5-today.isoweekday())
32
127
    context['begin'] = begin
33
128
    context['end'] = end
34
129
35
130
    days = [begin]
36
131
    while (end-days[-1]).days != 0:
37
132
        # Human translation: Keep adding days until the last day in the array of
38
133
        # days is the same day as the last day the user wants to see the roster for.
39
134
        days.append(days[-1] + datetime.timedelta(days=1))
40
135
    context['days'] = days
41
136
42
137
    # Collecting events
43
138
    course_events = CourseEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
44
139
    university_events = UniversityEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
45
-
    study_events = StudyEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
46
-
    events = Event.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
47
-
+
140
    #study_events = StudyEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
+
141
    #events = Event.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
+
142
48
143
    # Producing time blocks for display in the table
49
144
    time_blocks = []
50
145
    for i in range(8, 20):
51
146
        time_block = str(i)
52
147
        for j in range(0, 60, 15):
53
148
            if j == 0:
54
149
                time_blocks.append([time_block + ":00",""])
55
150
                continue
56
151
            time_blocks.append([time_block + ":" + str(j), ""])
57
152
    context['time_blocks'] = time_blocks
58
153
59
-
    # Preparing events for the template
60
-
    c_es = []
61
-
    for course_event in course_events:
62
-
        duration = course_event.end - course_event.begin
63
-
        quarters = duration.minutes // 15
64
-
        course_item = str(
65
-
              "<td style='background-color:'"+course_event.course.course.color+";' rowspan='"
66
-
            + str(quarters) + "'>"
67
-
            + "<a href=" + reverse("courses-course-index", args="")+"> "  # FIXME so that this links to the course's index page
68
-
            + str(course_event.course)
69
-
            + "<br />"
70
-
            + str(course_event.docent)
71
-
            + "<br />"
72
-
            + str(course_event.room) + " (" + str(course_event.subject) + ")</a></td>")
73
-
74
-
+
154
75
155
76
156
77
157
    return render(request, template, context)
78
158
    # TODO Finish!
79
159
80
160
def index(request):
81
161
    template = "administration/index.djhtml"
82
162
    context = {}
83
163
    return render(request, template, context)
84
164
85
165
    pass
86
166
87
167
def pre_registration(request):
88
168
    user_data_form = UserDataForm()
89
169
    template = "administration/pre_registration.djhtml"
90
170
    context = dict()
91
171
92
172
    if request.method == 'POST':
93
173
        user_data_form = UserDataForm(request.POST)
94
174
        context['user_data_form'] = user_data_form
95
175
        if user_data_form.is_valid():
96
176
            user_data_form.save()
97
177
            context['messsage'] = _("Your registration has been completed. You will receive an e-mail shortly.")
98
178
        else:
99
179
            context['messsage'] = _("The data you supplied had errors. Please review your submission.")
100
180
    else:
101
181
        context['user_data_form'] = UserDataForm(instance = user_data)
102
182
103
183
    return render(request, template, context)
104
184
    pass
105
185
106
186
@login_required
107
187
def settings(request):
108
188
    user_data = UserData.objects.get(user=request.user)
109
189
    user_data_form = UserDataForm(instance = user_data)
110
190
    template = "administration/settings.djhtml"
111
191
    context = dict()
112
192
113
193
    if request.method == 'POST':
114
194
        user_data_form = UserDataForm(request.POST, instance = user_data)
115
195
        context['user_data_form'] = user_data_form
116
196
        if user_data_form.is_valid():
117
197
            user_data_form.save()
118
198
            context['messsage'] = _("Your settings were successfully updated.")
119
199
        else:
120
200
            context['messsage'] = _("The data you supplied had errors. Please review your submission.")
121
201
    else:
122
202
        context['user_data_form'] = UserDataForm(instance = user_data)
123
203
124
204
    return render(request, template, context)
125
205
126
206
@login_required
127
207
def bulletin_board(request):
128
208
    context = dict()
129
209
    context['exam_commission_decisions'] = ExamCommissionDecision.objects.filter(user=request.user)
130
210
    context['education_department_messages'] = ExamCommissionDecision.objects.filter(user=request.user)
131
211
    template = "administration/bulletin_board.djhtml"
132
212
    return render(request, template, context)
133
213
134
214
def jobs(request):
135
215
    context = dict()
136
216
    template = "administration/jobs.djhtml"
137
217
    #@context['decisions'] = ExamCommissionDecision.objects.filter(user=request.user)
138
218
    return render(request, template, context)
139
219
140
220
141
221
def curriculum(request):
142
222
    return render(request, template, context)
143
223
144
224
def result(request):
145
225
    return render(request, template, context)
146
226
147
227
@login_required
148
228
def results(request):
149
229
    results = CourseResult.objects.filter(student=request.user)
150
230
    template = "administration/results.djhtml"
151
231
    # TODO
152
232
    return render(request, template, context)
153
233
154
234
def forms(request):
155
235
    return render(request, template, context)
156
236
157
237
def rooms(request):
158
238
    template = "administration/rooms.djhtml"
159
239
    return render(request, template, context)
160
240
161
241
def room_reservate(request):
162
242
    return render(request, template, context)
163
243
164
244
def login(request):
165
245
    context = dict()
166
246
    if request.method == "POST":
167
247
        name = request.POST['name']
168
248
        passphrase = request.POST['pass']
169
249
        user = authenticate(username=name, password=passphrase)
170
250
        if user is not None: # The user was successfully authenticated
171
251
            print("YA")
172
252
            return HttpResponseRedirect(request.POST['next'])
173
253
        else: # User credentials were wrong
174
254
            context['next'] = request.POST['next']
175
255
            context['message'] = _("The given credentials were not correct.")
176
256
    else:
177
257
        context['next'] = request.GET.get('next', None)
178
258
        if context['next'] is None:
179
259
            context['next'] = reverse('administration-index')
180
260
181
261
    template = 'administration/login.djhtml'
182
262
183
263
    return render(request, template, context)
184
264

courses/models.py

24 additions and 4 deletions.

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