joeni

Further bug fixes on roster and model validation

The roster code is a bit shitty at the moment, but it gets the job done properly and that's the most important thing.
I've added extensive model validation to both RoomReservation and CourseEvent, so that it's impossible to reservate a room that would cause an overlap of reservations.
Style for event with notification has been changed to double purple border for visibility purposes.

Author
Maarten 'Vngngdn' Vangeneugden
Date
Feb. 4, 2018, 10:15 p.m.
Hash
ac4bd94c88e9964d2df6e82c0832d7a22821639a
Parent
838a4c30e3ac78883df4d4a301ef8929e5be6ffe
Modified files
administration/migrations/0014_auto_20180204_2007.py
administration/migrations/0015_auto_20180204_2009.py
administration/migrations/0016_auto_20180204_2014.py
administration/models.py
administration/roster.py
administration/templates/administration/roster.djhtml
administration/views.py
static/css/base.css

administration/migrations/0014_auto_20180204_2007.py

50 additions and 0 deletions.

View changes Hide changes
+
1
+
2
import administration.models
+
3
import datetime
+
4
from django.db import migrations, models
+
5
+
6
+
7
class Migration(migrations.Migration):
+
8
+
9
    dependencies = [
+
10
        ('administration', '0013_auto_20180204_1444'),
+
11
    ]
+
12
+
13
    operations = [
+
14
        migrations.RenameField(
+
15
            model_name='userdata',
+
16
            old_name='user',
+
17
            new_name='user_data',
+
18
        ),
+
19
        migrations.RemoveField(
+
20
            model_name='roomreservation',
+
21
            name='start_time',
+
22
        ),
+
23
        migrations.AddField(
+
24
            model_name='roomreservation',
+
25
            name='begin_time',
+
26
            field=models.DateTimeField(default=datetime.datetime(2018, 2, 4, 20, 7, 35, 650784), help_text='The time that this reservation begin.', validators=[administration.models.validate_event_time, administration.models.validate_university_hours]),
+
27
            preserve_default=False,
+
28
        ),
+
29
        migrations.AlterField(
+
30
            model_name='event',
+
31
            name='begin_time',
+
32
            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, administration.models.validate_university_hours], verbose_name='begin time'),
+
33
        ),
+
34
        migrations.AlterField(
+
35
            model_name='event',
+
36
            name='end_time',
+
37
            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, but it must end on the same day as it begins!", validators=[administration.models.validate_event_time, administration.models.validate_university_hours], verbose_name='end time'),
+
38
        ),
+
39
        migrations.AlterField(
+
40
            model_name='roomreservation',
+
41
            name='end_time',
+
42
            field=models.DateTimeField(help_text='The time that this reservation ends.', validators=[administration.models.validate_event_time, administration.models.validate_university_hours]),
+
43
        ),
+
44
        migrations.AlterField(
+
45
            model_name='roomreservation',
+
46
            name='seats',
+
47
            field=models.PositiveSmallIntegerField(blank=True, help_text='Indicates how many seats are required. If this is left empty, it is assumed the entire room has to be reserved.', null=True),
+
48
        ),
+
49
    ]
+
50

administration/migrations/0015_auto_20180204_2009.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', '0014_auto_20180204_2007'),
+
9
    ]
+
10
+
11
    operations = [
+
12
        migrations.RenameField(
+
13
            model_name='userdata',
+
14
            old_name='user_data',
+
15
            new_name='user',
+
16
        ),
+
17
    ]
+
18

administration/migrations/0016_auto_20180204_2014.py

48 additions and 0 deletions.

View changes Hide changes
+
1
+
2
from django.db import migrations, models
+
3
+
4
+
5
class Migration(migrations.Migration):
+
6
+
7
    dependencies = [
+
8
        ('administration', '0015_auto_20180204_2009'),
+
9
    ]
+
10
+
11
    operations = [
+
12
        migrations.AlterField(
+
13
            model_name='userdata',
+
14
            name='study_cellphone',
+
15
            field=models.CharField(help_text='The cellphone number of the person. Prefix 0 can be presented with then national call code in the system.', max_length=64, null=True),
+
16
        ),
+
17
        migrations.AlterField(
+
18
            model_name='userdata',
+
19
            name='study_country',
+
20
            field=models.CharField(blank=True, max_length=64, null=True),
+
21
        ),
+
22
        migrations.AlterField(
+
23
            model_name='userdata',
+
24
            name='study_number',
+
25
            field=models.PositiveSmallIntegerField(blank=True, null=True),
+
26
        ),
+
27
        migrations.AlterField(
+
28
            model_name='userdata',
+
29
            name='study_postal_code',
+
30
            field=models.PositiveSmallIntegerField(blank=True, null=True),
+
31
        ),
+
32
        migrations.AlterField(
+
33
            model_name='userdata',
+
34
            name='study_street',
+
35
            field=models.CharField(blank=True, max_length=64, null=True),
+
36
        ),
+
37
        migrations.AlterField(
+
38
            model_name='userdata',
+
39
            name='study_telephone',
+
40
            field=models.CharField(blank=True, help_text='The telephone number for the study address. Prefix 0 can be presented with the national call code in the system.', max_length=64, null=True),
+
41
        ),
+
42
        migrations.AlterField(
+
43
            model_name='userdata',
+
44
            name='titularis_telephone',
+
45
            field=models.CharField(blank=True, help_text='The telephone number of the titularis. Prefix 0 can be presented with the national call code in the system.', max_length=64, null=True),
+
46
        ),
+
47
    ]
