joeni

Add changes for course editing and management

It's not possible right now to add scores/results for students, but this problem should be handled in another way.

Author
Maarten Vangeneugden
Date
April 18, 2018, 6:41 p.m.
Hash
197e97b68f091dd0c249af588da899a26395dee6
Parent
7876160a509d480e11c2f0c9587aeea3abf65aaf
Modified files
administration/models.py
courses/forms.py
courses/templates/courses/edit_course_items.djhtml
courses/views.py

administration/models.py

4 additions and 1 deletion.

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
294
class CourseResult(models.Model):
295
295
    """ A student has to obtain a certain course result. These are stored here,
296
296
    together with all the appropriate information. """
297
297
    # TODO: Validate that a course programme for a student can only be made once per year for each course, if possible.
298
298
    CRED = _("Credit acquired")
299
299
    FAIL = _("Credit not acquired")
300
300
    TLRD = _("Tolerated")
301
301
    ITLR = _("Tolerance used")
302
302
    BDRG = _("Fraud committed")
303
303
    VRST = _("Exemption")
304
304
    STOP = _("Course cancelled")
305
305
    # Possible to add more in the future
+
306
    # Possible to add more in the future
306
307
307
308
    student = models.ForeignKey(
308
309
        "User",
309
310
        on_delete=models.CASCADE,
310
311
        #limit_choices_to={'is_student': True},
311
312
        null=False,
312
313
        db_index=True,
313
314
        )
314
315
    course_programme = models.ForeignKey(
315
316
        "courses.CourseProgramme",
316
317
        on_delete=models.PROTECT,
317
318
        null=False,
318
319
        )
319
320
    year = models.PositiveIntegerField(
320
321
        null=False,
321
322
        default=datetime.date.today().year,
322
323
        help_text=_("The academic year this course took place in. If 2018 is entered, "
323
324
                    "then that means academic year '2018-2019'."),
324
325
        )
325
326
    released = models.DateField(
326
327
        auto_now=True,
327
328
        help_text=_("The date that this result was last updated."),
328
329
        )
329
330
    first_score = models.PositiveSmallIntegerField(
330
331
        null=True,  # It's possible a score does not exist.
331
332
        blank=True,
332
333
        validators=[MaxValueValidator(
333
334
            20,
334
335
            _("The score mustn't be higher than 20."),
335
336
            )],
336
337
        )
337
338
    second_score = models.PositiveSmallIntegerField(
338
339
        null=True,
339
340
        blank=True,
340
341
        validators=[MaxValueValidator(
341
342
            20,
342
343
            _("The score mustn't be higher than 20."),
343
344
            )],
344
345
        )
345
346
    result = models.CharField(
346
347
        max_length=10,
347
348
        choices = (
348
349
            ("CRED", CRED),
349
350
            ("FAIL", FAIL),
350
351
            ("TLRD", TLRD),
351
352
            ("ITLR", ITLR),
352
353
            ("BDRG", BDRG),
353
354
            ("VRST", VRST),
354
355
            ("STOP", STOP),
355
356
            ),
+
357
            ),
356
358
        blank=False,
357
359
        help_text=_("The final result this record constitutes."),
358
-
        )
+
360
        help_text=_("The result this record constitutes."),
+
361
        )
359
362
360
363
    def __str__(self):
361
364
        stdnum = str(self.student.number)
362
365
        result = self.result
363
366
        if result == "CRED":
364
367
            if self.first_score < 10:
365
368
                result = "C" + str(self.first_score) + "1"
366
369
            else:
367
370
                result = "C" + str(self.second_score) + "2"
368
371
        course = str(self.course_programme.course)
369
372
        return stdnum +" ("+ result +") | "+ course
370
373
371
374
class PreRegistration(models.Model):
372
375
    """ At the beginning of the new academic year, students can register
373
376
    themselves at the university. Online, they can do a preregistration already.
374
377
    These records are stored here and can later be retrieved for the actual
375
378
    registration process.
376
379
    Note: The current system in use at Hasselt University provides a password system.
377
380
    That will be eliminated here. Just make sure that the entered details are correct.
378
381
    Should there be an error, and the same email address is used to update something,
379
382
    a mail will be sent to that address to verify this was a genuine update."""
380
383
    created = models.DateField(auto_now_add=True)
381
384
    first_name = models.CharField(max_length=64, blank=False, help_text=_("Your first name."))
382
385
    last_name = models.CharField(max_length=64, blank=False, help_text=_("Your last name."))
383
386
    additional_names = models.CharField(max_length=64, blank=True, help_text=_("Any additional names."))
384
387
    title = models.CharField(
385
388
        max_length=64,
386
389
        blank=True,
387
390
        help_text=_("Any additional titles, prefixes, ..."),
388
391
        )
