joeni

Attempt to add student result management

There's a bug that I can't make the field of the student name non-editable, I have no idea how to fix it, and every attempt I made is met with another annoying bug that nobody on the internet seems to have. Nevertheless, should there be a fix anytime in the future, one line just needs to be uncommented and everything will be fine. For now, it's fingers crossed that the name isn't edited, even though it's in an editable field.

Author
Maarten Vangeneugden
Date
Aug. 24, 2018, 3:49 a.m.
Hash
1968b80302e3b9f87ccdaa1b9cebd8fe6eb8b09e
Parent
76b13992e2e6d9384992398daf7f4b9d869e7352
Modified files
administration/models.py
courses/forms.py
courses/templates/courses/course.djhtml
courses/templates/courses/course_results.djhtml
courses/views.py
joeni/templatetags/joeni_org.py

administration/models.py

1 addition and 1 deletion.

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

courses/forms.py

13 additions and 1 deletion.

View changes Hide changes
1
1
from django.forms import ModelForm, modelformset_factory
2
-
from . import models
+
2
from . import models
3
3
import administration
4
4
5
5
class AssignmentForm(ModelForm):
6
6
    class Meta:
7
7
        model = models.Assignment
8
8
        fields = ['title',
9
9
                  'information',
10
10
                  'deadline',
11
11
                  'digital_task',
12
12
                  ]
13
13
class AnnouncementForm(ModelForm):
14
14
    class Meta:
15
15
        model = models.Announcement
16
16
        fields = ['title',
17
17
                  'text',
18
18
                  ]
19
19
class UploadForm(ModelForm):
20
20
    class Meta:
21
21
        model = models.Upload
22
22
        fields = ['comment',
23
23
                  'file',
24
24
                  ]
25
25
class CourseItemForm(ModelForm):
26
26
    class Meta:
27
27
        model = models.CourseItem
28
28
        fields = ['file',
29
29
                  'note',
30
30
                  ]
31
31
32
32
AssignmentFormSet = modelformset_factory(
33
33
    models.Assignment, fields=('title', 'information', 'deadline', 'digital_task'),
34
34
    localized_fields="__all__",
35
35
    can_delete=True,
36
36
    )
37
37
AnnouncementFormSet = modelformset_factory(
38
38
    models.Announcement, fields=('title', 'text'),
39
39
    localized_fields="__all__",
40
40
    can_delete=True,
41
41
    )
42
42
CourseItemFormSet = modelformset_factory(
43
43
    models.CourseItem, fields=('file', 'note'),
44
44
    localized_fields="__all__",
45
45
    can_delete=True,
46
46
    )
47
47
CourseResultFormSet = modelformset_factory(
48
48
    administration.models.CourseResult, fields=('student', 'first_score', 'second_score', 'result'),
49
49
    localized_fields="__all__",
50
50
    can_delete=True,
+
51
    extra=0,
+
52
    # XXX: What about this commented widget?
+
53
    # It was supposed to be that the student couldn't be altered, but that per name
+
54
    # would be displayed in a disabled field. However Django for one reason or another
+
55
    # can't FOR THE LOVE OF GOD handle a changed widget like this. So it's disabled
+
56
    # and currently Joeni relies on the users to /not/ edit an editable field. Slick.
+
57
    #widgets={'student': TextInput(attrs={'disabled':'true', 'readonly':'readonly'})},
+
58
    )
+
59
UploadFormSet = modelformset_factory(
+
60
    models.Upload, fields=('file', 'comment'),
+
61
    localized_fields="__all__",
+
62
    can_delete=True,
51
63
    )
52
64

courses/templates/courses/course.djhtml

3 additions and 5 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
29
29
    <h2 id="{% trans "course-items" %}">{% trans "Course items" %}</h2>
30
30
    <div class="flex-container">
31
31
    {% for item in course_items %}
32
32
        <div style="border-color: #{{ course.color }};" class="flex-item">
33
33
            <a href="{{ item.file.url }}" download>{{ item.canonical }}</a><br />
34
34
            <time datetime="{{ item.timestamp|date:'c' }}">
35
35
                {% trans "Posted:" %} {{ item.timestamp|naturaltime }}
36
36
            </time>
37
37
            {% if item.note %}
38
38
            <p>{{ item.note|org }}</p>
39
39
            {% endif %}
40
40
        </div>
