joeni

Adding changes to roster branch

Author
Maarten 'Vngngdn' Vangeneugden
Date
April 11, 2018, 6:07 p.m.
Hash
975dae00e067e87580e76f559ddc403b45c1d8f4
Parent
a2bf82891efae1e751a01e38860f8a1a11646db7
Modified files
administration/models.py
administration/templates/administration/nav.djhtml
administration/urls.py
administration/views.py
courses/models.py
courses/urls.py
courses/views.py
joeni/templates/joeni/header.djhtml
static/css/header.css

administration/models.py

49 additions and 14 deletions.

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

administration/templates/administration/nav.djhtml

1 addition and 1 deletion.

View changes Hide changes
1
1
{% load i18n %}
2
2
<nav>
3
3
    <a href="{% url 'administration-settings' %}">{% trans "Personal settings" %}</a>
4
4
    <!--<a href="{% url 'administration-curriculum' %}">{% trans "Curricula" %}</a>-->
5
-
    <a href="{% url 'administration-forms' %}">{% trans "Forms" %}</a>
+
5
    <a href="{% url 'administration-forms' %}">{% trans "Forms" %}</a>
6
6
    <a href="{% url 'administration-rooms' %}">{% trans "Rooms" %}</a>
7
7
    <a href="{% url 'administration-jobs' %}">{% trans "Jobs" %}</a>
8
8
    <a href="{% url 'administration-roster' %}">{% trans "Personal Roster" %}</a>
9
9
    <a href="{% url 'administration-bulletin-board' %}">{% trans "Bulletin board" %}</a>
10
10
    PingPing: €{{ money }}
11
11
</nav>
12
12

administration/urls.py

13 additions and 13 deletions.

View changes Hide changes
1
1
from django.contrib.auth import views as auth_views
2
2
from . import views
3
3
from django.utils.translation import gettext_lazy as _
4
4
5
5
urlpatterns = ([
6
6
    path('', views.index, name='administration-index'),
7
7
    path(_('pre-registration'), views.pre_registration, name='administration-pre-registration'),
8
-
    path(_('settings'), views.settings, name='administration-settings'),
+
8
    path(_('settings'), views.settings, name='administration-settings'),
9
9
    path(_('curriculum'), views.curriculum, name='administration-curriculum'),
10
-
    # Commented because they might very well be merged with curriculum
+
10
    # Commented because they might very well be merged with curriculum
11
11
    #path(_('results'), views.results, name='administration-results'),
12
12
    #path(_('results/<slug:course>'), views.result, name='administration-results'),
13
13
    #path(_('results/<int:student_id>'), views.result, name='administration-results'),
14
14
    path(_('forms'), views.forms, name='administration-forms'),  # In Dutch: "Attesten"
15
-
    path(_('forms/<str:form>'), views.forms, name='administration-forms'),
16
-
    path(_('rooms'), views.rooms, name='administration-rooms'),
17
-
    path(_('rooms/<str:room>'), views.room_detail, name='administration-room-detail'),
18
-
    #path(_('rooms/reservate'), views.room_reservate, name='administration-room-reservate'),
+
15
    path(_('forms/<str:form>'), views.forms, name='administration-forms'),  # HOLD
+
16
    path(_('rooms'), views.rooms, name='administration-rooms'),  # TODO
+
17
    path(_('rooms/<str:room>'), views.room_detail, name='administration-room-detail'),  # TODO
+
18
    #path(_('rooms/reservate'), views.room_reservate, name='administration-room-reservate'),
19
19
    path(_('roster'), views.roster, name='administration-roster'),
20
-
    re_path(_('roster/(?P<begin>[0-9]{2}-[0-9]{2}-[0-9]{4})/(?P<end>[0-9]{2}-[0-9]{2}-[0-9]{4})'), views.roster, name='administration-roster'),
21
-
    path(_('jobs'), views.jobs, name='administration-jobs'),
22
-
    path(_('bulletin-board'), views.bulletin_board, name='administration-bulletin-board'),
23
-
    path(_('user/<slug:slug_name>'), views.user, name='administration-user'),
24
-
    path(_('roster/<slug:user_slug>.ics'), views.roster_ics, name='administration-roster-ics'),
25
-
+
20
    re_path(_('roster/(?P<begin>[0-9]{2}-[0-9]{2}-[0-9]{4})/(?P<end>[0-9]{2}-[0-9]{2}-[0-9]{4})'), views.roster, name='administration-roster'),  # TODO
+
21
    path(_('jobs'), views.jobs, name='administration-jobs'),  # HOLD
+
22
    path(_('bulletin-board'), views.bulletin_board, name='administration-bulletin-board'),  # TODO
+
23
    path(_('user/<slug:slug_name>'), views.user, name='administration-user'),  # TODO
+
24
    path(_('roster/<slug:user_slug>.ics'), views.roster_ics, name='administration-roster-ics'),  # TODO
+
25
26
26
    path('login', views.login, name='administration-login'),
27
-
    ])