389
392
    DOB = models.DateField(
390
393
        blank=False,
391
394
        #editable=False,
392
395
        help_text=_("Your date of birth."),
393
396
        )
394
397
    POB = models.CharField(
395
398
        max_length=64,
396
399
        blank=False,
397
400
        #editable=False,
398
401
        help_text=_("The place you were born."),
399
402
        )
400
403
    nationality = models.CharField(
401
404
        max_length=64,
402
405
        blank=False,
403
406
        help_text=_("Your current nationality."),
404
407
        )
405
408
    national_registry_number = models.BigIntegerField(
406
409
        null=True,
407
410
        help_text=_("If you have one, your national registry number."),
408
411
        )
409
412
    civil_status = models.CharField(
410
413
        max_length=32,
411
414
        choices = (
412
415
            ("Single", _("Single")),
413
416
            ("Married", _("Married")),
414
417
            ("Divorced", _("Divorced")),
415
418
            ("Widowed", _("Widowed")),
416
419
            ("Partnership", _("Partnership")),
417
420
            ),
418
421
        blank=False,
419
422
        # There may be more; consult http://www.aantrekkingskracht.com/trefwoord/burgerlijke-staat
420
423
        # for more information.
421
424
        help_text=_("Your civil/marital status."),
422
425
        )
423
426
    email = models.EmailField(
424
427
        blank=False,
425
428
        unique=True,
426
429
        help_text=_("The e-mail address we will use to communicate until your actual registration."),
427
430
        )
428
431
    study = models.ForeignKey(
429
432
        "courses.Study",
430
433
        on_delete=models.PROTECT,
431
434
        null=False,
432
435
        help_text=_("The study you wish to follow. Be sure to provide all legal"
433
436
                    "documents that are required for this study with this "
434
437
                    "application, or bring them with you to the final registration."),
435
438
        )
436
439
    study_type = models.CharField(
437
440
        max_length=32,
438
441
        choices = (
439
442
            ("Diplom contract", _("Diplom contract")),
440
443
            ("Exam contract", _("Exam contract")),
441
444
            ("Credit contract", _("Credit contract")),
442
445
            ),
443
446
        blank=False,
444
447
        help_text=_("The type of study contract you wish to follow."),
445
448
        )
446
449
    document = models.FileField(
447
450
        upload_to="pre-enrollment/%Y",
448
451
        help_text=_("Any legal documents regarding your enrollment."),
449
452
        )
450
453
    # XXX: If the database in production is PostgreSQL, comment document, and
451
454
    # uncomment the next column.
452
455
    """documents = models.ArrayField(
453
456
        models.FileField(upload_to="pre-enrollment/%Y"),
454
457
        help_text=_("Any legal documents regarding your enrollment."),
455
458
        )"""
456
459
457
460
    def __str__(self):
458
461
        name = self.last_name +" "+ self.first_name
459
462
        dob = self.DOB.strftime("%d/%m/%Y")
460
463
        return name +" | "+ dob
461
464
462
465
463
466
# Planning and organization related tables
464
467
class Room(models.Model):
465
468
    """ Represents a room in the university.
466
469
    Rooms can have a number of properties, which are stored in the database.
467
470
    """
468
471
    # Types of rooms
469
472
    LABORATORY = _("Laboratory")  # Chemistry/Physics equipped rooms
470
473
    CLASS_ROOM = _("Class room")  # Simple class rooms
471
474
    AUDITORIUM = _("Auditorium")  # Large rooms with ample seating and equipment for lectures
472
475
    PC_ROOM    = _("PC room"   )  # Rooms equipped for executing PC related tasks
473
476
    PUBLIC_ROOM= _("Public room") # Restaurants, restrooms, ... general public spaces
474
477
    OFFICE     = _("Office"    )  # Private offices for staff
475
478
    PRIVATE_ROOM = _("Private room")  # Rooms accessible for a limited public; cleaning cupboards, kitchens, ...
476
479
    WORKSHOP   = _("Workshop"  )  # Rooms with hardware equipment to build and work on materials
477
480
    OTHER      = _("Other"     )  # Rooms that do not fit in any other category
478
481
479
482
480
483
    name = models.CharField(
481
484
        max_length=20,
482
485
        primary_key=True,
483
486
        blank=False,
484
487
        help_text=_("The name of this room. If more appropriate, this can be the colloquial name."),
485
488
        )
486
489
    seats = models.PositiveSmallIntegerField(
487
490
        help_text=_("The amount of available seats in this room."),
488
491
        )
489
492
    wheelchair_accessible = models.BooleanField(default=True)
