joeni

Add bulletin board page

I changed the appropriate files to make a bulletin board page on which messages from the different departments will be posted.

Author
Maarten 'Vngngdn' Vangeneugden
Date
Feb. 1, 2018, 3:55 p.m.
Hash
bf3254b908dee6e18e21bccce1b5d832e87d871e
Parent
f3c3a199e290c7722eb1c3b5907c38a3cd00038b
Modified files
administration/models.py
administration/templates/administration/bulletin_board.djhtml
administration/urls.py
administration/views.py

administration/models.py

20 additions and 0 deletions.

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

administration/templates/administration/bulletin_board.djhtml

19 additions and 5 deletions.

View changes Hide changes
1
1
{% load i18n %}
2
2
{% load humanize %}
3
3
4
4
{% block title %}
5
5
    {% trans "Decisions of the exam commission" %} | ◀ Joeni /▶
6
-
{% endblock %}
+
6
{% endblock %}
7
7
8
8
<p>
9
9
    {% blocktrans %}
10
10
        Whenever the exam commission comes together to make a decision regarding
11
-
        you, the eventual decision resulting from the meetings will be posted
12
-
        here.
13
-
    {% endblocktrans %}
+
11
        listed. Some of these messages are public, some are private.
+
12
    {% endblocktrans %}
14
13
</p>
15
14
{% for decision in decisions %}
16
-
    <time datetime="{{ decision.date|date:"c" }}">{{ decision.date|date:"DATE_FORMAT" }}</time>
+
15
<h2 id="{% trans "exam-commission" %}">{% trans "Exam commission" %}</h2>
+
16
{% for decision in exam_commission_decisions %}
+
17
    <time datetime="{{ decision.date|date:"c" }}">{{ decision.date|date:"DATE_FORMAT" }}</time>
17
18
    <br />
18
19
    {{ decision.text|org }}
19
20
    <hr />
20
21
{% empty %}
21
22
    <p>
22
23
        {% trans "There are no decisions that affect you." %}
23
24
    </p>
24
25
    <hr />
25
26
{% endfor %}
26
27
27
28
<p>
+
29
{% for message in education_department_messages %}
+
30
    <h3 id="{{ message.title|slugify }}">{{ message.title }}</h3>
+
31
    <time datetime="{{ message.date|date:"c" }}">{{ message.date|date:"DATE_FORMAT" }}</time>
+
32
    <br />
+
33
    {{ message.text|org }}
+
34
    <hr />
+
35
{% empty %}
+
36
    <p>
+
37
        {% trans "No messages have been issued by the education department." %}
+
38
    </p>