+
48

administration/models.py

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

administration/roster.py

8 additions and 4 deletions.

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

administration/templates/administration/roster.djhtml

7 additions and 4 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
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
23
23
    <dl>
24
24
        <dt><span class="event-update">
25
25
            {% trans "Recent event update" %}
26
26
        </span></dt>
27
27
        <dd>
28
28
            {% blocktrans %}
29
29
                This event had one or more of its properties changed
30
30
                in the last five days. This can be the room, the hours, the subject, ...
31
31
                You're encouraged to take note of that.
32
32
            {% endblocktrans %}
33
33
        </dd>
34
34
        <dt><span class="event-new">
35
35
            {% trans "New event" %}
36
36
        </span></dt>
37
37
        <dd>
38
38
            {% blocktrans %}
39
39
                This is a new event, added in the last five days.
40
40
            {% endblocktrans %}
41
41
        </dd>
42
42
        <dt><span class="event-note">
43
43
            {% trans "Notification available" %}
44
44
        </span></dt>
45
45
        <dd>
46
46
            {% blocktrans %}
47
47
                This event has a note attached to it by the docent. Hover over
48
48
                the event to display the note.
49
49
            {% endblocktrans %}
50
50
        </dd>
51
51
    </dl>
52
52
53
53
    <h2>{% trans "Main hour roster" %}</h2>
54
54
    <style>
55
-
        table tr td {
56
-
        border: medium solid red;
57
-
        }
+
55
        table td {
+
56
            border-width: 0px 0px 1px 0px;
+
57
            border-bottom-style: solid;
+
58
            border-style: solid;
+
59
            border-color: red;
+
60
        }
58
61
    </style>
59
-
    <table>
+
62
    <table>
60
63
        <th>
61
64
            {#<td></td> {# Empty row for hours #} {# Apparantly this isn't necessary with <th /> #}
62
65
            {% for day in days %}
63
66
                <td>{{ day|date:"l (d/m)" }}</td>
64
67
            {% endfor %}
65
68
        </th>
66
69
        {% for element in time_blocks %}
67
70
            {{ element|safe }}
68
71
            <!--<tr>
69
72
                {% if hour == "hour" %}
70
73
                    <td>{{ time }}</td>
71
74
                {% else %}
72
75
                    <td></td>
73
76
                {% endif %}
74
77
                {% cycle hour %}
75
78
                <td>{{ time }}</td>
76
79
                <td>{{ event }}</td>
77
80
            </tr>-->
78
81
        {% endfor %}
79
82
    </table>
80
83
{% endblock main %}
81
84

administration/views.py

0 additions and 15 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
from .roster import create_roster_rows
9
9
import administration
10
10
from django.contrib.auth.decorators import login_required
11
11
from django.contrib.auth import authenticate
12
12
13
13
@login_required
14
14
def roster(request, begin=None, end=None):
15
15
    """Collects and renders the data that has to be displayed in the roster.
16
16
17
17
    The begin and end date can be specified. Only roster points in that range
18
18
    will be included in the response. If no begin and end are specified, it will
19
19
    take the current week as begin and end point. If it's
20
20
    weekend, it will take next week."""
21
21
22
22
    # TODO Handle given begin and end
23
23
    context = dict()
24
24
    template = "administration/roster.djhtml"
25
25
26
26
    if begin is None or end is None:
27
27
        today = datetime.date.today()
28
28
        if today.isoweekday() in {6,7}:  # Weekend
29
29
            begin = today + datetime.timedelta(days=8-today.isoweekday())
30
30
            end = today + datetime.timedelta(days=13-today.isoweekday())
31
31
        else:  # Same week
32
32
            begin = today - datetime.timedelta(days=today.weekday())
33
33
            end = today + datetime.timedelta(days=5-today.isoweekday())
34
34
    context['begin'] = begin
35
35
    context['end'] = end
36
36
37
37
    days = [begin]
38
38
    while (end-days[-1]).days != 0:
39
39
        # Human translation: Keep adding days until the last day in the array of
40
40
        # days is the same day as the last day the user wants to see the roster for.
41
41
        days.append(days[-1] + datetime.timedelta(days=1))
42
42
    context['days'] = days
43
43
44
44
    # Collecting events
45
45
    course_events = CourseEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end).order_by("begin_time")
46
46
    #university_events = UniversityEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
47
47
    #study_events = StudyEvent.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
48
48
    #events = Event.objects.filter(begin_time__gte=begin).filter(end_time__lte=end)
49
49
    table_code = create_roster_rows(course_events)
50
50
51
51
    # Producing time blocks for display in the table