490
493
    exams_equipped = models.BooleanField(
491
494
        default=True,
492
495
        help_text=_("Indicates if exams can reasonably be held in this room."),
493
496
        )
494
497
    loose_tables = models.BooleanField(
495
498
        default=True,
496
499
        help_text=_("If true, the tables in this room can be moved freely. "
497
500
                    "If false, they're bolted down in their positions."),
498
501
        )
499
502
    electrical_plugs = models.PositiveSmallIntegerField(
500
503
        help_text=_("The amount of electrical plugs that are available to the "
501
504
                    "people for free use. Electrical plugs that are more or "
502
505
                    "less constantly occupied by permanent equipment (such as "
503
506
                    "computers, beamers, ...) are excluded from counting."),
504
507
        )
505
508
    exterior_window = models.BooleanField(
506
509
        default=True,
507
510
        help_text=_("Indicates if this room has a window to the outside."),
508
511
        )
509
512
    software_available = models.TextField(
510
513
        blank=True,
511
514
        help_text=_("Some software used at the university is proprietary, and "
512
515
                    "thus not available at every system. If certain "
513
516
                    "software is installed on the computers in this room that "
514
517
                    "cannot be found on other computers, list them here."),
515
518
        )
516
519
    computers_available = models.PositiveSmallIntegerField(
517
520
        default=0,
518
521
        help_text=_("Indicates how many computers are available in this room."),
519
522
        )
520
523
    projector_available = models.BooleanField(
521
524
        default=False,
522
525
        help_text=_("Indicates if a projector is available at this room."),
523
526
        )
524
527
    blackboards_available = models.PositiveSmallIntegerField(
525
528
        help_text=_("The amount of blackboards available in this room."),
526
529
        )
527
530
    whiteboards_available = models.PositiveSmallIntegerField(
528
531
        help_text=_("The amount of whiteboards available in this room."),
529
532
        )
530
533
    category = models.CharField(
531
534
        max_length=16,
532
535
        blank=False,
533
536
        choices = (
534
537
            ("LABORATORY", LABORATORY),
535
538
            ("CLASS_ROOM", CLASS_ROOM),
536
539
            ("AUDITORIUM", AUDITORIUM),
537
540
            ("PC_ROOM", PC_ROOM),
538
541
            ("PUBLIC_ROOM", PUBLIC_ROOM),
539
542
            ("OFFICE", OFFICE),
540
543
            ("PRIVATE_ROOM", PRIVATE_ROOM),
541
544
            ("WORKSHOP", WORKSHOP),
542
545
            ("OTHER", OTHER),
543
546
            ),
544
547
        help_text=_("The category that best suits the character of this room."),
545
548
        )
546
549
    reservable = models.BooleanField(
547
550
        default=True,
548
551
        help_text=_("Indicates if this room can be reserved for something."),
549
552
        )
550
553
    note = models.TextField(
551
554
        blank=True,
552
555
        help_text=_("If some additional info is required for this room, like a "
553
556
                    "characteristic property (e.g. 'Usually occupied by 2BACH "
554
557
                    "informatics'), state it here."),
555
558
        )
556
559
    # TODO: Add a campus/building field or not?
557
560
558
561
    def next_reservation(self, time):
559
562
        """ Returns the next reservation starting from the given time, or, if
560
563
        the next reservation starts on the given time, that reservation.
561
564
        Returns None if there is no reservation from this moment on."""
562
565
        reservations = RoomReservation.objects.filter(room=self).filter(begin_time__gte=time).order_by('begin_time')
563
566
        if len(reservations) == 0:
564
567
            return None
565
568
        else:
566
569
            return reservations[0]
567
570
    def next_event(self, time):
568
571
        """ Returns the next event starting from the given time, or, if
569
572
        the next event starts on the given time, that event.
570
573
        Returns None if there is no event from this moment on."""
571
574
        events = CourseEvent.objects.filter(room=self).filter(begin_time__gte=time).order_by('begin_time')
572
575
        if len(events) == 0:
573
576
            return None
574
577
        else:
575
578
            return events[0]
576
579
577
580
578
581
    def reservation_possible(self, begin, end, seats=None):
579
582
        # TODO: Include events in the check for possibilities!
580
583
        """ Returns a boolean indicating if reservating during the given time
581
584
        is possible. If the begin overlaps with a reservation's end or vice versa,
582
585
        this is regarded as possible.
583
586
        Takes seats as optional argument. If not specified, it is assumed the entire
584
587
        room has to be reserved. """
585
588
        if self.reservable is False:
586
589
            return False
587
590
        if seats is not None and seats < 0: raise ValueError(_("seats ∈ ℕ"))
588
591
589
592
        reservations = RoomReservation.objects.filter(room=self)
590
593
        for reservation in reservations:
591
594
            if reservation.end_time <= begin or reservation.begin_time >= end:
592
595
                continue  # Can be trivially skipped, no overlap here
593
596
            elif seats is None or reservation.seats is None:
594
597
                return False  # The whole room cannot be reserved -> False
595
598
            elif seats + reservation.seats > self.seats:
596
599
                    return False  # Total amount of seats exceeds the available amount -> False
597
600
        return True  # No overlappings found -> True
598
601
599
602
    def __str__(self):
600
603
        return self.name
601
604
602
605
603
606
# Validators that will be used for RoomReservations and Events
604
607
def validate_event_time(time):
605
608
    """Checks if the time is a quarter of an hour (0, 15, 30, or 45)."""
606
609
    if time.minute not in [0, 15, 30, 45] or time.second != 0:
607
610
        raise ValidationError(
608
611
            _('%(time)s is not in the quarter of an hour.'),
609
612
            params={'time': time.strftime("%H:%M")})
610
613
def validate_university_hours(value):
611
614
    """Checks if the datetime value given takes place during the opening hours
612
615
    of the university (08:00 - 20:00)."""
613
616
    if value.hour < 8 or (value.hour == 22 and value.minute != 0) or value.hour >= 23:
614
617
        raise ValidationError(
615
618
            _("All events and reservations must begin and end between 08:00 "
616
619
              "and 22:00."))
617
620
def overlaps(begin_a, end_a, begin_b, end_b):
618
621
    """Checks if timespan a and b overlap with each other. If one of them ends at
619
622
    the same time the other one begins, it does not count as an overlap.
620
623
    This function assumes the end takes place strictly /after/ the begin."""
621
624
    if end_a <= begin_b or end_b <= begin_a:
622
625
        return False
623
626
    if (
624
627
            begin_a < begin_b <= end_a or
625
628
            begin_b < begin_a <= end_b or
626
629
            begin_a <= end_b < end_a or
627
630
            begin_b <= end_a < end_b):
628
631
        return True
629
632
    else:
630
633
        return False
631
634
632
635
633
636
def general_reservation_validator(self):
634
637
    # Check for overlapping reservations
635
638
    # TODO: Try to make it possible to link to the reservator,
636
639
    # to display the reason, to show the available times that a
637
640
    # reservation can be made for that room, and so on... Make it
638
641
    # a bit more interactive.
639
642
    for reservation in RoomReservation.objects.filter(room=self.room):
640
643
        if overlaps(self.begin_time,
641
644
                    self.end_time,
642
645
                    reservation.begin_time,
643
646
                    reservation.end_time):
644
647
            if isinstance(self, RoomReservation):
645
648
                if self.room.reservation_possible(self.begin_time, self.end_time, self.seats):
646
649
                    continue  # Both reservations can take place in the same room
647
650
                raise ValidationError(
648
651
                _("It is not possible to plan this event/reservation in "
649
652
                "%(room)s from %(self_begin)s to %(end_begin)s on %(day)s. "
650
653
                "%(reservator)s has already "
651
654
                "reserved it from %(res_begin)s to %(res_end)s."),
652
655
                params={'room': str(self.room),
653
656
                        'self_begin': self.begin_time.strftime("%H:%M"),
654
657
                        'self_end': self.end_time.strftime("%H:%M"),
655
658
                        'day': self.begin_time.strftime("%A (%d/%m)"),
656
659
                        'reservator': str(reservation.reservator),
657
660
                        'res_begin': reservation.begin_time.strftime("%H:%M"),
658
661
                        'res_end': reservation.end_time.strftime("%H:%M"),
659
662
                        })
660
663
    for course_event in CourseEvent.objects.filter(room=self.room):
661
664
        if overlaps(self.begin_time,
662
665
                    self.end_time,
663
666
                    course_event.begin_time,
664
667
                    course_event.end_time):
665
668
            raise ValidationError(
666
669
                _("%(docent)s has organized a %(subject)s in %(room)s from "
667
670
                    "%(res_begin)s to %(res_end)s on %(day)s, so you cannot "
668
671
                    "place a reservation there from %(self_begin)s to "
669
672
                    "%(self_end)s."),
670
673
                params={'room': str(self.room),
671
674
                        'self_begin': self.begin_time.strftime("%H:%M"),
672
675
                        'self_end': self.end_time.strftime("%H:%M"),
673
676
                        'day': self.begin_time.strftime("%A (%d/%m)"),
674
677
                        'docent': str(course_event.docent),
675
678
                        'subject': course_event.subject,
676
679
                        'res_begin': course_event.begin_time.strftime("%H:%M"),
677
680
                        'res_end': course_event.end_time.strftime("%H:%M"),})
678
681
679
682
    # Checking for correct timings:
