joeni

Add new migrations and implement roster models

The database system should be done by now. I've added a couple of new model classes so that work can begin on a roster system. I've also added a new Group class to the courses app, because certain studies may have different groups. This was told to me by the professor.
Additionally, Some old migrations have been added that I forgot to implement.

Author
Maarten 'Vngngdn' Vangeneugden
Date
Jan. 24, 2018, 2:15 a.m.
Hash
389f9833d742c0fd04de07f7f999cccb62e5dda4
Parent
8e88f95f28f20568e78174ef284f1ed22ff5c11c
Modified files
administration/migrations/0008_auto_20180124_0049.py
administration/migrations/0009_auto_20180124_0049.py
administration/models.py
administration/views.py
courses/migrations/0003_auto_20180124_0049.py
courses/migrations/0004_auto_20180124_0049.py
courses/models.py

administration/migrations/0008_auto_20180124_0049.py

28 additions and 0 deletions.

View changes Hide changes
+
1
+
2
from django.db import migrations, models
+
3
+
4
+
5
class Migration(migrations.Migration):
+
6
+
7
    dependencies = [
+
8
        ('administration', '0007_auto_20171121_2018'),
+
9
    ]
+
10
+
11
    operations = [
+
12
        migrations.CreateModel(
+
13
            name='Event',
+
14
            fields=[
+
15
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
16
                ('begin_time', models.DateTimeField(help_text='The begin date and time that this event takes place.', verbose_name='begin time')),
+
17
                ('end_time', models.DateTimeField(help_text='The end date and time that this event takes place.', verbose_name='end time')),
+
18
                ('note', models.TextField(blank=True, help_text='Optional. If necessary, this field allows for additional information that can be shown to the people for whom this event is.')),
+
19
                ('created', models.DateTimeField(auto_now_add=True)),
+
20
                ('last_update', models.DateTimeField(auto_now=True)),
+
21
            ],
+
22
        ),
+
23
        migrations.RemoveField(
+
24
            model_name='curriculum',
+
25
            name='courses',
+
26
        ),
+
27
    ]
+
28

administration/migrations/0009_auto_20180124_0049.py

57 additions and 0 deletions.

View changes Hide changes
+
1
+
2
from django.conf import settings
+
3
from django.db import migrations, models
+
4
import django.db.models.deletion
+
5
+
6
+
7
class Migration(migrations.Migration):
+
8
+
9
    dependencies = [
+
10
        ('courses', '0003_auto_20180124_0049'),
+
11
        ('administration', '0008_auto_20180124_0049'),
+
12
    ]
+
13
+
14
    operations = [
+
15
        migrations.AddField(
+
16
            model_name='curriculum',
+
17
            name='course_programmes',
+
18
            field=models.ManyToManyField(help_text='All the course programmes included in this curriculum.', to='courses.CourseProgramme'),
+
19
        ),
+
20
        migrations.AlterField(
+
21
            model_name='courseresult',
+
22
            name='course_programme',
+
23
            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='courses.CourseProgramme'),
+
24
        ),
+
25
        migrations.AlterField(
+
26
            model_name='curriculum',
+
27
            name='year',
+
28
            field=models.DateField(auto_now_add=True, db_index=True, help_text='The academic year for which this curriculum is. If this field is equal to 2008, then that means this curriculum is for the academic year 2008-2009.'),
+
29
        ),