52
-
    times = []
53
-
    for i in range(8, 20):
54
-
        time = str(i)
55
-
        for j in range(0, 60, 15):
56
-
            if j == 0:
57
-
                times.append(time + ":00")
58
-
            else:
59
-
                times.append(time + ":" + str(j))
60
-
61
-
    time_blocks = list()
62
-
    for i in range(len(times)):
63
-
        time_blocks.append([times[i],table_code[i]])
64
-
65
-
    #context['time_blocks'] = time_blocks
66
-
    context['time_blocks'] = table_code
67
52
    #print(time_blocks)
68
53
    return render(request, template, context)
69
54
    # TODO Finish!
70
55
71
56
def index(request):
72
57
    template = "administration/index.djhtml"
73
58
    context = {}
74
59
    return render(request, template, context)
75
60
76
61
    pass
77
62
78
63
def pre_registration(request):
79
64
    user_data_form = UserDataForm()
80
65
    template = "administration/pre_registration.djhtml"
81
66
    context = dict()
82
67
83
68
    if request.method == 'POST':
84
69
        user_data_form = UserDataForm(request.POST)
85
70
        context['user_data_form'] = user_data_form
86
71
        if user_data_form.is_valid():
87
72
            user_data_form.save()
88
73
            context['messsage'] = _("Your registration has been completed. You will receive an e-mail shortly.")
89
74
        else:
90
75
            context['messsage'] = _("The data you supplied had errors. Please review your submission.")
91
76
    else:
92
77
        context['user_data_form'] = UserDataForm(instance = user_data)
93
78
94
79
    return render(request, template, context)
95
80
    pass
96
81
97
82
@login_required
98
83
def settings(request):
99
84
    user_data = UserData.objects.get(user=request.user)
100
85
    user_data_form = UserDataForm(instance = user_data)
101
86
    template = "administration/settings.djhtml"
102
87
    context = dict()
103
88
104
89
    if request.method == 'POST':
105
90
        user_data_form = UserDataForm(request.POST, instance = user_data)
106
91
        context['user_data_form'] = user_data_form
107
92
        if user_data_form.is_valid():
108
93
            user_data_form.save()
109
94
            context['messsage'] = _("Your settings were successfully updated.")
110
95
        else:
111
96
            context['messsage'] = _("The data you supplied had errors. Please review your submission.")
112
97
    else:
113
98
        context['user_data_form'] = UserDataForm(instance = user_data)
114
99
115
100
    return render(request, template, context)
116
101
117
102
@login_required
118
103
def bulletin_board(request):
119
104
    context = dict()
120
105
    context['exam_commission_decisions'] = ExamCommissionDecision.objects.filter(user=request.user)
121
106
    context['education_department_messages'] = ExamCommissionDecision.objects.filter(user=request.user)
122
107
    template = "administration/bulletin_board.djhtml"
123
108
    return render(request, template, context)
124
109
125
110
def jobs(request):
126
111
    context = dict()
127
112
    template = "administration/jobs.djhtml"
128
113
    #@context['decisions'] = ExamCommissionDecision.objects.filter(user=request.user)
129
114
    return render(request, template, context)
130
115
131
116
132
117
def curriculum(request):
133
118
    return render(request, template, context)
134
119
135
120
def result(request):
136
121
    return render(request, template, context)
137
122
138
123
@login_required
139
124
def results(request):
140
125
    results = CourseResult.objects.filter(student=request.user)
141
126
    template = "administration/results.djhtml"
142
127
    # TODO
143
128
    return render(request, template, context)
144
129
145
130
def forms(request):
146
131
    return render(request, template, context)
147
132
148
133
def rooms(request):
149
134
    template = "administration/rooms.djhtml"
150
135
    return render(request, template, context)
151
136
152
137
def room_reservate(request):
153
138
    return render(request, template, context)
154
139
155
140
def login(request):
156
141
    context = dict()
157
142
    if request.method == "POST":
158
143
        name = request.POST['name']
159
144
        passphrase = request.POST['pass']
160
145
        user = authenticate(username=name, password=passphrase)
161
146
        if user is not None: # The user was successfully authenticated
162
147
            print("YA")
163
148
            return HttpResponseRedirect(request.POST['next'])
164
149
        else: # User credentials were wrong
165
150
            context['next'] = request.POST['next']
166
151
            context['message'] = _("The given credentials were not correct.")
167
152
    else:
168
153
        context['next'] = request.GET.get('next', None)
169
154
        if context['next'] is None:
170
155
            context['next'] = reverse('administration-index')
171
156
172
157
    template = 'administration/login.djhtml'
173
158
174
159
    return render(request, template, context)
175
160

static/css/base.css

1 addition and 1 deletion.

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
21
    background-color: yellow;
22
22
    color: red;
23
23
    border: medium dotted red;
24
24
}
25
25
.event-new {
26
26
    background-color: white;
27
27
    color: black;
28
28
    border: medium dashed black;
29
29
}
30
30
.event-note {
31
31
    color: purple;
32
32
    border: medium solid purple;
33
-
}
+
33
}
34
34