680
683
    if self.begin_time >= self.end_time:
681
684
        raise ValidationError(
682
685
            _("The begin time (%(begin)) must take place <em>before</em> "
683
686
                "the end time (%(end))."),
684
687
            params={'begin': self.begin_time.strftime("%H:%M"),
685
688
                    'end': self.end_time.strftime("%H:%M"),})
686
689
    """if not roster.same_day(self.begin_time, self.end_time):
687
690
        raise ValidationError(
688
691
            _("The event/reservation must begin and end on the same day."))"""
689
692
690
693
691
694
class RoomReservation(models.Model):
692
695
    """ Rooms are to be reserved from time to time. They can be reserved
693
696
    by externals, for something else, and whatnot. That is stored in this table.
694
697
    """
695
698
    room = models.ForeignKey(
696
699
        "Room",
697
700
        on_delete=models.CASCADE,
698
701
        null=False,
699
702
        #editable=False,
700
703
        db_index=True,
701
704
        limit_choices_to={"reservable": True},
702
705
        help_text=_("The room that is being reserved at this point."),
703
706
    )
704
707
    reservator = models.ForeignKey(
705
708
        "User",
706
709
        on_delete=models.CASCADE,
707
710
        null=False,
708
711
        #editable=False,
709
712
        help_text=_("The person that made the reservation (and thus responsible)."),
710
713
    )
711
714
    timestamp = models.DateTimeField(auto_now_add=True)
712
715
    begin_time = models.DateTimeField(
713
716
        null=False,
714
717
        help_text=_("The time that this reservation begin."),
715
718
        validators=[validate_event_time,validate_university_hours],
716
719
    )
717
720
    end_time = models.DateTimeField(
718
721
        null=False,
719
722
        help_text=_("The time that this reservation ends."),
720
723
        validators=[validate_event_time,validate_university_hours],
721
724
    )
722
725
    seats = models.PositiveSmallIntegerField(
723
726
        null=True,
724
727
        blank=True,
725
728
        help_text=_("Indicates how many seats are required. If this is left empty, "
726
729
                    "it is assumed the entire room has to be reserved."),
727
730
    )
728
731
    reason = models.CharField(
729
732
        max_length=64,
730
733
        blank=True,
731
734
        help_text=_("The reason for this reservation, if useful."),
732
735
    )
733
736
    note = models.TextField(
734
737
        blank=True,
735
738
        help_text=_("If some additional info is required for this reservation, "
736
739
                    "state it here."),
737
740
    )
738
741
739
742
    def __str__(self):
740
743
        start = self.start_time.strftime("%H:%M")
741
744
        end = self.end_time.strftime("%H:%M")
742
745
        return str(self.room) +" | "+ start +"-"+ end
743
746
744
747
    def clean(self):
745
748
        general_reservation_validator(self)
746
749
747
750
class Degree(models.Model):
748
751
    """ Contains all degrees that were achieved at this university.
749
752
    There are no foreign keys in this field. This allows system
750
753
    administrators to safely remove accounts from alumni, without
751
754
    the risk of breaking referential integrity or accidentally removing
752
755
    degrees.
753
756
    While keeping some fields editable that look like they shouldn't be
754
757
    (e.g. first_name), this makes it possible for alumni to have a name change
755
758
    later in their life, and still being able to get a copy of their degree. """
756
759
    """ Reason for an ID field for every degree:
757
760
    This system allows for employers to verify that a certain applicant has indeed,
758
761
    achieved the degrees (s)he proclaims to have. Because of privacy concerns,
759
762
    a university cannot disclose information about alumni.
760
763
    That's where the degree ID comes in. This ID can be printed on all future
761
764
    degrees. The employer can then visit the university's website, and simply
762
765
    enter the ID. The website will then simply print what study is attached to
763
766
    this degree, but not disclose names or anything identifiable. This strikes
764
767
    thé perfect balance between (easy and digital) degree verification for employers, and maintaining
765
768
    alumni privacy to the highest extent possible. """
766
769
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
767
770
    first_name = models.CharField(
768
771
        max_length=64,
769
772
        blank=False,
770
773
        )
771
774
    last_name = models.CharField(
772
775
        max_length=64,
773
776
        blank=False,
774
777
        )
775
778
    additional_names = models.CharField(
776
779
        max_length=64,
777
780
        blank=True,
778
781
        )
779
782
    DOB = models.DateField(null=False)#editable=False, null=False)  # This can't be changed, of course
780
783
    POB = models.CharField(
781
784
        max_length=64,
782
785
        blank=False,
783
786
        #editable=False,
784
787
        )
785
788
    # The study also has to be a charfield, because if a study is removed,
786
789
    # The information will be lost.