+
30
        migrations.CreateModel(
+
31
            name='CourseEvent',
+
32
            fields=[
+
33
                ('event_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='administration.Event')),
+
34
                ('subject', models.CharField(help_text="The subject of this event. Examples are 'Hoorcollege', 'Zelfstudie', ...", max_length=32)),
+
35
                ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='courses.CourseProgramme')),
+
36
                ('docent', models.ForeignKey(help_text='The person who will be the main overseer of this event.', limit_choices_to={'is_staff': True}, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
+
37
                ('group', models.ForeignKey(help_text="Some courses have multiple groups. If that's the case, and this event is only for a specific group, then that group must be referenced here.", null=True, on_delete=django.db.models.deletion.CASCADE, to='courses.Group')),
+
38
                ('room', models.ForeignKey(help_text='The room in which this event will be held.', on_delete=django.db.models.deletion.PROTECT, to='administration.Room')),
+
39
            ],
+
40
            bases=('administration.event',),
+
41
        ),
+
42
        migrations.CreateModel(
+
43
            name='StudyEvent',
+
44
            fields=[
+
45
                ('event_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='administration.Event')),
+
46
            ],
+
47
            bases=('administration.event',),
+
48
        ),
+
49
        migrations.CreateModel(
+
50
            name='UniversityEvent',
+
51
            fields=[
+
52
                ('event_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='administration.Event')),
+
53
            ],
+
54
            bases=('administration.event',),
+
55
        ),
+
56
    ]
+
57

administration/models.py

77 additions and 0 deletions.

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

administration/views.py

26 additions and 1 deletion.

View changes Hide changes
1
1
+
2
from django.urls import reverse # Why?
+
3
from django.utils.translation import gettext as _
+
4
from .models import *
+
5
import administration
+
6
from django.contrib.auth.decorators import login_required
+
7
2
8
# Create your views here.
3
-
+
9
def roster(begin=None, end=None):
+
10
    """Collects and renders the data that has to be displayed in the roster.
+
11
+
12
    The begin and end date can be specified. Only roster points in that range
+
13
    will be included in the response. If no begin and end are specified, it will
+
14
    take the current week as begin and end point. If it's
+
15
    weekend, it will take next week."""
+
16
    if begin is None or end is None:
+
17
        today = date.today()
+
18
        if today.isoweekday() in {6,7}:  # Weekend
+
19
            begin = today + date.timedelta(days=8-today.isoweekday())
+
20
            end = today + date.timedelta(days=13-today.isoweekday())
+
21
        else:  # Same week
+
22
            begin = today - date.timedelta(days=today.weekday())
+
23
            end = today + date.timedelta(days=5-today.isoweekday())
+
24
+
25
    
+
26
+
27
        return roster
+
28

courses/migrations/0003_auto_20180124_0049.py

87 additions and 0 deletions.

View changes Hide changes
+
1
+
2
import courses.models
+
3
from django.conf import settings
+
4
from django.db import migrations, models
+
5
+
6
+
7
class Migration(migrations.Migration):
+
8
+
9
    dependencies = [
+
10
        ('courses', '0002_auto_20171121_2018'),
+
11
    ]
+
12
+
13
    operations = [
+
14
        migrations.CreateModel(
+
15
            name='CourseItem',
+
16
            fields=[
+
17
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
18
                ('file', models.FileField(help_text='The file you wish to upload.', upload_to=courses.models.item_upload_directory)),
+
19
                ('timestamp', models.DateTimeField(auto_now_add=True)),
+
20
                ('note', models.TextField(blank=True, help_text='If you want to state some additional information about this upload, state it here.')),
+
21
            ],
+
22
        ),
+
23
        migrations.CreateModel(
+
24
            name='CourseProgramme',
+
25
            fields=[
+
26
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
27
                ('programme_type', models.CharField(choices=[('C', 'Compulsory'), ('O', 'Optional')], help_text='Type of this course for this study.', max_length=1)),
+
28
                ('study_hours', models.PositiveSmallIntegerField(help_text='The required amount of hours to study this course.')),
+
29
                ('ECTS', models.PositiveSmallIntegerField(help_text='The amount of ECTS points attached to this course.')),
+
30
                ('semester', models.PositiveSmallIntegerField(choices=[(1, 'First semester'), (2, 'Second semester'), (3, 'Full year course'), (4, 'Taught in first quarter'), (5, 'Taught in second quarter'), (6, 'Taught in third quarter'), (7, 'Taught in fourth quarter')], help_text='The period in which this course is being taught in this study.')),
+
31
                ('year', models.PositiveSmallIntegerField(help_text='The year in which this course is taught for this study.')),
+
32
                ('second_chance', models.BooleanField(default=True, help_text='Defines if a second chance exam is planned for this course.')),
+
33
                ('tolerable', models.BooleanField(default=True, help_text='Defines if a failed result can be tolerated.')),
+
34
                ('scoring', models.CharField(choices=[('N', 'Numerical'), ('FP', 'Fail/Pass')], default='N', help_text='How the obtained score for this course is given.', max_length=2)),
+
35
            ],
+
36
        ),
+
37
        migrations.CreateModel(
+
38
            name='Group',
+
39
            fields=[
+
40
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
41
            ],
+
42
        ),
+
43
        migrations.RenameModel(
+
44
            old_name='Prerequisites',
+
45
            new_name='Prerequisite',
+
46
        ),
+
47
        migrations.RemoveField(
+
48
            model_name='programmeinformation',
+
49
            name='course',
+
50
        ),
+
51
        migrations.RemoveField(
+
52
            model_name='programmeinformation',
+
53
            name='study',
+
54
        ),
+
55
        migrations.RemoveField(
+
56
            model_name='programmeinformation',
+
57
            name='study_programme',
+
58
        ),
+
59
        migrations.AddField(
+
60
            model_name='assignment',
+
61
            name='title',
+
62
            field=models.CharField(default='null', help_text='The title of this assignment.', max_length=32),
+
63
            preserve_default=False,
+
64
        ),
+
65
        migrations.AddField(
+
66
            model_name='course',
+
67
            name='color',
+
68
            field=models.CharField(default='E73B2B', help_text='The color for this course. Must be an hexadecimal code.', max_length=6),
+
69
        ),
+
70
        migrations.AddField(
+
71
            model_name='course',
+
72
            name='slug_name',
+
73
            field=models.SlugField(allow_unicode=True, default='empty_slug', help_text="A so-called 'slug name' for this course.", unique=True),
+
74
            preserve_default=False,
+
75
        ),
+
76
        migrations.AlterField(
+
77
            model_name='course',
+
78
            name='educating_team',
+
79
            field=models.ManyToManyField(help_text='The remaining team members of this course.', limit_choices_to={'is_staff': True}, related_name='educating_team', to=settings.AUTH_USER_MODEL),
+
80
        ),
+
81
        migrations.AlterField(
+
82
            model_name='study',
+
83
            name='degree_type',
+
84
            field=models.CharField(choices=[('BSc', 'Bachelor of Science'), ('MSc', 'Master of Science'), ('LL.B', 'Bachelor of Laws'), ('LL.M', 'Master of Laws'), ('ir.', 'Engineer'), ('ing.', 'Technological Engineer'), ('BA', 'Bachelor of Arts'), ('MA', 'Master of Arts')], help_text='The type of degree one obtains upon passing this study.', max_length=64),
+
85
        ),
+
86
    ]
+
87

courses/migrations/0004_auto_20180124_0049.py

43 additions and 0 deletions.

View changes Hide changes
+
1
+
2
from django.db import migrations, models
+
3
import django.db.models.deletion
+
4
+
5
+
6
class Migration(migrations.Migration):
+
7
+
8
    dependencies = [
+
9
        ('courses', '0003_auto_20180124_0049'),
+
10
        ('administration', '0009_auto_20180124_0049'),
+
11
    ]
+
12
+
13
    operations = [
+
14
        migrations.DeleteModel(
+
15
            name='ProgrammeInformation',
+
16
        ),
+
17
        migrations.AddField(
+
18
            model_name='group',
+
19
            name='study',
+
20
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='courses.Study'),
+
21
        ),
+
22
        migrations.AddField(
+
23
            model_name='courseprogramme',
+
24
            name='course',
+
25
            field=models.ForeignKey(help_text='The course that this programme is for.', on_delete=django.db.models.deletion.CASCADE, to='courses.Course'),
+
26
        ),
+
27
        migrations.AddField(
+
28
            model_name='courseprogramme',
+
29
            name='study',
+
30
            field=models.ForeignKey(help_text='The study in which the course is taught.', on_delete=django.db.models.deletion.CASCADE, to='courses.Study'),
+
31
        ),
+
32
        migrations.AddField(
+
33
            model_name='courseprogramme',
+
34
            name='study_programme',
+
35
            field=models.ForeignKey(help_text='The study programme that this course belongs to.', on_delete=django.db.models.deletion.CASCADE, to='courses.StudyProgramme'),
+
36
        ),
+
37
        migrations.AddField(
+
38
            model_name='courseitem',
+
39
            name='course',
+
40
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='courses.Course'),
+
41
        ),
+
42
    ]
+
43

courses/models.py

16 additions and 0 deletions.

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