+
39
+
40
<h2 id="{% trans "rights" %}">{% trans "Rights" %}</h2>
+
41
<p>
28
42
    {% blocktrans %} {# TODO #}
29
43
        Raadpleeg het onderwijs- en examenreglement
30
44
31
45
        Op grond van de Rechtspositieregeling in de OER-regeling voor studenten van de UHasselt/tUL kan een student die oordeelt dat een ongunstige studievoortgangsbeslissing (omschreven in Art. 1.2 van de Rechtspositieregeling) aangetast is door een schending van het recht, intern beroep aantekenen, voor zover dit geen voorwerp was van een eerder beroep. Dit beroep wordt formeel ingediend bij de secretaris van de beroepscommissie conform Art. 1.3 lid 4 van de Rechtspositieregeling op volgend adres:
32
46
33
47
        Secretaris interne beroepscommissie
34
48
        Lien Mampaey
35
49
        Universiteit Hasselt
36
50
        Martelarenlaan 42
37
51
        B-3500 Hasselt
38
52
39
53
        Om administratieve redenen wordt de student verzocht om het beroep ook te melden op het volgende emailadres:intern.beroep@uhasselt.be. Het verzoekschrift wordt op straffe van niet-ontvankelijkheid ingediend per aangetekend schrijven binnen een vervaltermijn van 7 kalenderdagen, die ingaat op de dag na de kennisgeving van de genomen studievoortgangsbeslissing aan de student. Als datum van het beroep geldt de datum van het postmerk van de aangetekende zending. Het verzoekschrift van de student omvat op straffe van niet-ontvankelijkheid tenminste:
40
54
41
55
            De naam, een correspondentieadres en de handtekening van de student of zijn raadsman;
42
56
            een vermelding van de beslissing waartegen het beroep gericht is en alle relevante (bewijs)stukken;
43
57
            een omschrijving van de ingeroepen bezwaren.
44
58
    {% endblocktrans %}
45
59
</p>
46
60

administration/urls.py

1 addition and 0 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
8
    path(_('settings'), views.settings, name='administration-settings'),
9
9
    path(_('curriculum'), views.curriculum, name='administration-curriculum'),
10
10
    path(_('results'), views.results, name='administration-results'),
11
11
    path(_('results/<slug:course>'), views.result, name='administration-results'),
12
12
    path(_('results/<int:student_id>'), views.result, name='administration-results'),
13
13
    path(_('forms'), views.forms, name='administration-forms'),  # In Dutch: "Attesten"
14
14
    path(_('forms/<str:form>'), views.forms, name='administration-forms'),
15
15
    path(_('rooms'), views.rooms, name='administration-rooms'),
16
16
    path(_('rooms/<str:room>'), views.rooms, name='administration-rooms'),
17
17
    path(_('rooms/reservate'), views.room_reservate, name='administration-room-reservate'),
18
18
    path(_('roster'), views.roster, name='administration-roster'),
19
19
    path(_('jobs'), views.jobs, name='administration-jobs'),
20
20
+
21
21
22
    path('login', views.login, name='administration-login'),
22
23
    ])
23
24

administration/views.py

10 additions and 4 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 UserDataForm
6
6
import administration
7
7
from django.contrib.auth.decorators import login_required
8
8
9
9
@login_required
10
10
def roster(request, begin=None, end=None):
11
11
    """Collects and renders the data that has to be displayed in the roster.
12
12
13
13
    The begin and end date can be specified. Only roster points in that range
14
14
    will be included in the response. If no begin and end are specified, it will
15
15
    take the current week as begin and end point. If it's
16
16
    weekend, it will take next week."""
17
17
18
18
    template = "administration/roster.djhtml"
19
19
20
20
    if begin is None or end is None:
21
21
        today = datetime.date.today()
22
22
        if today.isoweekday() in {6,7}:  # Weekend
23
23
            begin = today + datetime.timedelta(days=8-today.isoweekday())
24
24
            end = today + datetime.timedelta(days=13-today.isoweekday())
25
25
        else:  # Same week
26
26
            begin = today - datetime.timedelta(days=today.weekday())
27
27
            end = today + datetime.timedelta(days=5-today.isoweekday())
28
28
29
29
30
30
    return render(request, template, context)
+
31
    university_events = UniversityEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
+
32
    study_events = StudyEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
+
33
    events = Event.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
+
34
+
35
    return render(request, template, context)
31
36
    # TODO Finish!
32
37
33
38
def index(request):
34
39
    template = "administration/index.djhtml"
35
40
    context = {}
36
41
    return render(request, template, context)
37
42
38
43
    pass
39
44
40
45
def pre_registration(request):
41
46
    user_data_form = UserDataForm()
42
47
    template = "administration/pre_registration.djhtml"
43
48
    context = dict()
44
49
45
50
    if request.method == 'POST':
46
51
        user_data_form = UserDataForm(request.POST)
47
52
        context['user_data_form'] = user_data_form
48
53
        if user_data_form.is_valid():
49
54
            user_data_form.save()
50
55
            context['messsage'] = _("Your registration has been completed. You will receive an e-mail shortly.")
51
56
        else:
52
57
            context['messsage'] = _("The data you supplied had errors. Please review your submission.")
53
58
    else:
54
59
        context['user_data_form'] = UserDataForm(instance = user_data)
55
60
56
61
    return render(request, template, context)
57
62
    pass
58
63
59
64
@login_required
60
65
def settings(request):
61
66
    user_data = UserData.objects.get(user=request.user)
62
67
    user_data_form = UserDataForm(instance = user_data)
63
68
    template = "administration/settings.djhtml"
64
69
    context = dict()
65
70
66
71
    if request.method == 'POST':
67
72
        user_data_form = UserDataForm(request.POST, instance = user_data)
68
73
        context['user_data_form'] = user_data_form
69
74
        if user_data_form.is_valid():
70
75
            user_data_form.save()
71
76
            context['messsage'] = _("Your settings were successfully updated.")
72
77
        else:
73
78
            context['messsage'] = _("The data you supplied had errors. Please review your submission.")
74
79
    else:
75
80
        context['user_data_form'] = UserDataForm(instance = user_data)
76
81
77
82
    return render(request, template, context)
78
83
79
84
@login_required
80
85
def exam_commission_decisions(request):
81
-
    context = dict()
+
86
    context = dict()
82
87
    context['decisions'] = ExamCommissionDecision.objects.filter(user=request.user)
83
-
    template = "administration/exam_commission.djhtml"
84
-
    return render(request, template, context)
+
88
    context['education_department_messages'] = ExamCommissionDecision.objects.filter(user=request.user)
+
89
    template = "administration/bulletin_board.djhtml"
+
90
    return render(request, template, context)
85
91
86
92
def jobs(request):
87
93
    context = dict()
88
94
    template = "administration/jobs.djhtml"
89
95
    context['decisions'] = ExamCommissionDecision.objects.filter(user=request.user)
90
-
    return render(request, template, context)
+
96
    return render(request, template, context)
91
97
92
98
93
99
def curriculum(request):
94
100
    return render(request, template, context)
95
101
96
102
def result(request):
97
103
    return render(request, template, context)
98
104
99
105
@login_required
100
106
def results(request):
101
107
    results = CourseResult.objects.filter(student=request.user)
102
108
    template = "administration/results.djhtml"
103
109
    # TODO
104
110
    return render(request, template, context)
105
111
106
112
def forms(request):
107
113
    return render(request, template, context)
108
114
109
115
def rooms(request):
110
116
    template = "administration/rooms.djhtml"
111
117
    return render(request, template, context)
112
118
113
119
def room_reservate(request):
114
120
    return render(request, template, context)
115
121
116
122
def login(request):
117
123
    if request.method == "POST":
118
124
        name = request.POST['name']
119
125
        passphrase = request.POST['password']
120
126
        user = authenticate(username=name, password=passphrase)
121
127
        if user is not None: # The user was successfully authenticated.
122
128
            login(request, user)
123
129
            # Because of Leen, I now track when and where is logged in:
124
130
            loginRecord = Login()
125
131
            loginRecord.ip = request.META['REMOTE_ADDR']
126
132
            loginRecord.name = name
127
133
            loginRecord.save()
128
134
            return HttpResponseRedirect(reverse('ITdays-index'))
129
135
130
136
    template = 'administration/login.djhtml'
131
137
132
138
    footer_links = [
133
139
            ["Home", "/"],
134
140
            ["Contact", "mailto:maarten.vangeneugden@student.uhasselt.be"],
135
141
            ]
136
142
137
143
    context = {
138
144
            'materialDesign_color': "deep-purple",
139
145
            'materialDesign_accentColor': "amber",
140
146
            'navbar_title': "Authentication",
141
147
            'navbar_fixed': True,
142
148
            'navbar_backArrow': True,
143
149
            'footer_title': "Quotebook",
144
150
            'footer_description': "Een lijst van citaten uit 2BACH Informatica @ UHasselt.",
145
151
            'footer_links': footer_links,
146
152
            }
147
153
    return render(request, template, context)
148
154