787
790
    study = models.CharField(
788
791
        max_length=64,
789
792
        blank=False,
790
793
        #editable=False,
791
794
        )
792
795
    achieved = models.DateField(null=False)#editable=False, null=False)
793
796
    user = models.ForeignKey(
794
797
        "User",
795
798
        on_delete=models.SET_NULL,
796
799
        null=True,
797
800
        help_text=_("The person that achieved this degree, if (s)he still has "
798
801
                    "an account at this university. If the account is deleted "
799
802
                    "at a later date, this field will be set to NULL, but the "
800
803
                    "other fields will be retained."),
801
804
        )
802
805
803
806
    def __str__(self):
804
807
        return self.first_name +" "+ self.last_name +" | "+ self.study
805
808
806
809
807
810
# Classes regarding roster items
808
811
809
812
810
813
class Event(models.Model):
811
814
    """An event that will show up in the roster of accounts that need to be
812
815
    aware of this event. This can be a multitude of things, like colleges
813
816
    for certain courses, meetings like blood donations, and so on. There are
814
817
    specialized classes for certain types of events that take place."""
815
818
    begin_time = models.DateTimeField(
816
819
        null=False,
817
820
        help_text=_("The begin date and time that this event takes place. "
818
821
                    "This value must be a quarter of an hour (0, 15, 30, 45), "
819
822
                    "and take place <em>before</em> this event's end time."),
820
823
        verbose_name=_("begin time"),
821
824
        validators=[validate_event_time, validate_university_hours],
822
825
        )
823
826
    end_time = models.DateTimeField(
824
827
        null=False,
825
828
        help_text=_("The end date and time that this event takes place. "
826
829
                    "This value must be a quarter of an hour (0, 15, 30, 45), "
827
830
                    "and take place <em>after</em> this event's begin time, "
828
831
                    "but it must end on the same day as it begins!"),
829
832
        verbose_name=_("end time"),
830
833
        validators=[validate_event_time, validate_university_hours],
831
834
        )
832
835
    note = models.TextField(
833
836
        blank=True,
834
837
        help_text=_("Optional. If necessary, this field allows for additional "
835
838
                    "information that can be shown to the people for whom this "
836
839
                    "event is."),
837
840
        )
838
841
    created = models.DateTimeField(
839
842
        auto_now_add=True,
840
843
        )
841
844
    last_update = models.DateTimeField(
842
845
        auto_now=True,
843
846
        )
844
847
845
848
    def recently_created(self):
846
849
        """Indicates if this event was created in the last 5 days."""
847
850
        return (datetime.datetime.now(datetime.timezone.utc) - self.created).days <= 5
848
851
    def recently_updated(self):
849
852
        """Indicates if this event was updated in the last 5 days."""
850
853
        return (datetime.datetime.now(datetime.timezone.utc) - self.last_update).days <= 5
851
854
852
855
class CourseEvent(Event):
853
856
    """An event related to a particular course. This includes a location,
854
857
    a group (if applicable), and other data."""
855
858
    course = models.ForeignKey(
856
859
        "courses.CourseProgramme",
857
860
        on_delete=models.CASCADE,
858
861
        null=False,
859
862
        )
860
863
    docent = models.ForeignKey(
861
864
        "User",
862
865
        on_delete=models.PROTECT,
863
866
        null=False,
864
867
        limit_choices_to={'is_staff': True},
865
868
        help_text=_("The person who will be the main overseer of this event."),
866
869
        )
867
870
    room = models.ForeignKey(
868
871
        "Room",
869
872
        on_delete=models.PROTECT,
870
873
        limit_choices_to={'reservable': True},
871
874
        null=False,
872
875
        help_text=_("The room in which this event will be held."),
873
876
        )
874
877
    subject = models.CharField(
875
878
        max_length=32,
876
879
        blank=False,
877
880
        help_text=_("The subject of this event. Examples are 'Hoorcollege', "
878
881
                    "'Zelfstudie', ..."),
879
882
        )
880
883
    group = models.ForeignKey(
881
884
        "courses.CourseGroup",
882
885
        on_delete = models.CASCADE,
883
886
        null=True,
884
887
        blank=True,
885
888
        help_text=_("Some courses have multiple groups. If that's the case, "
886
889
                    "and this event is only for a specific group, then that "
887
890
                    "group must be referenced here."),
888
891
        )
889
892
890
893
    def clean(self):
891
894
        general_reservation_validator(self)
892
895
893
896
894
897
class UniversityEvent(Event):
895
898
    """University wide events. These include events like blood donations for the
896
899
    Red Cross, for example."""
897
900
    pass