41
41
    {% empty %}
42
42
        {% trans "There is no course material available for this course." %}
43
43
    {% endfor %}
44
44
    </div>
45
45
46
46
47
47
    <h2 id="{% trans "assignments" %}">{% trans "Assignments" %}</h2>
48
48
    <div class="flex-container">
49
49
    {% for assignment in assignments %}
50
50
        <div style="border-color: #{{ course.color }};" class="flex-item">
51
51
            <h3 id="{{ assignment.title|slugify }}">{{ assignment.title }}</h3>
52
52
            <time datetime="{{ assignment.posted|date:'c' }}">
53
53
                {% trans "Posted:" %} {{ assignment.posted|date:"DATE_FORMAT" }} {# {{ assignment.posted|naturaltime }}#}
54
54
            </time><br />
55
55
            <time datetime="{{ assignment.deadline|date:'c' }}">
56
56
                {% trans "Deadline:" %} {{ assignment.deadline|date:"DATE_FORMAT" }}
57
57
            </time>
58
58
59
59
            {% if assignment.information %}
60
60
                <p>{{ assignment.information|org }}</p>
61
61
            {% endif %}
62
62
            {#{% trans "Posted" %}: {{ assignment.posted|date:"DATE_FORMAT" }}#}
63
63
            {% if assignment.digital_task %}
64
64
                <h4>{% trans "Your uploads" %}</h4>
65
65
                {% for upload in uploads %}
66
66
                    {% if upload.assignment ==  assignment %}
67
67
                        {% trans "Uploaded:"%} {{ upload.upload_time|date:"SHORT_DATETIME_FORMAT" }}<br />
68
68
                        {% if upload.comment %}
69
69
                            <p>{{ upload.comment }}</p>
70
70
                        {% endif %}
71
71
                        {% if upload.upload_time > assignment.deadline %}
72
72
                            <strong>{% trans "This upload is overdue." %}</strong>
73
73
                        {% endif %}
74
74
                    {% endif %}
75
75
                {% empty %}
76
76
                    {% with now as current_time %}
77
77
                    {% if current_time > assignment.deadline %}
78
78
                        <p>
79
79
                            <strong>
80
80
                                {% blocktrans %}
81
81
                                    You have failed to provide an upload for this
82
82
                                    assignment. Any future uploads will be automatically
83
83
                                    overdue.
84
84
                                {% endblocktrans %}
85
85
                            </strong>
86
86
                        </p>
87
87
                    {% else %}
88
88
                        <p>
89
89
                            {% blocktrans %}
90
90
                                You haven't uploaded anything for this assignment
91
91
                                yet.
92
92
                            {% endblocktrans %}
93
93
                        </p>
94
94
                    {% endif %}
95
95
                    {% endwith %}
96
96
                {% endfor %}
97
97
                <h5>{% trans "Upload a task" %}</h5>
98
98
                <form action="{% url "courses-course-index" course.slug_name %}" method="post">
99
-
                    {% csrf_token %} {# todo i don't think that's necessary here #}
+
99
                    {% csrf_token %} {# todo i don't think that's necessary here #}
100
100
                    {% include "joeni/form.djhtml" with form=upload_form %}
101
101
                    <input type="submit" value="{% trans "Submit" %}" />
102
-
                </form>
+
102
                    <!--<input type="submit" value="{% trans "Submit" %}" />-->
+
103
                </form>
103
104
            {% endif %}
104
105
        </div>
105
106
    {% endfor %}
106
107
    </div>
107
108
    <h1 id="{% trans "management" %}">{% trans "Course management" %}</h1>
108
109
    <style>
109
110
        a.btn {
110
111
        color: #{{ course.color }};
111
112
        border-color: #{{ course.color }};
112
113
        }
113
114
        a.btn:hover {
114
115
        color: white;
115
116
        background-color: #{{ course.color }};
116
117
        }
117
118
    </style>
118
119
    <a class="btn" href="{% url "courses-eci" course_slug=course.slug_name %}">
119
120
        {% trans "Edit course page" %}
120
121
    </a>
121
122
    {% comment %}
122
-
    <a class="btn" href="{% url "courses-results" course_slug=course.slug_name %}">
123
123
        {% trans "Student results" %}
124
124
    </a>
125
125
    Uncomment this section when the course results page is finished.
126
-
    {% endcomment %}
127
-
    <h2 id="{% trans "students" %}">{% trans "Students" %}</h2>
128
126
    <table>
129
127
        <tr>
130
128
            <th>{% trans "Student name" %}</th>
131
129
            <th>{% trans "Student number" %}</th>
132
130
            <th>{% trans "First result" %}</th>
133
131
            <th>{% trans "Second result" %}</th>
134
132
            <th>{% trans "Decision" %}</th>
135
133
        </tr>
136
134
            {% for student in student_list %}
137
135
                <tr>
138
136
                <td>{{ student.student }}</td>
139
137
                <td>{{ student.student.number }}</td>
140
138
                <td>{{ student.first_score|default_if_none:"-" }}</td>
141
139
                <td>{{ student.second_score|default_if_none:"-" }}</td>
142
140
                <td>{% with result=student.result %}
143
141
                    {% if result == "CRED" or result == "VRST" or result == "TLRD" or result == "ITLR"%}
144
142
                        <span style="color:green;">
145
143
                    {% elif result == "FAIL" %}
146
144
                        <span style="color:red;">
147
145
                    {% elif result == "BDRG" %}
148
146
                        <span style="background-color:red; color:white;">
149
147
                    {% elif result == "STOP" %}
150
148
                        <span style="color:black;">
151
149
                    {% endif %}
152
150
                    {{ student.get_result_display }}</span>
153
151
                {% endwith %}</td>
154
152
        </tr>
155
153
        {% endfor %}
156
154
        </table>
157
155
{% endblock main %}
158
156

courses/templates/courses/course_results.djhtml

5 additions and 20 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
    <style>
11
11
    div.display {
12
12
         }
13
13
    div.print {
14
14
        }
15
15
    </style>
16
16
17
17
    <h1>{% trans "Student result management" %}</h1>
18
18
<!--
19
-
    <form action="" method="POST">
20
19
        {% csrf_token %}
21
20
        <h2 id="{% trans "announcements" %}">{% trans "Announcements" %}</h2>
22
-
            <table>
23
-
                {{ announcements }}
24
-
            </table>
25
-
        <h2 id="{% trans "assignments" %}">{% trans "Assignments" %}</h2>
26
-
            <table>
27
-
            {{ assignments }}
28
-
            </table>
29
-
        <h2 id="{% trans "course-items" %}">{% trans "Course items" %}</h2>
30
-
            <table>
31
-
                {{ course_items }}
32
-
            </table>
33
-
        <h2 id="{% trans "students" %}">{% trans "Students" %}</h2>
34
-
            <table>
35
-
                {{ course_results }}
36
-
            </table>
37
-
        <hr />
+
21
            {{ course_results }}
+
22
        </table>
+
23
        <hr />
38
24
        <input type="submit" value="{% trans "Save everything" %}" />
39
-
    </form>-->
40
-
41
-
{% endblock main %}
+
25
    </form>
+
26
{% endblock main %}
42
27

courses/views.py

40 additions and 10 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
        uploads = UploadFormSet(request.POST, request.FILES, prefix='uploads')
+
35
        if uploads.is_valid():
+
36
            uploads.save(commit=False)
+
37
            for new_upload in uploads.new_objects:
+
38
                new_upload.course = course_
+
39
            uploads.save()
+
40
            request.method = 'GET'
+
41
            return course(request, course_slug)
+
42
    else:
+
43
        uploads = UploadFormSet(
+
44
            queryset=Upload.objects.filter(course=course_).filter(student=request.user),
+
45
            prefix="uploads",
+
46
            )
+
47
    # Check if user can see this page
34
48
    if request.user.user_data.is_student:
35
49
        if course not in request.user.user_data.current_courses():
36
-
            """ I'm currently just redirecting to the index page, but maybe it's
+
50
            """ I'm currently just redirecting to the index page, but maybe it's
37
51
            just as good to make an announcement that this course cannot be
38
52
            used by this user. """
39
53
            return index(request)
40
54
41
55
    context = {
42
56
        'course': course,
43
-
        'main_color': course.color,
44
-
        'announcements': Announcement.objects.filter(course=course),
45
-
        'assignments': Assignment.objects.filter(course=course),
46
-
        'course_items': CourseItem.objects.filter(course=course),
47
-
        'study-groups': StudyGroup.objects.filter(course=course),
48
-
        'uploads': Upload.objects.filter(course=course).filter(student=request.user)
49
-
        }
+
57
        'main_color': course_.color,
+
58
        'announcements': Announcement.objects.filter(course=course_),
+
59
        'assignments': Assignment.objects.filter(course=course_),
+
60
        'course_items': CourseItem.objects.filter(course=course_),
+
61
        'study-groups': StudyGroup.objects.filter(course=course_),
+
62
        #'uploads': Upload.objects.filter(course=course).filter(student=request.user)
+
63
        }
50
64
    if request.user.user_data.is_student:
51
65
        context['upload_form'] = UploadForm()
52
66
    #else:
53
67
        context['student_list'] = administration.models.CourseResult.objects.filter(course_programme__course=course)#.filter(year=current_academic_year()),
54
-
        # FIXME I disabled the year filter for testing purposes. Enable in deployment.
+
68
        # FIXME I disabled the year filter for testing purposes. Enable in deployment.
55
69
56
70
    return render(request, template, context)
57
71
58
72
# TODO: Find a way to see if it's possible to require some permissions and to
59
73
# put them in a decorator
60
74
#@permission_required
61
75
@login_required
62
76
def new_item(request, course_slug):
63
77
    template = "courses/new_item.djhtml"
64
78
    course = Course.objects.get(slug_name=course_slug)
65
79
66
80
    if request.user.user_data.is_student or request.user not in course.course_team:
67
81
        # Students can't add new items. Redirect to index
68
82
        # Also redirect people who are not part of the course team
69
83
        redirect('courses-index')
70
84
    # Now able to assume user is allowed to add items to this course
71
85
72
86
    context = {
73
87
        'course': course,
74
88
        'announcements': Announcement.objects.filter(course=course),
75
89
        'assignments': Assignment.objects.filter(course=course),
76
90
        'course-items': CourseItem.objects.filter(course=course),
77
91
        'study-groups': StudyGroup.objects.filter(course=course),
78
92
        'uploads': Upload.objects.filter(course=course)
79
93
        }
80
94
    return render(request, template, context)
81
95
82
96
def upload(request):
83
97
    return render(request, template, context)
84
98
85
99
#@create_context  # TODO
86
100
@login_required
87
101
def course_results(request, course_slug):
88
102
    template = "courses/course_results.djhtml"
89
103
    context = dict()
90
104
    course_ = Course.objects.get(slug_name=course_slug)
91
105
    return render(request, template, context)
+
106
        course_results = CourseResultFormSet(request.POST, prefix='course_results')
+
107
        if course_results.is_valid():
+
108
            course_results.save()
+
109
            request.method = 'GET'
+
110
            return course(request, course_slug)
+
111
    else:
+
112
        course_results = CourseResultFormSet(
+
113
            #queryset=administration.models.CourseResult.objects.filter(course_programme__course=course_).filter(year=current_academic_year()),
+
114
            queryset=administration.models.CourseResult.objects.filter(course_programme__course=course_),
+
115
            prefix="course_results",
+
116
            )
+
117
+
118
    context['course'] = course_
+
119
    context['course_results'] = course_results
+
120
    return render(request, template, context)
92
121
93
122
@login_required
94
123
def edit_course_items(request, course_slug):
95
124
    # TODO Only allow people on the course team to this page!
96
125
    template = "courses/edit_course_items.djhtml"
97
126
    context = dict()
98
127
    course_ = Course.objects.get(slug_name=course_slug)
99
128
    if request.method == 'POST':
100
129
        assignments = AssignmentFormSet(request.POST, prefix='assignments')
101
130
        announcements = AnnouncementFormSet(request.POST, prefix='announcements')
102
131
        course_items = CourseItemFormSet(request.POST, request.FILES, prefix='course_items')
103
132
        #course_results = CourseResultFormSet(request.POST, prefix='course_results')
104
133
        if assignments.is_valid() and announcements.is_valid() and course_items.is_valid(): #and course_results.is_valid():
105
134
            assignments.save(commit=False)
106
135
            announcements.save(commit=False)
107
136
            course_items.save(commit=False)
108
137
            #course_results.save(commit=False)
109
138
            for new_assignment in assignments.new_objects:
110
139
                new_assignment.course = course_
111
140
            for new_announcement in announcements.new_objects:
112
141
                new_announcement.course = course_
113
142
            for new_course_item in course_items.new_objects:
114
143
                new_course_item.course = course_
115
144
            #for new_course_result in course_results.new_objects:
116
145
                #new_coutse_result.course = course_
117
146
            assignments.save()
118
147
            announcements.save()
119
148
            course_items.save()
120
149
            #course_results.save()
121
150
            return course(request, course_slug)
+
151
            return course(request, course_slug)
122
152
    else:
123
153
        assignments = AssignmentFormSet(
124
154
            queryset=Assignment.objects.filter(course=course_),
125
155
            prefix="assignments",
126
156
            )
127
157
        announcements = AnnouncementFormSet(
128
158
            queryset=Announcement.objects.filter(course=course_),
129
159
            prefix="announcements",
130
160
            )
131
161
        course_items = CourseItemFormSet(
132
162
            queryset=CourseItem.objects.filter(course=course_),
133
163
            prefix="course_items",
134
164
            )
135
165
        #course_results = CourseResultFormSet(
136
166
            #queryset=administration.models.CourseResult.objects.filter(course_programme__course=course_).filter(year=current_academic_year()),
137
167
            #prefix="course_results",
138
168
            #)
139
169
140
170
    context['course'] = course_
141
171
    context['assignments'] = assignments
142
172
    context['announcements'] = announcements
143
173
    context['course_items'] = course_items
144
174
    #context['course_results'] = course_results
145
175
    return render(request, template, context)
146
176
147
177
148
178
149
179
@login_required
150
180
def groups(request):
151
181
    pass
152
182
153
183
def fiche(request, course_slug):
154
184
    """Displays the fiche for the given course. Includes information about all
155
185
    course programs."""
156
186
    template = "courses/fiche.djhtml"
157
187
    context = dict()
158
188
    course = Course.objects.get(slug_name=course_slug)
159
189

joeni/templatetags/joeni_org.py

1 addition and 0 deletions.

View changes Hide changes
1
1
from django.utils.safestring import mark_safe
2
2
import subprocess  # To call Pandoc
3
3
import requests
4
4
5
5
register = template.Library()
6
6
7
7
@register.filter
8
8
def org(value):
9
9
    """ Takes an input, and transpiles it as org-mode syntax to HTML syntax. """
10
10
    # TODO Write bug handling code and exception handling
11
11
    f = open('/tmp/django-temp.org', 'w')
12
12
    f.write(value)
13
13
14
14
    f.close()
15
15
    f = open('/tmp/django-temp.org', 'r')
16
16
    #f2 = open('/tmp/django-output.html', 'w+')
17
17
    subprocess.run(["pandoc", "--from=org", "--to=html", "-o" "/tmp/django-output.html"], stdin=f)
18
18
    #f2.close()
19
19
    f2 = open('/tmp/django-output.html', 'r')
20
20
    result = f2.read()
21
21
    f.close()
22
22
    f2.close()
23
23
    return mark_safe(result)
24
24
    #print("OK!")
25
25
    #return mark_safe(subprocess.check_output(["iconv", "-t", "utf-8", "/tmp/django-temp.org", "|", "pandoc", "--from=org", "--to=html"]))#, "/tmp/django-temp.org"]))
26
26
#return mark_safe(subprocess.check_output(["pandoc", "--from=org", "--to=html", value]))
27
27
28
28
@register.simple_tag
29
29
def pingping():
30
30
    link = "https://uhasselt-pxl.mynetpay.be/Account/Login"
+
31
    link = "https://uhasselt-pxl.mynetpay.be/Account/Login"
31
32
    # Get CSRF token
32
33
    first_call = requests.get(link)
33
34
    text = first_call.text
34
35
    begin = text.find('__RequestVerificationToken')
35
36
    begin = text.find('value="', begin)
36
37
    end = text.find('" ', begin)
37
38
    token = text[begin+len('value="'):end]
38
39
    cookies = first_call.cookies
39
40
40
41
    username = ""
41
42
    password = ""
42
43
43
44
    response = requests.post(link, data={
44
45
        'Username':username,
45
46
        'LoginType':'Student',
46
47
        'Password':password,
47
48
        '__RequestVerificationToken':token,
48
49
        }, cookies=cookies)
49
50
50
51
    html_response = response.text
51
52
    start = html_response.find(": &euro; ")
52
53
    offset = len(": &euro; ")
53
54
    return html_response[start+offset:start+offset+5]
54
55