joeni

Split result management and course page management

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

administration/templates/administration/roster.djhtml

3 additions and 0 deletions.

View changes Hide changes
1
1
{% cycle "hour" "first quarter" "half" "last quarter" as hour silent %}
2
2
{# "silent" blocks the cycle operator from printing the cycler, and in subsequent calls #}
3
3
{% load i18n %}
4
4
5
5
{% block title %}
6
6
    {% trans "Roster" %} | {{ block.super }}
7
7
{% endblock %}
8
8
9
9
{% block main %}
10
10
    <h1>{% trans "Personal timetable" %}</h1>
11
11
    <h2>{% trans "Main hour roster" %}</h2>
12
12
13
13
14
14
    {% include "administration/roster_t.djhtml" %}
15
15
16
16
17
17
        <a class="btn" href="{% url "administration-roster" begin=prev_begin end=prev_end %}">
18
18
            {% trans "Previous week" %}
19
19
        </a>
20
20
        <a class="btn" href="{% url "administration-roster" %}">
21
21
            {% trans "Current week" %}
22
22
        </a>
23
23
        <a class="btn" href="{% url "administration-roster" begin=next_begin end=next_end %}">
24
24
            {% trans "Next week" %}
25
25
        </a>
26
26
        {# TODO: Add links to "previous week", "next week" and "current week" with large buttons #}
+
27
            {% trans "ICS file" %}
+
28
        </a>
+
29
        {# TODO: Add links to "previous week", "next week" and "current week" with large buttons #}
27
30
28
31
    <h2>{% trans "Explanation" %}</h2>
29
32
    <p>
30
33
        {% trans "Personal roster from" %} {{ begin|date }} {% trans "to" %} {{ end|date }}
31
34
    </p>
32
35
    <p>
33
36
        {% blocktrans %}
34
37
            Some fields may have additional information that might be of interest
35
38
            to you. This information is shown in different ways with colour codes.
36
39
        {% endblocktrans %}
37
40
    </p>
38
41
39
42
    <dl>
40
43
        <dt><span class="event-update">
41
44
            {% trans "Recent event update" %}
42
45
        </span></dt>
43
46
        <dd>
44
47
            {% blocktrans %}
45
48
                This event had one or more of its properties changed
46
49
                in the last five days. This can be the room, the hours, the subject, ...
47
50
                You're encouraged to take note of that.
48
51
            {% endblocktrans %}
49
52
        </dd>
50
53
        <dt><span class="event-new">
51
54
            {% trans "New event" %}
52
55
        </span></dt>
53
56
        <dd>
54
57
            {% blocktrans %}
55
58
                This is a new event, added in the last five days.
56
59
            {% endblocktrans %}
57
60
        </dd>
58
61
        <dt><span class="event-note">
59
62
            {% trans "Notification available" %}
60
63
        </span></dt>
61
64
        <dd>
62
65
            {% blocktrans %}
63
66
                This event has a note attached to it by the docent. Hover over
64
67
                the event to display the note.
65
68
            {% endblocktrans %}
66
69
        </dd>
67
70
    </dl>
68
71
69
72
{% endblock main %}
70
73

courses/models.py

9 additions and 0 deletions.

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

courses/templates/courses/course.djhtml

39 additions and 3 deletions.

View changes Hide changes
1
1
{% load static %}
2
2
{% load i18n %}
3
3
{% load humanize %}
4
4
{% load joeni_org %}
5
5
6
6
{% block title %}
7
7
    {{ course.name }} | {{ block.super }}
8
8
{% endblock %}
9
9
10
10
{% block main %}
11
11
    <h1>{{ course.name }}</h1>
12
12
13
13
    <h2 id="{% trans "announcements" %}">{% trans "Announcements" %}</h2>
14
14
    <div class="flex-container">
15
15
    {% for announcement in announcements %}
16
16
        <div style="border-color: #{{ course.color }};" class="flex-item">
17
17
            <h3 id="{{ announcement.title|slugify }}">{{ announcement.title }}</h3>
18
18
            <time datetime="{{ announcement.posted|date:'c' }}">
19
19
                {% trans "Posted" %} {{ announcement.posted|naturaltime }}
20
20
            </time>
21
21
            <p>{{ announcement.text|org }}</p>
22
22
        </div>
23
23
    {% empty %}
24
24
        {% trans "No announcements have been made for this course." %}
25
25
    {% endfor %}
26
26
    </div>
27
27
28
28
    <h2 id="{% trans "assignments" %}">{% trans "Assignments" %}</h2>
29
29
    <div class="flex-container">
30
30
    {% for assignment in assignments %}
31
31
        <div style="border-color: #{{ course.color }};" class="flex-item">
32
32
            <h3 id="{{ assignment.title|slugify }}">{{ assignment.title }}</h3>
33
33
            <time datetime="{{ assignment.posted|date:'c' }}">
34
34
                {% trans "Posted" %} {{ assignment.posted|naturaltime }}
35
-
            </time>
+
35
            </time>
36
36
            {% if assignment.information %}
37
37
                <p>{{ assignment.information|org }}</p>
38
38
            {% endif %}
39
39
            {#{% trans "Posted" %}: {{ assignment.posted|naturaltime }}#}
40
-
            {% trans "Posted" %}: {{ assignment.posted|date:"DATE_FORMAT" }}
41
-
            {% if assignment.digital_task %}
+
40
            {% if assignment.digital_task %}
42
41
                <h4>{% trans "Your uploads" %}</h4>
43
42
                {% for upload in uploads %}
44
43
                    {% if upload.assignment ==  assignment %}
45
44
                        {% trans "Uploaded:"%} {{ upload.upload_time|date:"SHORT_DATETIME_FORMAT" }}<br />
46
45
                        {% if upload.comment %}
47
46
                            <p>{{ upload.comment }}</p>
48
47
                        {% endif %}
49
48
                        {% if upload.upload_time > assignment.deadline %}
50
49
                            <strong>{% trans "This upload is overdue." %}</strong>
51
50
                        {% endif %}
52
51
                    {% endif %}
53
52
                {% empty %}
54
53
                    {% with now as current_time %}
55
54
                    {% if current_time > assignment.deadline %}
56
55
                        <p>
57
56
                            <strong>
58
57
                                {% blocktrans %}
59
58
                                    You have failed to provide an upload for this
60
59
                                    assignment. Any future uploads will be automatically
61
60
                                    overdue.
62
61
                                {% endblocktrans %}
63
62
                            </strong>
64
63
                        </p>
65
64
                    {% else %}
66
65
                        <p>
67
66
                            {% blocktrans %}
68
67
                                You haven't uploaded anything for this assignment
69
68
                                yet.
70
69
                            {% endblocktrans %}
71
70
                        </p>
72
71
                    {% endif %}
73
72
                    {% endwith %}
74
73
                {% endfor %}
75
74
                <h5>{% trans "Upload a task" %}</h5>
76
75
                <form action="{% url "courses-course-index" course.slug_name %}" method="post">
77
76
                    {% csrf_token %} {# todo i don't think that's necessary here #}
78
77
                    {% include "joeni/form.djhtml" with form=upload_form %}
79
78
                    <input type="submit" value="{% trans "Submit" %}" />
80
79
                </form>
81
80
            {% endif %}
82
81
        </div>
83
82
    {% endfor %}
84
83
    </div>
85
84
{% endblock main %}
+
85
    <a class="btn" href="{% url "courses-eci" course_slug=course.slug_name %}">
+
86
        {% trans "Edit course page" %}
+
87
    </a>
+
88
    <a class="btn" href="{% url "courses-results" course_slug=course.slug_name %}">
+
89
        {% trans "Student results" %}
+
90
    </a>
+
91
    <h2 id="{% trans "students" %}">{% trans "Students" %}</h2>
+
92
    <table>
+
93
        <tr>
+
94
            <th>{% trans "Student name" %}</th>
+
95
            <th>{% trans "Student number" %}</th>
+
96
            <th>{% trans "First result" %}</th>
+
97
            <th>{% trans "Second result" %}</th>
+
98
            <th>{% trans "Decision" %}</th>
+
99
        </tr>
+
100
            {% for student in student_list %}
+
101
                <tr>
+
102
                <td>{{ student.student }}</td>
+
103
                <td>{{ student.student.number }}</td>
+
104
                <td>{{ course_result.first_score|default:"-" }}</td>
+
105
                <td>{{ course_result.second_score|default:"-" }}</td>
+
106
                <td>{% with result=course_result.result %}
+
107
                    {% if result == "CRED" or result == "VRST" or result == "TLRD" or result == "ITLR"%}
+
108
                        <span style="color:green;">
+
109
                    {% elif result == "FAIL" %}
+
110
                        <span style="color:red;">
+
111
                    {% elif result == "BDRG" %}
+
112
                        <span style="background-color:red; color:white;">
+
113
                    {% elif result == "STOP" %}
+
114
                        <span style="color:black;">
+
115
                    {% endif %}
+
116
                    {{ course_result.get_result_display }}</span>
+
117
                {% endwith %}</td>
+
118
        </tr>
+
119
        {% endfor %}
+
120
        </table>
+
121
{% endblock main %}
86
122

courses/templates/courses/course_results.djhtml

33 additions and 0 deletions.

View changes Hide changes
+
1
{% load static %}
+
2
{% load i18n %}
+
3
{% load joeni_org %}
+
4
+
5
{% block title %}
+
6
    {{ course.name }} | {{ block.super }}
+
7
{% endblock %}
+
8
+
9
{% block main %}
+
10
<!--
+
11
    <form action="" method="POST">
+
12
        {% csrf_token %}
+
13
        <h2 id="{% trans "announcements" %}">{% trans "Announcements" %}</h2>
+
14
            <table>
+
15
                {{ announcements }}
+
16
            </table>
+
17
        <h2 id="{% trans "assignments" %}">{% trans "Assignments" %}</h2>
+
18
            <table>
+
19
            {{ assignments }}
+
20
            </table>
+
21
        <h2 id="{% trans "course-items" %}">{% trans "Course items" %}</h2>
+
22
            <table>
+
23
                {{ course_items }}
+
24
            </table>
+
25
        <h2 id="{% trans "students" %}">{% trans "Students" %}</h2>
+
26
            <table>
+
27
                {{ course_results }}
+
28
            </table>
+
29
        <hr />
+
30
        <input type="submit" value="{% trans "Save everything" %}" />
+
31
    </form>-->
+
32
{% endblock main %}
+
33

courses/templates/courses/edit_course_items.djhtml

2 additions and 0 deletions.

View changes Hide changes
1
1
{% load static %}
2
2
{% load i18n %}
3
3
{% load joeni_org %}
4
4
5
5
{% block title %}
6
6
    {{ course.name }} | {{ block.super }}
7
7
{% endblock %}
8
8
9
9
{% block main %}
10
10
    <p>
11
11
        {% blocktrans %}You can edit announcements, assignments, and course
12
12
            items on this page, mark them for deletion, and add new ones.
13
13
            When all changes are saved successfully, you will be redirected to
14
14
            the course's main page. When errors occur, you will stay on this page,
15
15
            and receive error notifications where necessary.{% endblocktrans %}
16
16
    </p>
17
17
18
18
    <form action="" method="POST">
19
19
        {% csrf_token %}
20
20
        <h2 id="{% trans "announcements" %}">{% trans "Announcements" %}</h2>
21
21
            <table>
22
22
                {{ announcements }}
23
23
            </table>
24
24
        <h2 id="{% trans "assignments" %}">{% trans "Assignments" %}</h2>
25
25
            <table>
26
26
            {{ assignments }}
27
27
            </table>
28
28
        <h2 id="{% trans "course-items" %}">{% trans "Course items" %}</h2>
29
29
            <table>
30
30
                {{ course_items }}
31
31
            </table>
32
32
        <h2 id="{% trans "students" %}">{% trans "Students" %}</h2>
+
33
        <h2 id="{% trans "students" %}">{% trans "Students" %}</h2>
33
34
            <table>
34
35
                {{ course_results }}
35
36
            </table>
36
37
        <hr />
+
38
        <hr />
37
39
        <input type="submit" value="{% trans "Save everything" %}" />
38
40
    </form>
39
41
{% endblock main %}
40
42

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
    path(_('<slug:course_slug>/edit-course-items'), views.edit_course_items, name='courses-eci'),
11
11
    ]
+
12
    ]
12
13

courses/views.py

25 additions and 11 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
from joeni.constants import current_academic_year
9
9
10
10
@login_required
11
11
def index(request):
12
12
    """ Starting page regarding the courses. This serves two specific groups:
13
13
    - Students: Displays all courses that this student has in his/her curriculum
14
14
                for this academic year. Requires the curriculum to be accepted.
15
15
    - Staff: Displays all courses in which the staff member is part of the
16
16
             educating team, or is otherwise related to the course.
17
17
    Users who are not logged in will be sent to the login page.
18
18
    """
19
19
    template = "courses/index.djhtml"
20
20
    courses = request.user.user_data.current_courses()
21
21
22
22
    context = {
23
23
        'courses': courses,
24
24
        }
25
25
26
26
    return render(request, template, context)
27
27
28
28
@login_required
29
29
def course(request, course_slug):
30
30
    template = "courses/course.djhtml"
31
31
    course = Course.objects.get(slug_name=course_slug)
32
32
33
33
    # Check if user can see this page
34
34
    if request.user.user_data.is_student:
35
35
        if course not in request.user.user_data.current_courses():
36
36
            """ I'm currently just redirecting to the index page, but maybe it's
37
37
            just as good to make an announcement that this course cannot be
38
38
            used by this user. """
39
39
            return index(request)
40
40
41
41
    context = {
42
42
        'course': course,
43
43
        'main_color': course.color,
44
44
        'announcements': Announcement.objects.filter(course=course),
45
45
        'assignments': Assignment.objects.filter(course=course),
46
46
        'course-items': CourseItem.objects.filter(course=course),
47
47
        'study-groups': StudyGroup.objects.filter(course=course),
48
48
        'uploads': Upload.objects.filter(course=course).filter(student=request.user)
49
49
        }
50
50
    if request.user.user_data.is_student:
51
51
        context['upload_form'] = UploadForm()
52
52
+
53
        #context['student_list'] = administration.models.CourseResult.objects.filter(course_programme__course=course).filter(year=current_academic_year()),
+
54
        context['student_list'] = administration.models.CourseResult.objects.all()
+
55
53
56
    return render(request, template, context)
54
57
55
58
# TODO: Find a way to see if it's possible to require some permissions and to
56
59
# put them in a decorator
57
60
#@permission_required
58
61
@login_required
59
62
def new_item(request, course_slug):
60
63
    template = "courses/new_item.djhtml"
61
64
    course = Course.objects.get(slug_name=course_slug)
62
65
63
66
    if request.user.user_data.is_student or request.user not in course.course_team:
64
67
        # Students can't add new items. Redirect to index
65
68
        # Also redirect people who are not part of the course team
66
69
        redirect('courses-index')
67
70
    # Now able to assume user is allowed to add items to this course
68
71
69
72
    context = {
70
73
        'course': course,
71
74
        'announcements': Announcement.objects.filter(course=course),
72
75
        'assignments': Assignment.objects.filter(course=course),
73
76
        'course-items': CourseItem.objects.filter(course=course),
74
77
        'study-groups': StudyGroup.objects.filter(course=course),
75
78
        'uploads': Upload.objects.filter(course=course)
76
79
        }
77
80
+
81
78
82
    return render(request, template, context)
+
83
    return render(request, template, context)
79
84
80
85
#@create_context  # TODO
81
86
@login_required
+
87
def course_results(request, course_slug):
+
88
    template = "courses/course_results.djhtml"
+
89
    context = dict()
+
90
    course_ = Course.objects.get(slug_name=course_slug)
+
91
    return render(request, template, context)
+
92
+
93
@login_required
82
94
def edit_course_items(request, course_slug):
83
95
    # TODO Only allow people on the course team to this page!
84
96
    template = "courses/edit_course_items.djhtml"
85
97
    context = dict()
86
98
    course_ = Course.objects.get(slug_name=course_slug)
87
99
    if request.method == 'POST':
88
100
        assignments = AssignmentFormSet(request.POST, prefix='assignments')
89
101
        announcements = AnnouncementFormSet(request.POST, prefix='announcements')
90
102
        course_items = CourseItemFormSet(request.POST, request.FILES, prefix='course_items')
91
103
        course_results = CourseResultsFormSet(request.POST, prefix='course_results')
92
-
        if assignments.is_valid() and announcements.is_valid() and course_items.is_valid() and course_results.is_valid():
93
-
            assignments.save(commit=False)
+
104
        if assignments.is_valid() and announcements.is_valid() and course_items.is_valid(): #and course_results.is_valid():
+
105
            assignments.save(commit=False)
94
106
            announcements.save(commit=False)
95
107
            course_items.save(commit=False)
96
108
            course_results.save(commit=False)
97
-
            for new_assignment in assignments.new_objects:
+
109
            for new_assignment in assignments.new_objects:
98
110
                new_assignment.course = course_
99
111
            for new_announcement in announcements.new_objects:
100
112
                new_announcement.course = course_
101
113
            for new_course_item in course_items.new_objects:
102
114
                new_coutse_item.course = course_
103
115
            for new_course_result in course_results.new_objects:
104
-
                new_coutse_result.course = course_
105
-
            assignments.save()
+
116
                #new_coutse_result.course = course_
+
117
            assignments.save()
106
118
            announcements.save()
107
119
            course_items.save()
108
120
            course_results.save()
109
-
            return course(request, course_slug)
+
121
            return course(request, course_slug)
110
122
    else:
111
123
        assignments = AssignmentFormSet(
112
124
            queryset=Assignment.objects.filter(course=course_),
113
125
            prefix="assignments",
114
126
            )
115
127
        announcements = AnnouncementFormSet(
116
128
            queryset=Announcement.objects.filter(course=course_),
117
129
            prefix="announcements",
118
130
            )
119
131
        course_items = CourseItemFormSet(
120
132
            queryset=CourseItem.objects.filter(course=course_),
121
133
            prefix="course_items",
122
134
            )
123
135
        course_results = CourseResultFormSet(
124
-
            queryset=CourseResult.objects.filter(course=course_).filter(year=current_academic_year()),
125
-
            prefix="course_results",
126
-
            )
127
-
    context['assignments'] = assignments
+
136
            #queryset=administration.models.CourseResult.objects.filter(course_programme__course=course_).filter(year=current_academic_year()),
+
137
            #prefix="course_results",
+
138
            #)
+
139
+
140
    context['course'] = course_
+
141
    context['assignments'] = assignments
128
142
    context['announcements'] = announcements
129
143
    context['course_items'] = course_items
130
144
    context['course_results'] = course_results
131
-
    return render(request, template, context)
+
145
    return render(request, template, context)
132
146
133
147
134
148
135
149
@login_required
136
150
def groups(request):
137
151
    pass
138
152
139
153
def fiche(request, course_slug):
140
154
    """Displays the fiche for the given course. Includes information about all
141
155
    course programs."""
142
156
    template = "courses/fiche.djhtml"
143
157
    context = dict()
144
158
    course = Course.objects.get(slug_name=course_slug)
145
159