898
901
899
902
class StudyEvent(Event):
900
903
    """An event that is linked to a particular study, like lectures from guest
901
904
    speakers about a certain subject, the Flemish Programming Contest, ..."""
902
905
    pass
903
906
904
907
class ExamCommissionDecision(models.Model):
905
908
    """The Exam commission can make certain decisions regarding individual
906
909
    students. Every decision on its own is stored in this table, and is linked
907
910
    to the recipient's account."""
908
911
    user = models.ForeignKey(
909
912
        User,
910
913
        on_delete=models.CASCADE,
911
914
        null=False,
912
915
        help_text=_("The recipient of this decision."),
913
916
        )
914
917
    date = models.DateField(auto_now_add=True)
915
918
    text = models.TextField(
916
919
        blank=False,
917
920
        help_text=_("The text describing the decision. Org syntax available.")
918
921
        )
919
922
    def __str__(self):
920
923
        return str(self.user) + " | " + str(self.date)
921
924
922
925
    class Meta:
923
926
        verbose_name = _("Decision of the exam commission")
924
927
        verbose_name_plural = _("Decisions of the exam commission")
925
928
926
929
class EducationDepartmentMessages(models.Model):
927
930
    """The department of education can issue messages that are to be shown to
928
931
    all students. Their contents are stored here."""
929
932
    date = models.DateField(auto_now_add=True)
930
933
    title = models.CharField(
931
934
        max_length=64,
932
935
        blank=False,
933
936
        help_text=_("A short, well-describing title for this message."),
934
937
        )
935
938
    text = models.TextField(
936
939
        blank=False,
937
940
        help_text=_("The message text. Org syntax available.")
938
941
        )
939
942
    def __str__(self):
940
943
        return str(self.date) + " | " + str(self.title)
941
944
942
945
    class Meta:
943
946
        verbose_name = _("Message of the education department")
944
947
        verbose_name_plural = _("Messages of the education department")
945
948

courses/forms.py

6 additions and 0 deletions.

View changes Hide changes
1
1
from django.forms import ModelForm, modelformset_factory
2
2
from . import models
3
3
+
4
4
5
class AssignmentForm(ModelForm):
5
6
    class Meta:
6
7
        model = models.Assignment
7
8
        fields = ['title',
8
9
                  'information',
9
10
                  'deadline',
10
11
                  'digital_task',
11
12
                  ]
12
13
class AnnouncementForm(ModelForm):
13
14
    class Meta:
14
15
        model = models.Announcement
15
16
        fields = ['title',
16
17
                  'text',
17
18
                  ]
18
19
class UploadForm(ModelForm):
19
20
    class Meta:
20
21
        model = models.Upload
21
22
        fields = ['comment',
22
23
                  'file',
23
24
                  ]
24
25
class CourseItemForm(ModelForm):
25
26
    class Meta:
26
27
        model = models.CourseItem
27
28
        fields = ['file',
28
29
                  'note',
29
30
                  ]
30
31
31
32
AssignmentFormSet = modelformset_factory(
32
33
    models.Assignment, fields=('title', 'information', 'deadline', 'digital_task'),
33
34
    localized_fields="__all__",
34
35
    can_delete=True,
35
36
    )
36
37
AnnouncementFormSet = modelformset_factory(
37
38
    models.Announcement, fields=('title', 'text'),
38
39
    localized_fields="__all__",
39
40
    can_delete=True,
40
41
    )
41
42
CourseItemFormSet = modelformset_factory(
42
43
    models.CourseItem, fields=('file', 'note'),
43
44
    localized_fields="__all__",
44
45
    can_delete=True,
45
46
    )
46
47
+
48
    administration.models.CourseResult, fields=('student', 'first_score', 'second_score', 'result'),
+
49
    localized_fields="__all__",
+
50
    can_delete=True,
+
51
    )
+
52

courses/templates/courses/edit_course_items.djhtml

4 additions and 1 deletion.

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
    {% include "administration/nav.djhtml" %}
11
-
    <p>
12
11
        {% blocktrans %}You can edit announcements, assignments, and course
13
12
            items on this page, mark them for deletion, and add new ones.
14
13
            When all changes are saved successfully, you will be redirected to
15
14
            the course's main page. When errors occur, you will stay on this page,
16
15
            and receive error notifications where necessary.{% endblocktrans %}
17
16
    </p>
18
17
19
18
    <form action="" method="POST">
20
19
        {% csrf_token %}
21
20
        <h2 id="{% trans "announcements" %}">{% trans "Announcements" %}</h2>
22
21
            <table>
23
22
                {{ announcements }}
24
23
            </table>
25
24
        <h2 id="{% trans "assignments" %}">{% trans "Assignments" %}</h2>
26
25
            <table>
27
26
            {{ assignments }}
28
27
            </table>
