joeni

Refinement of roster module in administration

The functionality of the roster has been moved to its own module, to keep it nicely seperated from what is the view code.
Several updates in the models have been made, so the migrations have been included.
Added a couple models to the admin registry so elements can be added in the admin interface.
Also added some new style classes for the roster to indicate important things like a change, new event, or notification.

Author
Maarten 'Vngngdn' Vangeneugden
Date
Feb. 4, 2018, 7:21 p.m.
Hash
838a4c30e3ac78883df4d4a301ef8929e5be6ffe
Parent
db2baced09f3dd36acf8ca5ebb352e88c7b30ed1
Modified files
administration/admin.py
administration/migrations/0012_auto_20180204_1349.py
administration/migrations/0013_auto_20180204_1444.py
administration/models.py
administration/roster.py
administration/templates/administration/roster.djhtml
administration/views.py
courses/admin.py
courses/migrations/0005_auto_20180204_1349.py
courses/migrations/0006_auto_20180204_1349.py
courses/models.py
static/css/base.css

administration/admin.py

5 additions and 0 deletions.

View changes Hide changes
1
1
from .models import *
2
2
from django.contrib.auth.admin import UserAdmin
3
3
4
4
admin.site.register(User, UserAdmin)
5
5
6
6
admin.site.register(Curriculum)
7
7
admin.site.register(UserData)
8
8
admin.site.register(CourseResult)
9
9
admin.site.register(PreRegistration)
10
10
admin.site.register(Room)
11
11
admin.site.register(RoomReservation)
12
12
admin.site.register(Degree)
13
13
14
14
admin.site.register(CourseEvent)
+
15
admin.site.register(CourseEvent)
15
16
admin.site.register(UniversityEvent)
16
17
admin.site.register(StudyEvent)
17
18
+
19
admin.site.register(ExamCommissionDecision)
+
20
admin.site.register(EducationDepartmentMessages)
+
21
+
22

administration/migrations/0012_auto_20180204_1349.py

57 additions and 0 deletions.

View changes Hide changes
+
1
+
2
import administration.models
+
3
from django.conf import settings
+
4
from django.db import migrations, models
+
5
import django.db.models.deletion
+
6
+
7
+
8
class Migration(migrations.Migration):
+
9
+
10
    dependencies = [
+
11
        ('administration', '0011_auto_20180128_1935'),
+
12
    ]