+
27
    ])
28
28

administration/views.py

23 additions and 2 deletions.

View changes Hide changes
1
1
from collections import OrderedDict
2
2
from django.http import HttpResponseRedirect
3
3
import datetime
4
4
from django.urls import reverse # Why?
5
5
from django.utils.translation import gettext as _
6
6
from .models import *
7
7
from .forms import UserDataForm
8
8
from .new_roster import create_roster_rows
9
9
import administration
10
10
from django.contrib.auth.decorators import login_required
11
11
from django.contrib.auth import authenticate
12
12
13
13
@login_required
14
14
def roster(request, begin=None, end=None):
15
15
    """Collects and renders the data that has to be displayed in the roster.
16
16
17
17
    The begin and end date can be specified. Only roster points in that range
18
18
    will be included in the response. If no begin and end are specified, it will
19
19
    take the current week as begin and end point. If it's
20
20
    weekend, it will take next week."""
21
21
22
22
    # TODO Handle given begin and end
23
23
    context = dict()
24
24
    #context = {'money' : update_balance(None)}
25
25
    template = "administration/roster.djhtml"
26
26
27
27
    if begin is None or end is None:
28
28
        today = datetime.date.today()
29
29
        if today.isoweekday() in {6,7}:  # Weekend
30
30
            begin = today + datetime.timedelta(days=8-today.isoweekday())
31
31
            end = today + datetime.timedelta(days=13-today.isoweekday())
32
32
        else:  # Same week
33
33
            begin = today - datetime.timedelta(days=today.weekday())
34
34
            end = today + datetime.timedelta(days=5-today.isoweekday())
35
35
    else:  # Changing regexes to date objects
36
36
        b = begin.split("-")
37
37
        e = end.split("-")
38
38
        begin = datetime.datetime(int(b[2]),int(b[1]),int(b[0]))
39
39
        end = datetime.datetime(int(e[2]),int(e[1]),int(e[0]))
40
40
41
41
    context['begin'] = begin
42
42
    context['end'] = end
43
43
44
44
    context['prev_begin'] = (begin - datetime.timedelta(days=7)).strftime("%d-%m-%Y")
45
45
    context['prev_end'] = (begin - datetime.timedelta(days=2)).strftime("%d-%m-%Y")
46
46
    context['next_begin'] = (end + datetime.timedelta(days=2)).strftime("%d-%m-%Y")
47
47
    context['next_end'] = (end + datetime.timedelta(days=7)).strftime("%d-%m-%Y")
48
48
49
49
    days = [begin]
50
50
    while (end-days[-1]).days != 0:
51
51
        # Human translation: Keep adding days until the last day in the array of
52
52
        # days is the same day as the last day the user wants to see the roster for.
53
53
        days.append(days[-1] + datetime.timedelta(days=1))
54
54
    context['days'] = days
55
55
56
56
    # Collecting events
57
57
    course_events = CourseEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end).order_by("begin_time")
58
58
    #university_events = UniversityEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
59
59
    #study_events = StudyEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
60
60
    #events = Event.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
61
61
    conflicts, table_code = create_roster_rows(course_events)
62
62
63
63
    context['time_blocks'] = table_code
64
64
    context['conflicts'] = conflicts
65
65
    #print(time_blocks)
66
66
    return render(request, template, context)
67
67
    # TODO Finish!
68
68
69
69
def roster_ics(request, user_slug):
70
70
    template = "administration/roster.ics"
71
71
    context = dict()
72
72
    context['events'] = CourseEvent.objects.all()  # FIXME: Filter to personal calendar items!
73
73
    return render(request, template, context)
74
74
75
75
def index(request):
76
76
    template = "administration/index.djhtml"
77
77
    #context = {'money': update_balance(None)}
78
78
    return render(request, template, context)
79
79
80
80
    pass