29
28
        <h2 id="{% trans "course-items" %}">{% trans "Course items" %}</h2>
30
29
            <table>
31
30
                {{ course_items }}
32
31
            </table>
33
32
        <hr />
+
33
            <table>
+
34
                {{ course_results }}
+
35
            </table>
+
36
        <hr />
34
37
        <input type="submit" value="{% trans "Save everything" %}" />
35
38
    </form>
36
39
{% endblock main %}
37
40

courses/views.py

12 additions and 9 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
53
    return render(request, template, context)
54
54
55
55
# TODO: Find a way to see if it's possible to require some permissions and to
56
56
# put them in a decorator
57
57
#@permission_required
58
58
@login_required
59
59
def new_item(request, course_slug):
60
60
    template = "courses/new_item.djhtml"
61
61
    course = Course.objects.get(slug_name=course_slug)
62
62
63
63
    if request.user.user_data.is_student or request.user not in course.course_team:
64
64
        # Students can't add new items. Redirect to index
65
65
        # Also redirect people who are not part of the course team
66
66
        redirect('courses-index')
67
67
    # Now able to assume user is allowed to add items to this course
68
68
69
69
    context = {
70
70
        'course': course,
71
71
        'announcements': Announcement.objects.filter(course=course),
72
72
        'assignments': Assignment.objects.filter(course=course),
73
73
        'course-items': CourseItem.objects.filter(course=course),
74
74
        'study-groups': StudyGroup.objects.filter(course=course),
75
75
        'uploads': Upload.objects.filter(course=course)
76
76
        }
77
77
78
78
    return render(request, template, context)
79
79
80
80
@login_required
+
81
@login_required
81
82
def edit_course_items(request, course_slug):
82
83
    # TODO Only allow people on the course team to this page!
83
84
    template = "courses/edit_course_items.djhtml"
84
85
    context = dict()
85
86
    course_ = Course.objects.get(slug_name=course_slug)
86
87
    if request.method == 'POST':
87
88
        assignments = AssignmentFormSet(request.POST, prefix='assignments')
88
89
        announcements = AnnouncementFormSet(request.POST, prefix='announcements')
89
90
        course_items = CourseItemFormSet(request.POST, request.FILES, prefix='course_items')
90
91
        if assignments.is_valid() and announcements.is_valid() and course_items.is_valid():
91
-
            assignments.save(commit=False)
+
92
        if assignments.is_valid() and announcements.is_valid() and course_items.is_valid() and course_results.is_valid():
+
93
            assignments.save(commit=False)
92
94
            announcements.save(commit=False)
93
95
            course_items.save(commit=False)
94
96
            for new_assignment in assignments.new_objects:
+
97
            for new_assignment in assignments.new_objects:
95
98
                new_assignment.course = course_
96
99
            for new_announcement in announcements.new_objects:
97
100
                new_announcement.course = course_
98
101
            for new_course_item in course_items.new_objects:
99
102
                new_coutse_item.course = course_
100
103
            assignments.save()
+
104
                new_coutse_result.course = course_
+
105
            assignments.save()
101
106
            announcements.save()
102
107
            course_items.save()
103
108
            return course(request, course_slug)
+
109
            return course(request, course_slug)
104
110
    else:
105
111
        assignments = AssignmentFormSet(
106
112
            queryset=Assignment.objects.filter(course=course_),
107
113
            prefix="assignments",
108
114
            )
109
115
        announcements = AnnouncementFormSet(
110
116
            queryset=Announcement.objects.filter(course=course_),
111
117
            prefix="announcements",
112
118
            )
113
119
        course_items = CourseItemFormSet(
114
120
            queryset=CourseItem.objects.filter(course=course_),
115
121
            prefix="course_items",
116
122
            )
117
123
    context['assignments'] = assignments
+
124
            queryset=CourseResult.objects.filter(course=course_).filter(year=current_academic_year()),
+
125
            prefix="course_results",
+
126
            )
+
127
    context['assignments'] = assignments
118
128
    context['announcements'] = announcements
119
129
    context['course_items'] = course_items
120
130
    return render(request, template, context)
+
131
    return render(request, template, context)
121
132
122
133
123
134
@login_required
124
-
def remove(request, type, id):
125
-
    pass
126
-
127
-
@login_required
128
-
def upload(request):
129
-
    pass
130
-
131
135
@login_required
132
136
def groups(request):
133
137
    pass
134
138
135
139
def fiche(request, course_slug):
136
140
    """Displays the fiche for the given course. Includes information about all
137
141
    course programs."""
138
142
    template = "courses/fiche.djhtml"
139
143
    context = dict()
140
144
    course = Course.objects.get(slug_name=course_slug)
141
145
    
142
-