+
13
+
14
    operations = [
+
15
        migrations.CreateModel(
+
16
            name='EducationDepartmentMessages',
+
17
            fields=[
+
18
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
19
                ('date', models.DateField(auto_now_add=True)),
+
20
                ('title', models.CharField(help_text='A short, well-describing title for this message.', max_length=64)),
+
21
                ('text', models.TextField(help_text='The message text. Org syntax available.')),
+
22
            ],
+
23
            options={
+
24
                'verbose_name': 'Decision of the exam commission',
+
25
                'verbose_name_plural': 'Decisions of the exam commission',
+
26
            },
+
27
        ),
+
28
        migrations.CreateModel(
+
29
            name='ExamCommissionDecision',
+
30
            fields=[
+
31
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
32
                ('date', models.DateField(auto_now_add=True)),
+
33
                ('text', models.TextField(help_text='The text describing the decision. Org syntax available.')),
+
34
                ('user', models.ForeignKey(help_text='The recipient of this decision.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+
35
            ],
+
36
            options={
+
37
                'verbose_name': 'Decision of the exam commission',
+
38
                'verbose_name_plural': 'Decisions of the exam commission',
+
39
            },
+
40
        ),
+
41
        migrations.AddField(
+
42
            model_name='courseresult',
+
43
            name='year',
+
44
            field=models.PositiveIntegerField(default=2018, help_text="The academic year this course took place in. If 2018 is entered, then that means academic year '2018-2019'."),
+
45
        ),
+
46
        migrations.AlterField(
+
47
            model_name='event',
+
48
            name='begin_time',
+
49
            field=models.DateTimeField(help_text="The begin date and time that this event takes place. This value must be a quarter of an hour (0, 15, 30, 45), and take place <em>before</em> this event's end time.", validators=[administration.models.validate_event_time], verbose_name='begin time'),
+
50
        ),
+
51
        migrations.AlterField(
+
52
            model_name='event',
+
53
            name='end_time',
+
54
            field=models.DateTimeField(help_text="The end date and time that this event takes place. This value must be a quarter of an hour (0, 15, 30, 45), and take place <em>after</em> this event's begin time.", validators=[administration.models.validate_event_time], verbose_name='end time'),
+
55
        ),
+
56
    ]
+
57

administration/migrations/0013_auto_20180204_1444.py

29 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
        ('administration', '0012_auto_20180204_1349'),
+
11
    ]
+
12
+
13
    operations = [
+
14
        migrations.AlterModelOptions(
+
15
            name='educationdepartmentmessages',
+
16
            options={'verbose_name': 'Message of the education department', 'verbose_name_plural': 'Messages of the education department'},
+
17
        ),
+
18
        migrations.AlterField(
+
19
            model_name='courseevent',
+
20
            name='group',
+
21
            field=models.ForeignKey(blank=True, 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.CourseGroup'),
+
22
        ),
+
23
        migrations.AlterField(
+
24
            model_name='curriculum',
+
25
            name='student',
+
26
            field=models.ForeignKey(limit_choices_to={'groups': 1}, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, unique_for_year='year'),
+
27
        ),
+
28
    ]
+
29

administration/models.py

6 additions and 4 deletions.

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

administration/roster.py

106 additions and 0 deletions.

View changes Hide changes
+
1
building of the roster. """
+
2
from django.shortcuts import render
+
3
from collections import OrderedDict
+
4
import datetime
+
5
from django.urls import reverse # Why?
+
6
from django.utils.translation import gettext as _
+
7
from .models import *
+
8
import administration
+
9
+
10
def same_day(datetime_a, datetime_b):
+
11
    """True if both a and b are on the same day, false otherwise."""
+
12
    return (
+
13
        datetime_a.day == datetime_b.day and
+
14
        datetime_a.month == datetime_b.month and
+
15
        datetime_a.year == datetime_b.year)
+
16
+
17
+
18
def make_quarter_buckets(events):
+
19
    """Returns a dict with all days that the given events take place.
+
20
    Every quarter between the first and last event will get a bucket in the dict,
+
21
    with the keys in format "dd-mm-yyyy". Also quarters without events will be
+
22
    included, but will simply have empty buckets."""
+
23
    quarters = OrderedDict()  # This is the first time I ever use an OrderedDict and I intend it to be my last one as well.
+
24
    current_quarter = datetime.datetime.now().replace(hour=8, minute=0)
+
25
    while current_quarter.hour != 20 or current_quarter.minute != 00:
+
26
        quarters[current_quarter.strftime("%H:%M")] = list()
+
27
        current_quarter += datetime.timedelta(minutes=15)
+
28
    for event in events:
+
29
        quarters[event.begin_time.strftime("%H:%M")].append(event)
+
30
+
31
    return quarters  # Yay! ^.^
+
32
+
33
+
34
def create_roster_rows(events):
+
35
    """Creates the rows for use in the roster table.
+
36
    None of the times in the given events may overlap, and all must start and
+
37
    end at a quarter of the hour (so :00, :15, :30, or :45). If you think you're above this,
+
38
    I'll raise you a ValueError, kind sir.
+
39
    Events must be of administration.models.Event type."""
+
40
    for event in events:
+
41
        for other_event in events:
+
42
            if other_event is not event and (
+
43
                    (event.begin_time >= other_event.begin_time and event.begin_time < other_event.end_time)
+
44
                    or (event.end_time > other_event.begin_time and event.end_time <= other_event.end_time)
+
45
                    ):
+
46
                raise ValueError("One of the events overlaps with another event.")
+
47
        if event.begin_time.minute not in [0, 15, 30, 45] or event.end_time.minute not in [0, 15, 30, 45]:
+
48
            raise ValueError("One of the events did not begin or end on a quarter.")
+
49
+
50
    # XXX: I assume here the given queryset is sorted:
+
51
    first_day = events[0].begin_time
+
52
    last_day = events.last().begin_time
+
53
    # All events validated
+
54
    quarter_buckets = make_quarter_buckets(events)
+
55
    skip_row = dict()
+
56
    table_code = list()
+
57
+
58
    for quarter_bucket in quarter_buckets.keys():
+
59
        # Preparing time column
+
60
        quarter_line = "<tr><td style='font-size: xx-small;'>"
+
61
        if quarter_bucket[3:] == "00":
+
62
            quarter_line += quarter_bucket
+
63
        quarter_line += "</td>"
+
64
+
65
        current_day = first_day
+
66
        while not same_day(current_day, last_day+datetime.timedelta(days=1)):
+
67
            for event in quarter_buckets[quarter_bucket]:
+
68
                if same_day(current_day, event.begin_time):
+
69
                    quarters = (event.end_time - event.begin_time).seconds // 60 // 15
+
70
                    skip_row[quarter_bucket] = quarters
+
71
                    event_line = "<td "
+
72
                    if isinstance(event, CourseEvent) and (datetime.datetime.now(datetime.timezone.utc) - event.last_update).days > 5:
+
73
                        event_line += "style='backgrond-color: #"+event.course.color+"; color: white;' "
+
74
                    elif (datetime.datetime.now(datetime.timezone.utc) - event.last_update).days <= 5:
+
75
                        event_line += "style='backgrond-color: yellow; color: red;' "
+
76
                    if event.note:
+
77
                        event_line +="title='" +event.note+ "' "
+
78
+
79
                    # FIXME: From here, I just assume the events are all CourseEvents, because
+
80
                    # that is the most important one to implement for the prototype.
+
81
                    if quarters > 1:
+
82
                        event_line += "rowspan='"+str(quarters)+"' "
+
83
+
84
                    event_line +=">"
+
85
                    event_line += str(
+
86
                        #"<a href=" + reverse("courses-course-index", args="")+"> "  # FIXME so that this links to the course's index page
+
87
                        "<a href=""> "
+
88
                        + str(event.course)
+
89
                        + "<br />"
+
90
                        + str(event.docent)
+
91
                        + "<br />"
+
92
                        + str(event.room) + " (" + str(event.subject) + ")</a></td>")
+
93
                    quarter_line += event_line
+
94
                else:
+
95
                    if quarter_bucket in skip_row:
+
96
                        skip_row[quarter_bucket] -= 1
+
97
                        if skip_row[quarter_bucket] == 0:
+
98
                            del skip_row[quarter_bucket]
+
99
                            quarter_line += "<td></td>"
+
100
                    else:
+
101
                        quarter_line += "<td></td>"
+
102
            current_day += datetime.timedelta(days=1)
+
103
        quarter_line += "</tr>"
+
104
        table_code.append(quarter_line)
+
105
    return table_code
+
106

administration/templates/administration/roster.djhtml

43 additions and 43 deletions.

View changes Hide changes
1
1
{% cycle "hour" "first quarter" "half" "last quarter" as hour silent %}
2
2
{# "silent" blocks the cycle operator from printing the cycler, and in subsequent calls #}
3
3
{% load i18n %}
4
4
5
5
{% block title %}
6
6
    {% trans "Roster | ◀ Joeni /▶" %}
7
-
{% endblock %}
+
7
{% endblock %}
8
8
9
9
{% block main %}
10
10
    {% include "administration/nav.djhtml" %}
11
11
    <h1>{% trans "Personal timetable" %}</h1>
12
12
    <h2>{% trans "Explanation" %}</h2>
13
13
    <p>
14
14
        {% trans "Personal roster from" %} {{ begin|date }} {% trans "to" %} {{ end|date }}
15
15
    </p>
16
16
    <p>
17
17
        {% blocktrans %}
18
18
            Some fields may have additional information that might be of interest
19
19
            to you. This information is shown in different ways with colour codes.
20
20
        {% endblocktrans %}
21
21
    </p>
22
22
    <p>
23
-
        {% blocktrans %}
24
-
            Most fields have a single colour, determined by the course. This makes
25
-
            it easier to differentiate on a glance what hours are for what courses.
26
-
        {% endblocktrans %}
27
-
    </p>
28
-
    <p>
29
-
        {% blocktrans %}
30
-
            Some fields have a
31
-
            <span style="background-color:yellow; color: red; border: medium dotted red;">
32
-
                bright yellow background, with red text and a red dotted border.</span>
33
-
            This indicates this event had one or more of its properties changed
34
-
            in the last five days. This can be the room, the hours, the subject, ...
35
-
            You're encouraged to take note of that.
36
-
        {% endblocktrans %}
37
-
    </p>
38
-
    <p>
39
-
        {% blocktrans %}
40
-
            Some fields are <span style="border: medium dashed black; background-color: white; color: black;">
41
-
            white with a dashed black border.</span> This indicates this event
42
-
            is new, and was added in the last five days.
43
-
        {% endblocktrans %}
44
-
    </p>
45
-
    <p>
46
-
        {% blocktrans %}
47
-
            Fields that flash <span style="color:orange; border: medium solid orange;">
48
-
            orange with the same coloured border</span> have a note attached to
49
-
            them by the docent/speaker. Hover over the event to display the note.
50
-
        {% endblocktrans %}
51
-
    </p>
52
-
53
23
    <h2>{% trans "Main hour roster" %}
54
-
    <table>
+
24
        <dt><span class="event-update">
+
25
            {% trans "Recent event update" %}
+
26
        </span></dt>
+
27
        <dd>
+
28
            {% blocktrans %}
+
29
                This event had one or more of its properties changed
+
30
                in the last five days. This can be the room, the hours, the subject, ...
+
31
                You're encouraged to take note of that.
+
32
            {% endblocktrans %}
+
33
        </dd>
+
34
        <dt><span class="event-new">
+
35
            {% trans "New event" %}
+
36
        </span></dt>
+
37
        <dd>
+
38
            {% blocktrans %}
+
39
                This is a new event, added in the last five days.
+
40
            {% endblocktrans %}
+
41
        </dd>
+
42
        <dt><span class="event-note">
+
43
            {% trans "Notification available" %}
+
44
        </span></dt>
+
45
        <dd>
+
46
            {% blocktrans %}
+
47
                This event has a note attached to it by the docent. Hover over
+
48
                the event to display the note.
+
49
            {% endblocktrans %}
+
50
        </dd>
+
51
    </dl>
+
52
+
53
    <h2>{% trans "Main hour roster" %}</h2>
+
54
    <style>
+
55
        table tr td {
+
56
        border: medium solid red;
+
57
        }
+
58
    </style>
+
59
    <table>
55
60
        <th>
56
61
            <td></td> {# Empty row for hours #}
57
-
            {% for day in days %}
+
62
            {% for day in days %}
58
63
                <td>{{ day|date:"l (d/m)" }}</td>
59
64
            {% endfor %}
60
65
        </th>
61
66
        {% for time, events in time_blocks %}
62
-
            <tr>
63
-
                {% if hour == "hour" %}
+
67
            {{ element|safe }}
+
68
            <!--<tr>
+
69
                {% if hour == "hour" %}
64
70
                    <td>{{ time }}</td>
65
71
                {% else %}
66
72
                    <td></td>
67
73
                {% endif %}
68
74
                {% cycle hour %}
69
75
                <td>{{ time }}</td>
70
76
                <!--<td rowspan="5">AI</td>
71
-
                <td>Dinsdag</td>
72
-
                <td>Dinsdag</td>
73
-
                <td>Woensdag</td>
74
-
                <td>Dondeddag</td>
75
-
                <td>Vdijdag</td>
76
-
                <td>Zaterdag</td>-->
77
-
            </tr>
78
-
        {% endfor %}
+
77
            </tr>-->
+
78
        {% endfor %}
79
79
    </table>
80
80
{% endblock main %}
81
81

administration/views.py

14 additions and 103 deletions.

View changes Hide changes
1
1
from collections import OrderedDict
2
2
from django.http import HttpResponseRedirect
3
3
import datetime
4
4
from django.urls import reverse # Why?
5
5
from django.utils.translation import gettext as _
6
6
from .models import *
7
7
from .forms import UserDataForm
8
8
import administration
+
9
import administration
9
10
from django.contrib.auth.decorators import login_required
10
11
from django.contrib.auth import authenticate
11
12
12
13
13
-
def make_day_buckets(events):
14
-
    """Returns a dict with all days that the given events take place.
15
-
    Every day between the first and last event will get a bucket in the dict,
16
-
    with the keys in format "dd-mm-yyyy". Also days without events will be
17
-
    included, but will simply have empty buckets."""
18
-
    first_event = events[0]
19
-
    last_event = events[0]
20
-
    days = OrderedDict()  # This is the first time I ever use an OrderedDict and I intend it to be my last one as well.
21
-
    for event in events:
22
-
        if event < first_event:
23
-
            first_event = event
24
-
        if event > last_event:
25
-
            last_event = event
26
-
    days_count = (last_event - first_event).days
27
-
    event_counter = first_event
28
-
    for i in range(days_count):
29
-
        days[event_counter.strftime("%d-%m-%Y")] = list()
30
-
        event_counter += datetime.timedelta(days=1)
31
-
    for event in events:
32
-
        days[event.strftime("%d-%m-%Y")].append(event)
33
-
34
-
    return days  # Yay! ^.^
35
-
36
-
37
-
def create_roster_rows(events):
38
-
    """Creates the rows for use in the roster table.
39
-
    None of the times in the given events may overlap, and all must start and
40
-
    end at a quarter of the hour (so :00, :15, :30, or :45). If you think you're above this,
41
-
    I'll raise you a ValueError, kind sir.
42
-
    Events must be of administration.models.Event type."""
43
-
    for event in events:
44
-
        for other_event in events:
45
-
            if (
46
-
                    (event.begin_time > other_event.begin_time and event.begin_time > other_event.end_time)
47
-
                    or (event.end_time > other_event.begin_time and event.end_time > other_event.end_time)
48
-
                    ):
49
-
                raise ValueError("One of the events overlaps with another event.")
50
-
        if event.begin_time.minute not in [0, 15, 30, 45] or event.end_time.minute not in [0, 15, 30, 45]:
51
-
            raise ValueError("One of the events did not begin or end on a quarter.")
52
-
53
-
    # All events validated
54
-
    days = make_day_buckets(events)
55
-
56
-
    table_code = list()
57
-
    for hour in range(8, 20):
58
-
        for quarter in [0, 15, 30, 45]:
59
-
            quarter_line = "<tr><td>"
60
-
            if hour < 10:
61
-
                quarter_line += "0"
62
-
            quarter_line += str(hour) + ":"
63
-
            if quarter == 0:
64
-
                quarter_line += "0"
65
-
            quarter_line += str(quarter) + "</td>"
66
-
67
-
            for day in days:
68
-
                for event in day:
69
-
                    if event.begin_time.hour == hour and event.begin_time.minute == quarter:
70
-
                        quarters = (event.end_time - event.begin_time).minutes // 15
71
-
                        event_line = "<td "
72
-
                        if isinstance(event, administration.CourseEvent) and (datetime.datetime.today() - event.last_update).days > 5:
73
-
                            event_line += "style='backgrond-color: #"+event.course.color+"; color: white;' "
74
-
                        elif (datetime.datetime.today() - event.last_update).days <= 5:
75
-
                            event_line += "style='backgrond-color: yellow; color: red;' "
76
-
                        if event.note:
77
-
                            event_line +="title='" +event.note+ "' "
78
-
79
-
                        # FIXME: From here, I just assume the events are all CourseEvents, because
80
-
                        # that is the most important one to implement for the prototype.
81
-
                        if quarters > 1:
82
-
                            event_line += "rowspan='"+str(quarters)+"' "
83
-
84
-
                        event_line +=">"
85
-
                        event_line += str(
86
-
                            + "<a href=" + reverse("courses-course-index", args="")+"> "  # FIXME so that this links to the course's index page
87
-
                            + str(event.course)
88
-
                            + "<br />"
89
-
                            + str(event.docent)
90
-
                            + "<br />"
91
-
                            + str(event.room) + " (" + str(event.subject) + ")</a></td>")
92
-
                        quarter_line += event_line
93
-
                    else:
94
-
                        quarter_line += "<td></td>"
95
-
96
-
            quarter_line += "</tr>"
97
-
            code += quarter_line
98
-
    return code
99
-
100
-
101
-
102
-
103
-
104
-
105
-
106
-
@login_required
107
14
def roster(request, begin=None, end=None):
108
15
    """Collects and renders the data that has to be displayed in the roster.
109
16
110
17
    The begin and end date can be specified. Only roster points in that range
111
18
    will be included in the response. If no begin and end are specified, it will
112
19
    take the current week as begin and end point. If it's
113
20
    weekend, it will take next week."""
114
21
115
22
    # TODO Handle given begin and end
116
23
    context = dict()
117
24
    template = "administration/roster.djhtml"
118
25
119
26
    if begin is None or end is None:
120
27
        today = datetime.date.today()
121
28
        if today.isoweekday() in {6,7}:  # Weekend
122
29
            begin = today + datetime.timedelta(days=8-today.isoweekday())
123
30
            end = today + datetime.timedelta(days=13-today.isoweekday())
124
31
        else:  # Same week
125
32
            begin = today - datetime.timedelta(days=today.weekday())
126
33
            end = today + datetime.timedelta(days=5-today.isoweekday())
127
34
    context['begin'] = begin
128
35
    context['end'] = end
129
36
130
37
    days = [begin]
131
38
    while (end-days[-1]).days != 0:
132
39
        # Human translation: Keep adding days until the last day in the array of
133
40
        # days is the same day as the last day the user wants to see the roster for.
134
41
        days.append(days[-1] + datetime.timedelta(days=1))
135
42
    context['days'] = days
136
43
137
44
    # Collecting events
138
45
    course_events = CourseEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
139
-
    #university_events = UniversityEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
+
46
    #university_events = UniversityEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
140
47
    #study_events = StudyEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
141
48
    #events = Event.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
142
49
+
50
143
51
    # Producing time blocks for display in the table
144
52
    time_blocks = []
145
-
    for i in range(8, 20):
+
53
    for i in range(8, 20):
146
54
        time_block = str(i)
147
-
        for j in range(0, 60, 15):
+
55
        for j in range(0, 60, 15):
148
56
            if j == 0:
149
57
                time_blocks.append([time_block + ":00",""])
150
-
                continue
151
-
            time_blocks.append([time_block + ":" + str(j), ""])
152
-
    context['time_blocks'] = time_blocks
153
-
    #context['
154
-
155
-
+
58
            else:
+
59
                times.append(time + ":" + str(j))
+
60
156
61
+
62
    for i in range(len(times)):
+
63
        time_blocks.append([times[i],table_code[i]])
+
64
157
65
    return render(request, template, context)
+
66
    context['time_blocks'] = table_code
+
67
    #print(time_blocks)
+
68
    return render(request, template, context)
158
69
    # TODO Finish!
159
70
160
71
def index(request):
161
72
    template = "administration/index.djhtml"
162
73
    context = {}
163
74
    return render(request, template, context)
164
75
165
76
    pass
166
77
167
78
def pre_registration(request):
168
79
    user_data_form = UserDataForm()
169
80
    template = "administration/pre_registration.djhtml"
170
81
    context = dict()
171
82
172
83
    if request.method == 'POST':
173
84
        user_data_form = UserDataForm(request.POST)
174
85
        context['user_data_form'] = user_data_form
175
86
        if user_data_form.is_valid():
176
87
            user_data_form.save()
177
88
            context['messsage'] = _("Your registration has been completed. You will receive an e-mail shortly.")
178
89
        else:
179
90
            context['messsage'] = _("The data you supplied had errors. Please review your submission.")
180
91
    else:
181
92
        context['user_data_form'] = UserDataForm(instance = user_data)
182
93
183
94
    return render(request, template, context)
184
95
    pass
185
96
186
97
@login_required
187
98
def settings(request):
188
99
    user_data = UserData.objects.get(user=request.user)
189
100
    user_data_form = UserDataForm(instance = user_data)
190
101
    template = "administration/settings.djhtml"
191
102
    context = dict()
192
103
193
104
    if request.method == 'POST':
194
105
        user_data_form = UserDataForm(request.POST, instance = user_data)
195
106
        context['user_data_form'] = user_data_form
196
107
        if user_data_form.is_valid():
197
108
            user_data_form.save()
198
109
            context['messsage'] = _("Your settings were successfully updated.")
199
110
        else:
200
111
            context['messsage'] = _("The data you supplied had errors. Please review your submission.")
201
112
    else:
202
113
        context['user_data_form'] = UserDataForm(instance = user_data)
203
114
204
115
    return render(request, template, context)
205
116
206
117
@login_required
207
118
def bulletin_board(request):
208
119
    context = dict()
209
120
    context['exam_commission_decisions'] = ExamCommissionDecision.objects.filter(user=request.user)
210
121
    context['education_department_messages'] = ExamCommissionDecision.objects.filter(user=request.user)
211
122
    template = "administration/bulletin_board.djhtml"
212
123
    return render(request, template, context)
213
124
214
125
def jobs(request):
215
126
    context = dict()
216
127
    template = "administration/jobs.djhtml"
217
128
    #@context['decisions'] = ExamCommissionDecision.objects.filter(user=request.user)
218
129
    return render(request, template, context)
219
130
220
131
221
132
def curriculum(request):
222
133
    return render(request, template, context)
223
134
224
135
def result(request):
225
136
    return render(request, template, context)
226
137
227
138
@login_required
228
139
def results(request):
229
140
    results = CourseResult.objects.filter(student=request.user)
230
141
    template = "administration/results.djhtml"
231
142
    # TODO
232
143
    return render(request, template, context)
233
144
234
145
def forms(request):
235
146
    return render(request, template, context)
236
147
237
148
def rooms(request):
238
149
    template = "administration/rooms.djhtml"
239
150
    return render(request, template, context)
240
151
241
152
def room_reservate(request):
242
153
    return render(request, template, context)
243
154
244
155
def login(request):
245
156
    context = dict()
246
157
    if request.method == "POST":
247
158
        name = request.POST['name']
248
159
        passphrase = request.POST['pass']
249
160
        user = authenticate(username=name, password=passphrase)
250
161
        if user is not None: # The user was successfully authenticated
251
162
            print("YA")
252
163
            return HttpResponseRedirect(request.POST['next'])
253
164
        else: # User credentials were wrong
254
165
            context['next'] = request.POST['next']
255
166
            context['message'] = _("The given credentials were not correct.")
256
167
    else:
257
168
        context['next'] = request.GET.get('next', None)
258
169
        if context['next'] is None:
259
170
            context['next'] = reverse('administration-index')
260
171
261
172
    template = 'administration/login.djhtml'
262
173
263
174
    return render(request, template, context)
264
175

courses/admin.py

1 addition and 0 deletions.

View changes Hide changes
1
1
from .models import *
2
2
3
3
admin.site.register(Course)
4
4
admin.site.register(Prerequisite)
5
5
admin.site.register(CourseProgramme)
6
6
admin.site.register(Study)
7
7
admin.site.register(StudyProgramme)
8
8
admin.site.register(Assignment)
9
9
admin.site.register(Announcement)
10
10
admin.site.register(Upload)
11
11
admin.site.register(StudyGroup)
12
12
+
13

courses/migrations/0005_auto_20180204_1349.py

51 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
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+
11
        ('courses', '0004_auto_20180124_0049'),
+
12
    ]
+
13
+
14
    operations = [
+
15
        migrations.AddField(
+
16
            model_name='course',
+
17
            name='co_owners',
+
18
            field=models.ManyToManyField(blank=True, help_text='If applicable: The co-owners of this course.', limit_choices_to={'is_staff': True}, related_name='co_owners', to=settings.AUTH_USER_MODEL),
+
19
        ),
+
20
        migrations.AlterField(
+
21
            model_name='course',
+
22
            name='color',
+
23
            field=models.CharField(default='E73B2B', help_text="The color for this course. Must be an hexadecimal code. Some standard colors if you don't know what to pick: <ul><li>0076BE: Faculty of Sciences / Blue</li><li>C0D633: Faculty of Transportation Sciences / Green</li><li>F4802D: Faculty of Architecture and Arts / Orange</li><li>00ACEE: Faculty of Business Economics / Turquoise</li><li>9C3591: Faculty of Medicine and Life Sciences / Purple</li><li>5BC4BA: Faculty of Engineering Technology / Light blue</li><li>E41F3A: Faculty of Law / Red</li></ul>", max_length=6),
+
24
        ),
+
25
        migrations.AlterField(
+
26
            model_name='course',
+
27
            name='educating_team',
+
28
            field=models.ManyToManyField(blank=True, help_text='The remaining team members of this course.', limit_choices_to={'is_staff': True}, related_name='educating_team', to=settings.AUTH_USER_MODEL),
+
29
        ),
+
30
        migrations.AlterField(
+
31
            model_name='prerequisite',
+
32
            name='ECTS_for_required_study',
+
33
            field=models.PositiveSmallIntegerField(blank=True, help_text='The amount of obtained ECTS points for the required course, if any.', null=True),
+
34
        ),
+
35
        migrations.AlterField(
+
36
            model_name='prerequisite',
+
37
            name='in_curriculum',
+
38
            field=models.ManyToManyField(blank=True, help_text='All courses that have to be in the curriculum to follow this. If a credit was achieved, that course can be omitted.', related_name='in_curriculum', to='courses.Course'),
+
39
        ),
+
40
        migrations.AlterField(
+
41
            model_name='prerequisite',
+
42
            name='required_study',
+
43
            field=models.ForeignKey(blank=True, help_text='If one must have a certain amount of obtained ECTS points for a particular course, state that course here.', null=True, on_delete=django.db.models.deletion.CASCADE, to='courses.Study'),
+
44
        ),
+
45
        migrations.AlterField(
+
46
            model_name='prerequisite',
+
47
            name='sequentialities',
+
48
            field=models.ManyToManyField(blank=True, help_text="All courses for which a credit must've been received in order to follow the course.", related_name='sequentialities', to='courses.Course'),
+
49
        ),
+
50
    ]
+
51

courses/migrations/0006_auto_20180204_1349.py

18 additions and 0 deletions.

View changes Hide changes
+
1
+
2
from django.db import migrations
+
3
+
4
+
5
class Migration(migrations.Migration):
+
6
+
7
    dependencies = [
+
8
        ('administration', '0012_auto_20180204_1349'),
+
9
        ('courses', '0005_auto_20180204_1349'),
+
10
    ]
+
11
+
12
    operations = [
+
13
        migrations.RenameModel(
+
14
            old_name='Group',
+
15
            new_name='CourseGroup',
+
16
        ),
+
17
    ]
+
18

courses/models.py

1 addition and 1 deletion.

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

static/css/base.css

15 additions and 0 deletions.

View changes Hide changes
1
1
    font-family: Verdana, Futura, Arial, sans-serif;
2
2
    background-color: #dedede;
3
3
}
4
4
5
5
a.btn {
6
6
    border-style: solid;
7
7
    text-transform: uppercase;
8
8
    border-size: 3em;
9
9
    border-color: blue;
10
10
    padding: 0.5em;
11
11
    text-decoration: none;
12
12
    color: blue;
13
13
    font-weight: bold;
14
14
}
15
15
a.btn:hover {
16
16
    background-color: blue;
17
17
    color: white;
18
18
}
19
19
20
20
.event-update {
+
21
    background-color: yellow;
+
22
    color: red;
+
23
    border: medium dotted red;
+
24
}
+
25
.event-new {
+
26
    background-color: white;
+
27
    color: black;
+
28
    border: medium dashed black;
+
29
}
+
30
.event-note {
+
31
    color: purple;
+
32
    border: medium solid purple;
+
33
}
+
34
+
35