81
81
82
82
def pre_registration(request):
83
83
    user_data_form = UserDataForm()
84
84
    template = "administration/pre_registration.djhtml"
85
85
    context = dict()
86
86
87
87
    if request.method == 'POST':
88
88
        user_data_form = UserDataForm(request.POST)
89
89
        context['user_data_form'] = user_data_form
90
90
        if user_data_form.is_valid():
91
91
            user_data_form.save()
92
92
            context['messsage'] = _("Your registration has been completed. You will receive an e-mail shortly.")
93
93
        else:
94
94
            context['messsage'] = _("The data you supplied had errors. Please review your submission.")
95
95
    else:
96
96
        context['user_data_form'] = UserDataForm(instance = user_data_form)
97
97
98
98
    return render(request, template, context)
99
99
    pass
100
100
101
101
@login_required
102
102
def settings(request):
103
103
    user_data = UserData.objects.get(user=request.user)
104
104
    user_data_form = UserDataForm(instance = user_data)
105
105
    template = "administration/settings.djhtml"
106
106
    #context = {'money' : update_balance(None)}
+
107
    #context = {'money' : update_balance(None)}
107
108
108
109
    if request.method == 'POST':
109
110
        user_data_form = UserDataForm(request.POST, instance = user_data)
110
111
        context['user_data_form'] = user_data_form
111
112
        if user_data_form.is_valid():
112
113
            user_data_form.save()
113
114
            context['messsage'] = _("Your settings were successfully updated.")
114
115
        else:
115
116
            context['messsage'] = _("The data you supplied had errors. Please review your submission.")
116
117
    else:
117
118
        context['user_data_form'] = UserDataForm(instance = user_data)
118
119
119
120
    return render(request, template, context)
120
121
121
122
@login_required
122
123
def bulletin_board(request):
123
124
    context = dict()
124
125
    #context = {'money' : update_balance(None)}
125
126
    context['exam_commission_decisions'] = ExamCommissionDecision.objects.filter(user=request.user)
126
127
    context['education_department_messages'] = EducationDepartmentMessages.objects.all()
127
128
    for item in context['education_department_messages']:
128
129
        print(item.text)
129
130
    template = "administration/bulletin_board.djhtml"
130
131
    return render(request, template, context)
131
132
132
133
def jobs(request):
133
134
    context = dict()
134
135
    #context = {'money' : update_balance(None)}
135
136
    template = "administration/jobs.djhtml"
136
137
    #@context['decisions'] = ExamCommissionDecision.objects.filter(user=request.user)
137
138
    return render(request, template, context)
138
139
139
140
140
141
@login_required
141
142
def curriculum(request):
142
143
    context = dict()
143
144
    #context = {'money' : update_balance(None)}
144
145
    template = "administration/curriculum.djhtml"
145
146
    context['curricula'] = Curriculum.objects.filter(student=request.user)
146
147
    context['cource_results'] = CourseResult.objects.filter(student=request.user)
147
-
    return render(request, template, context)
+
148
        for co in item.course_programmes_results():
+
149
            print(co)
+
150
    return render(request, template, context)
148
151
149
152
def result(request):
150
153
    return render(request, template, context)
151
154
152
155
@login_required
153
156
def results(request):
154
157
    results = CourseResult.objects.filter(student=request.user)
155
158
    template = "administration/results.djhtml"
156
159
    # TODO
157
160
    return render(request, template, context)
158
161
159
162
def forms(request):
160
163
    context = dict()
161
164
    #context = {'money' : update_balance(None)}
162
165
    template = "administration/forms.djhtml"
163
166
    return render(request, template, context)
164
167
165
168
def user(request, slug_name):
166
169
    pass
167
170
168
171
def rooms(request):
169
172
    context = dict()
170
173
    #context = {'money' : update_balance(None)}
171
174
    context['rooms'] = Room.objects.all()
172
175
    context['room_reservations'] = RoomReservation.objects.all()
173
176
    context['course_events'] = CourseEvent.objects.all()
174
177
    context['blocks'] = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
175
178
176
179
177
-
+
180
    now = datetime.datetime.now(datetime.timezone.utc)
+
181
    end = now + datetime.timedelta(hours=2)
+
182
    free_rooms = dict()
+
183
    for room in context['rooms']:
+
184
        if room.reservation_possible(now, end):
+
185
            event = room.next_event(end)
+
186
            reservation = room.next_reservation(end)
+
187
            if event is None and reservation is None:
+
188
                free_rooms[room] = None
+
189
            elif reservation is not None:
+
190
                free_rooms[room] = event.begin_time
+
191
            elif event is not None:
+
192
                free_rooms[room] = reservation.begin_time
+
193
            elif event.begin_time < reservation.begin_time:
+
194
                free_rooms[room] = event.begin_time
+
195
            else:
+
196
                free_rooms[room] = reservation.begin_time
+
197
    context['free_rooms'] = free_rooms
+
198
178
199
    template = "administration/rooms.djhtml"
179
200
    return render(request, template, context)
180
201
181
202
def room_detail(request, room):
182
203
    template = "administration/room_detail.djhtml"
183
204
    context = dict()
184
205
    #context = {'money' : update_balance(None)}
185
206
    room = Room.objects.get(name=room)
186
207
    context['room'] = room
187
208
    context['reservations'] = RoomReservation.objects.filter(room=room).filter(begin_time__gte=datetime.datetime.now())
188
209
    context['course_events'] = CourseEvent.objects.filter(room=room).filter(begin_time__gte=datetime.datetime.now())
189
210
    # Building the room occupancy of today:
190
211
    today = datetime.date.today()
191
212
    if today.isoweekday() in {6,7}:  # Weekend
192
213
        today = today + datetime.timedelta(days=8-today.isoweekday())
193
214
194
215
    context['days'] = [today]
195
216
196
217
    # Collecting events
197
218
    course_events = CourseEvent.objects.filter(room=room).filter(begin_time__date=today)
198
219
    print(course_events)
199
220
    #university_events = UniversityEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
200
221
    #study_events = StudyEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
201
222
    #events = Event.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
202
223
203
224
    conflicts, table_code = create_roster_rows(course_events)
204
225
    context['time_blocks'] = table_code
205
226
    context['conflicts'] = conflicts
206
227
    print(context['time_blocks'])
207
228
    return render(request, template, context)
208
229
209
230
def login(request):
210
231
    context = dict()
211
232
    #context = {'money' : update_balance(None)}
212
233
    if request.method == "POST":
213
234
        name = request.POST['name']
214
235
        passphrase = request.POST['pass']
215
236
        user = authenticate(username=name, password=passphrase)
216
237
        if user is not None: # The user was successfully authenticated
217
238
            print("YA")
218
239
            return HttpResponseRedirect(request.POST['next'])
219
240
        else: # User credentials were wrong
220
241
            context['next'] = request.POST['next']
221
242
            context['message'] = _("The given credentials were not correct.")
222
243
    else:
223
244
        context['next'] = request.GET.get('next', None)
224
245
        if context['next'] is None:
225
246
            context['next'] = reverse('administration-index')
226
247
227
248
    template = 'administration/login.djhtml'
228
249
229
250
    return render(request, template, context)
230
251

courses/models.py

1 addition and 1 deletion.

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

courses/urls.py

1 addition and 0 deletions.

View changes Hide changes
1
1
from . import views
2
2
from django.utils.translation import ugettext_lazy as _
3
3
4
4
urlpatterns = [
5
5
    path('', views.index, name='courses-index'),
6
6
    path('<slug:course_slug>', views.course, name='courses-course-index'),
7
7
    path(_('<slug:course_slug>/<int:assignment_id>/upload'), views.upload, name='courses-upload'),
8
8
    path(_('<slug:course_slug>/new-item'), views.new_item, name='courses-new-item'),
9
9
    path(_('<slug:course_slug>/groups'), views.groups, name='courses-groups'),
10
10
    ]
+
11
    ]
11
12

courses/views.py

52 additions and 0 deletions.

View changes Hide changes
1
1
import datetime
2
2
from django.urls import reverse # Why?
3
3
from django.utils.translation import gettext as _
4
4
from .models import *
5
5
from .forms import *
6
6
import administration
7
7
from django.contrib.auth.decorators import login_required
8
8
9
9
def current_academic_year():
10
10
    """ Returns the current academic year. The year is determined as follows:
11
11
    - If today is before September 15 of the current year, the returned value
12
12
      is the current year - 1.
13
13
    - If today is after September 15 of the current year, but before January 1
14
14
      of the next year, it returns the current year as is.
15
15
    """
16
16
    today = datetime.datetime.now()
17
17
    switch = datetime.datetime(datetime.datetime.now().year, 9, 15)
18
18
    if today < switch:
19
19
        return today.year - 1
20
20
    else:
21
21
        return today.year
22
22
23
23
@login_required
24
24
def index(request):
25
25
    """ Starting page regarding the courses. This serves two specific groups:
26
26
    - Students: Displays all courses that this student has in his/her curriculum
27
27
                for this academic year. Requires the curriculum to be accepted.
28
28
    - Staff: Displays all courses in which the staff member is part of the
29
29
             educating team, or is otherwise related to the course.
30
30
    Users who are not logged in will be sent to the login page.
31
31
    """
32
32
    template = "courses/index.djhtml"
33
33
    courses = set()
34
34
    if request.user.user_data.is_student:
35
35
        curricula = administration.models.Curriculum.objects.filter(student=request.user)
36
36
        #current_curriculum = curricula.filter(year__year=current_academic_year())
37
37
        #courses = current_curriculum.courses
38
38
        courses = curricula.first().courses
39
39
    elif request.user.user_data.is_staff:
40
40
        courses += adminstration.models.Course.filter(course_team__contains=request.user)
41
41
    else:
42
42
        raise django.exceptions.FieldError("User "+request.user.number+" is neither staff nor student")
43
43
44
44
    context = {
45
45
        'courses': courses,
46
46
        }
47
47
48
48
    return render(request, template, context)
49
49
50
50
@login_required
51
51
def course(request, course_slug):
52
52
    template = "courses/course.djhtml"
53
53
    course = Course.objects.get(slug_name=course_slug)
54
54
55
55
    # Check if user can see this page
56
56
    if request.user.user_data.is_student:
57
57
        curricula = administration.models.Curriculum.objects.filter(student=request.user)
58
58
        #current_curriculum = curricula.filter(year__year=current_academic_year())
59
59
        current_curriculum = curricula.first()
60
60
        if course not in current_curriculum.courses():
61
61
            """ I'm currently just redirecting to the index page, but maybe it's
62
62
            just as good to make an announcement that this course cannot be
63
63
            used by this user. """
64
64
            return index(request)
65
65
66
66
    context = {
67
67
        'course': course,
68
68
        'announcements': Announcement.objects.filter(course=course),
69
69
        'assignments': Assignment.objects.filter(course=course),
70
70
        'course-items': CourseItem.objects.filter(course=course),
71
71
        'study-groups': StudyGroup.objects.filter(course=course),
72
72
        'uploads': Upload.objects.filter(course=course).filter(student=request.user)
73
73
        }
74
74
    if request.user.user_data.is_student:
75
75
        context['upload_form'] = UploadForm()
76
76
77
77
    return render(request, template, context)
78
78
79
79
# TODO: Find a way to see if it's possible to require some permissions and to
80
80
# put them in a decorator
81
81
#@permission_required
82
82
@login_required
83
83
def new_item(request, course_slug):
84
84
    template = "courses/new_item.djhtml"
85
85
    course = Course.objects.get(slug_name=course_slug)
86
86
87
87
    if request.user.user_data.is_student or request.user not in course.course_team:
88
88
        # Students can't add new items. Redirect to index
89
89
        # Also redirect people who are not part of the course team
90
90
        redirect('courses-index')
91
91
    # Now able to assume user is allowed to add items to this course
92
92
93
93
    context = {
94
94
        'course': course,
95
95
        'announcements': Announcement.objects.filter(course=course),
96
96
        'assignments': Assignment.objects.filter(course=course),
97
97
        'course-items': CourseItem.objects.filter(course=course),
98
98
        'study-groups': StudyGroup.objects.filter(course=course),
99
99
        'uploads': Upload.objects.filter(course=course)
100
100
        }
101
101
102
102
    return render(request, template, context)
103
103
104
104
@login_required
+
105
def edit_course_items(request, course_slug):
+
106
    # TODO Only allow people on the course team to this page!
+
107
    template = "courses/edit_course_items.djhtml"
+
108
    context = dict()
+
109
    course_ = Course.objects.get(slug_name=course_slug)
+
110
    if request.method == 'POST':
+
111
        assignments = AssignmentFormSet(request.POST, prefix='assignments')
+
112
        announcements = AnnouncementFormSet(request.POST, prefix='announcements')
+
113
        course_items = CourseItemFormSet(request.POST, request.FILES, prefix='course_items')
+
114
        if assignments.is_valid() and announcements.is_valid() and course_items.is_valid():
+
115
            assignments.save(commit=False)
+
116
            announcements.save(commit=False)
+
117
            course_items.save(commit=False)
+
118
            for new_assignment in assignments.new_objects:
+
119
                new_assignment.course = course_
+
120
            for new_announcement in announcements.new_objects:
+
121
                new_announcement.course = course_
+
122
            for new_course_item in course_items.new_objects:
+
123
                new_coutse_item.course = course_
+
124
            assignments.save()
+
125
            announcements.save()
+
126
            course_items.save()
+
127
            return course(request, course_slug)
+
128
    else:
+
129
        assignments = AssignmentFormSet(
+
130
            queryset=Assignment.objects.filter(course=course_),
+
131
            prefix="assignments",
+
132
            )
+
133
        announcements = AnnouncementFormSet(
+
134
            queryset=Announcement.objects.filter(course=course_),
+
135
            prefix="announcements",
+
136
            )
+
137
        course_items = CourseItemFormSet(
+
138
            queryset=CourseItem.objects.filter(course=course_),
+
139
            prefix="course_items",
+
140
            )
+
141
    context['assignments'] = assignments
+
142
    context['announcements'] = announcements
+
143
    context['course_items'] = course_items
+
144
    return render(request, template, context)
+
145
+
146
+
147
@login_required
105
148
def remove(request, type, id):
106
149
    pass
107
150
108
151
@login_required
109
152
def upload(request):
110
153
    pass
111
154
112
155
@login_required
113
156
def groups(request):
114
157
    pass
115
158
+
159
def fiche(request, course_slug):
+
160
    """Displays the fiche for the given course. Includes information about all
+
161
    course programs."""
+
162
    template = "courses/fiche.djhtml"
+
163
    context = dict()
+
164
    course = Course.objects.get(slug_name=course_slug)
+
165
    
+
166
+
167

joeni/templates/joeni/header.djhtml

26 additions and 11 deletions.

View changes Hide changes
1
1
{% load static %}
2
2
{% get_media_prefix as media %}
3
3
<img width="200px" src="{% static "logos/uhasselt/simple_white.svg" %}" alt="◀ Joeni /▶" />
4
4
<ul>
5
-
    <li>
6
-
        <a href="{% url 'agora-index' %}">Agora</a>
7
-
    </li>
8
-
    <li>
9
-
        <a href="{% url 'administration-index' %}">{% trans "Administration" %}</a>
10
-
    </li>
11
-
    <li>
12
-
        <a href="{% url 'courses-index' %}">{% trans "Courses" %}</a>
13
-
    </li>
14
-
</ul>
15
-
+
5
    <ul>
+
6
        <li><a href="{% url 'agora-index' %}">Agora</a>
+
7
            <ul>
+
8
            </ul>
+
9
        </li>
+
10
+
11
        <li><a href="{% url 'administration-index' %}">{% trans "Administration" %}</a>
+
12
            <ul>
+
13
                <li><a href="{% url 'administration-settings' %}">{% trans "Personal settings" %}</a></li>
+
14
                <li><a href="{% url 'administration-curriculum' %}">{% trans "Curricula" %}</a></li>
+
15
                <li><a href="{% url 'administration-forms' %}">{% trans "Forms" %}</a></li>
+
16
                <li><a href="{% url 'administration-rooms' %}">{% trans "Rooms" %}</a></li>
+
17
                <li><a href="{% url 'administration-jobs' %}">{% trans "Jobs" %}</a></li>
+
18
                <li><a href="{% url 'administration-roster' %}">{% trans "Personal Roster" %}</a></li>
+
19
                <li><a href="{% url 'administration-bulletin-board' %}">{% trans "Bulletin board" %}</a></li>
+
20
            </ul>
+
21
        </li>
+
22
+
23
        <li><a href="{% url 'courses-index' %}">{% trans "Courses" %}</a>
+
24
            <ul>
+
25
            </ul>
+
26
        </li>
+
27
    </ul>
+
28
</nav>
+
29
PingPing: €{{ money }}
+
30

static/css/header.css

0 additions and 12 deletions.

View changes Hide changes
1
-
    position: relative;
2
-
    top: 0;
3
-
    width: 100%;
4
-
    background-color: #E73B2B;
5
-
    margin-top: -8px;
6
-
    margin-left: -8px;
7
-
}
8
-
header ul {
9
-
    display: inline;
10
-
    float: right;
11
-
}
12
-