joeni

Lots of changes regarding administration app

These should've better been in different commits, but I forgot about doing that. Nevertheless, here's a list of all the major changes that went in this commit:
- Added forms to the administration app. This will gradually expand to include all forms that can be displayed in to the users. - Changes to the models. The User has been split into a User and a UserData model, the latter holding all relevant data such as addresses and billing accounts. Some new models have been added, such as events (will be displayed in the roster), and decisions from the exam committee can now be stored as well. - Migrations of the mentioned model changes have been included. - A *lot* of new templates. These templates are almost all for the administration app. Their goals are self-explanatory. Most of them have their URLs already added, although I may have forgotten some of them. - New view functions. Some are already completely finished (like settings() and exam_commission_decisions()), others only have a pass for now. - New base templates. These will be used in almost all templates, so they look very general and flexible. - django-crispy-forms *has dishonorably been removed.* I thought I had something cool here, but when I got to work with it, it only supported JS-infected CSS/JS libraries like Bootstrap, which *COMPLETELY* defeated the purpose and benefits that it offers. I took my chances with Django's standard form system and was ashamed of myself that it worked so well, and that I had the audacity to distrust it before even doing something sensible with it first. I do need a little bit more of code than with crispy, but most of the extra code is tucked away in a seperate form for exactly that purpose. And now I actually *CAN* use simple HTML5 and CSS to make everything work properly, instead of using Bootstrap.

Author
Maarten 'Vngngdn' Vangeneugden
Date
Jan. 28, 2018, 9:37 p.m.
Hash
f454425f58f752e0d4a2b49fcba4bd7215383d8f
Parent
389f9833d742c0fd04de07f7f999cccb62e5dda4
Modified files
administration/admin.py
administration/forms.py
administration/migrations/0010_auto_20180124_1712.py
administration/migrations/0011_auto_20180128_1935.py
administration/models.py
administration/templates/administration/exam_commission.djhtml
administration/templates/administration/index.djhtml
administration/templates/administration/login.djhtml
administration/templates/administration/nav.djhtml
administration/templates/administration/public.djhtml
administration/templates/administration/roster.djhtml
administration/templates/administration/settings.djhtml
administration/urls.py
administration/views.py
courses/urls.py
courses/views.py
joeni/settings.py
joeni/templates/joeni/base.djhtml
joeni/templates/joeni/footer.djhtml
joeni/templates/joeni/header.djhtml
joeni/urls.py

administration/admin.py

4 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(CourseResult)
+
8
admin.site.register(CourseResult)
8
9
admin.site.register(PreRegistration)
9
10
admin.site.register(Room)
10
11
admin.site.register(RoomReservation)
11
12
admin.site.register(Degree)
12
13
13
14
admin.site.register(CourseEvent)
+
15
admin.site.register(UniversityEvent)
+
16
admin.site.register(StudyEvent)
+
17

administration/forms.py

33 additions and 0 deletions.

View changes Hide changes
+
1
from django.forms import ModelForm
+
2
from . import models
+
3
+
4
class UserDataForm(ModelForm):
+
5
    class Meta:
+
6
        model = models.UserData
+
7
        fields = ['first_name',
+
8
                  'last_name',
+
9
                  'title',
+
10
                  'nationality',
+
11
                  'civil_status',
+
12
                  'home_street',
+
13
                  'home_number',
+
14
                  'home_bus',
+
15
                  'home_postal_code',
+
16
                  'home_country',
+
17
                  'home_telephone',
+
18
                  'study_street',
+
19
                  'study_number',
+
20
                  'study_bus',
+
21
                  'study_postal_code',
+
22
                  'study_country',
+
23
                  'study_telephone',
+
24
                  'study_cellphone',
+
25
                  'titularis_street',
+
26
                  'titularis_number',
+
27
                  'titularis_bus',
+
28
                  'titularis_postal_code',
+
29
                  'titularis_country',
+
30
                  'titularis_telephone',
+
31
                  'bank_account_number',
+
32
                  ]
+
33

administration/migrations/0010_auto_20180124_1712.py

185 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', '0009_auto_20180124_0049'),
+
12
    ]
+
13
+
14
    operations = [
+
15
        migrations.CreateModel(
+
16
            name='UserData',
+
17
            fields=[
+
18
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
19
                ('first_name', models.CharField(max_length=64)),
+
20
                ('last_name', models.CharField(max_length=64)),
+
21
                ('title', models.CharField(blank=True, help_text='The academic title of this user, if applicable.', max_length=64)),
+
22
                ('DOB', models.DateField(help_text='The date of birth of this user.')),
+
23
                ('POB', models.CharField(help_text='The place of birth of this user.', max_length=64)),
+
24
                ('nationality', models.CharField(help_text='The current nationality of this user.', max_length=64)),
+
25
                ('national_registry_number', models.BigIntegerField(help_text='The assigned national registry number of this user.', unique=True)),
+
26
                ('civil_status', models.CharField(choices=[('Single', 'Single'), ('Married', 'Married'), ('Divorced', 'Divorced'), ('Widowed', 'Widowed'), ('Partnership', 'Partnership')], help_text='The civil/marital status of the user.', max_length=32)),
+
27
                ('is_staff', models.BooleanField(default=False, help_text="Determines if this user is part of the university's staff.")),
+
28
                ('is_student', models.BooleanField(default=True, help_text='Indicates if this user is a student at the university.')),
+
29
                ('home_street', models.CharField(max_length=64)),
+
30
                ('home_number', models.PositiveSmallIntegerField()),
+
31
                ('home_bus', models.PositiveSmallIntegerField(null=True)),
+
32
                ('home_postal_code', models.PositiveSmallIntegerField()),
+
33
                ('home_country', models.CharField(max_length=64)),
+
34
                ('home_telephone', models.CharField(help_text='The telephone number for the house address. Prefix 0 can be presented with the national call code in the system.', max_length=64)),
+
35
                ('study_street', models.CharField(max_length=64)),
+
36
                ('study_number', models.PositiveSmallIntegerField()),
+
37
                ('study_bus', models.PositiveSmallIntegerField(null=True)),
+
38
                ('study_postal_code', models.PositiveSmallIntegerField()),
+
39
                ('study_country', models.CharField(max_length=64)),
+
40
                ('study_telephone', models.CharField(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)),
+
41
                ('study_cellphone', 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)),
+
42
                ('titularis_street', models.CharField(max_length=64, null=True)),
+
43
                ('titularis_number', models.PositiveSmallIntegerField(null=True)),
+
44
                ('titularis_bus', models.PositiveSmallIntegerField(null=True)),
+
45
                ('titularis_postal_code', models.PositiveSmallIntegerField(null=True)),
+
46
                ('titularis_country', models.CharField(max_length=64, null=True)),
+
47
                ('titularis_telephone', models.CharField(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)),
+
48
                ('bank_account_number', models.CharField(help_text='The IBAN of this user. No spaces!', max_length=34, validators=[administration.models.validate_IBAN])),
+
49
                ('BIC', models.CharField(help_text="The BIC of this user's bank.", max_length=11, validators=[administration.models.validate_BIC])),
+
50
            ],
+
51
        ),
+
52
        migrations.RemoveField(
+
53
            model_name='user',
+
54
            name='BIC',
+
55
        ),
+
56
        migrations.RemoveField(
+
57
            model_name='user',
+
58
            name='DOB',
+
59
        ),
+
60
        migrations.RemoveField(
+
61
            model_name='user',
+
62
            name='POB',
+
63
        ),
+
64
        migrations.RemoveField(
+
65
            model_name='user',
+
66
            name='bank_account_number',
+
67
        ),
+
68
        migrations.RemoveField(
+
69
            model_name='user',
+
70
            name='civil_status',
+
71
        ),
+
72
        migrations.RemoveField(
+
73
            model_name='user',
+
74
            name='home_bus',
+
75
        ),
+
76
        migrations.RemoveField(
+
77
            model_name='user',
+
78
            name='home_country',
+
79
        ),
+
80
        migrations.RemoveField(
+
81
            model_name='user',
+
82
            name='home_number',
+
83
        ),
+
84
        migrations.RemoveField(
+
85
            model_name='user',
+
86
            name='home_postal_code',
+
87
        ),
+
88
        migrations.RemoveField(
+
89
            model_name='user',
+
90
            name='home_street',
+
91
        ),
+
92
        migrations.RemoveField(
+
93
            model_name='user',
+
94
            name='home_telephone',
+
95
        ),
+
96
        migrations.RemoveField(
+
97
            model_name='user',
+
98
            name='is_student',
+
99
        ),
+
100
        migrations.RemoveField(
+
101
            model_name='user',
+
102
            name='national_registry_number',
+
103
        ),
+
104
        migrations.RemoveField(
+
105
            model_name='user',
+
106
            name='nationality',
+
107
        ),
+
108
        migrations.RemoveField(
+
109
            model_name='user',
+
110
            name='study_bus',
+
111
        ),
+
112
        migrations.RemoveField(
+
113
            model_name='user',
+
114
            name='study_cellphone',
+
115
        ),
+
116
        migrations.RemoveField(
+
117
            model_name='user',
+
118
            name='study_country',
+
119
        ),
+
120
        migrations.RemoveField(
+
121
            model_name='user',
+
122
            name='study_number',
+
123
        ),
+
124
        migrations.RemoveField(
+
125
            model_name='user',
+
126
            name='study_postal_code',
+
127
        ),
+
128
        migrations.RemoveField(
+
129
            model_name='user',
+
130
            name='study_street',
+
131
        ),
+
132
        migrations.RemoveField(
+
133
            model_name='user',
+
134
            name='study_telephone',
+
135
        ),
+
136
        migrations.RemoveField(
+
137
            model_name='user',
+
138
            name='title',
+
139
        ),
+
140
        migrations.RemoveField(
+
141
            model_name='user',
+
142
            name='titularis_bus',
+
143
        ),
+
144
        migrations.RemoveField(
+
145
            model_name='user',
+
146
            name='titularis_country',
+
147
        ),
+
148
        migrations.RemoveField(
+
149
            model_name='user',
+
150
            name='titularis_number',
+
151
        ),
+
152
        migrations.RemoveField(
+
153
            model_name='user',
+
154
            name='titularis_postal_code',
+
155
        ),
+
156
        migrations.RemoveField(
+
157
            model_name='user',
+
158
            name='titularis_street',
+
159
        ),
+
160
        migrations.RemoveField(
+
161
            model_name='user',
+
162
            name='titularis_telephone',
+
163
        ),
+
164
        migrations.AlterField(
+
165
            model_name='user',
+
166
            name='first_name',
+
167
            field=models.CharField(blank=True, max_length=30, verbose_name='first name'),
+
168
        ),
+
169
        migrations.AlterField(
+
170
            model_name='user',
+
171
            name='is_staff',
+
172
            field=models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status'),
+
173
        ),
+
174
        migrations.AlterField(
+
175
            model_name='user',
+
176
            name='last_name',
+
177
            field=models.CharField(blank=True, max_length=150, verbose_name='last name'),
+
178
        ),
+
179
        migrations.AddField(
+
180
            model_name='userdata',
+
181
            name='user',
+
182
            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
+
183
        ),
+
184
    ]
+
185

administration/migrations/0011_auto_20180128_1935.py

94 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', '0010_auto_20180124_1712'),
+
9
    ]
+
10
+
11
    operations = [
+
12
        migrations.AddField(
+
13
            model_name='userdata',
+
14
            name='home_city',
+
15
            field=models.CharField(default='Diepenbeek', max_length=64),
+
16
            preserve_default=False,
+
17
        ),
+
18
        migrations.AlterField(
+
19
            model_name='userdata',
+
20
            name='home_bus',
+
21
            field=models.CharField(blank=True, max_length=10, null=True),
+
22
        ),
+
23
        migrations.AlterField(
+
24
            model_name='userdata',
+
25
            name='home_country',
+
26
            field=models.CharField(default='België', max_length=64),
+
27
        ),
+
28
        migrations.AlterField(
+
29
            model_name='userdata',
+
30
            name='home_postal_code',
+
31
            field=models.PositiveIntegerField(),
+
32
        ),
+
33
        migrations.AlterField(
+
34
            model_name='userdata',
+
35
            name='home_telephone',
+
36
            field=models.CharField(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).', max_length=64),
+
37
        ),
+
38
        migrations.AlterField(
+
39
            model_name='userdata',
+
40
            name='national_registry_number',
+
41
            field=models.BigIntegerField(blank=True, help_text='The assigned national registry number of this user.'),
+
42
        ),
+
43
        migrations.AlterField(
+
44
            model_name='userdata',
+
45
            name='nationality',
+
46
            field=models.CharField(default='Belg', help_text='The current nationality of this user.', max_length=64),
+
47
        ),
+
48
        migrations.AlterField(
+
49
            model_name='userdata',
+
50
            name='study_bus',
+
51
            field=models.CharField(blank=True, max_length=10, null=True),
+
52
        ),
+
53
        migrations.AlterField(
+
54
            model_name='userdata',
+
55
            name='study_country',
+
56
            field=models.CharField(blank=True, max_length=64),
+
57
        ),
+
58
        migrations.AlterField(
+
59
            model_name='userdata',
+
60
            name='study_number',
+
61
            field=models.PositiveSmallIntegerField(blank=True),
+
62
        ),
+
63
        migrations.AlterField(
+
64
            model_name='userdata',
+
65
            name='study_postal_code',
+
66
            field=models.PositiveSmallIntegerField(blank=True),
+
67
        ),
+
68
        migrations.AlterField(
+
69
            model_name='userdata',
+
70
            name='study_street',
+
71
            field=models.CharField(blank=True, max_length=64),
+
72
        ),
+
73
        migrations.AlterField(
+
74
            model_name='userdata',
+
75
            name='study_telephone',
+
76
            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),
+
77
        ),
+
78
        migrations.AlterField(
+
79
            model_name='userdata',
+
80
            name='titularis_bus',
+
81
            field=models.CharField(blank=True, max_length=10, null=True),
+
82
        ),
+
83
        migrations.AlterField(
+
84
            model_name='userdata',
+
85
            name='titularis_country',
+
86
            field=models.CharField(blank=True, max_length=64, null=True),
+
87
        ),
+
88
        migrations.AlterField(
+
89
            model_name='userdata',
+
90
            name='titularis_street',
+
91
            field=models.CharField(blank=True, max_length=64, null=True),
+
92
        ),
+
93
    ]
+
94

administration/models.py

41 additions and 13 deletions.

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

administration/templates/administration/exam_commission.djhtml

48 additions and 0 deletions.

View changes Hide changes
+
1
{% load i18n %}
+
2
{% load humanize %}
+
3
+
4
{% block title %}
+
5
    {% trans "Decisions of the exam commission" %} | ◀ Joeni /▶
+
6
{% endblock %}
+
7
+
8

+
9
    {% blocktrans %}
+
10
        Whenever the exam commission comes together to make a decision regarding
+
11
        you, the eventual decision resulting from the meetings will be posted
+
12
        here.
+
13
    {% endblocktrans %}
+
14

+
15
{% for decision in decisions %}
+
16
    
+
17
    
+
18
    {{ decision.text|org }}
+
19
    
+
20
{% empty %}
+
21
    

+
22
        {% trans "There are no decisions that affect you." %}
+
23
    

+
24
    
+
25
{% endfor %}
+
26
+
27

+
28
    {% blocktrans %} {# TODO #}
+
29
        Raadpleeg het onderwijs- en examenreglement
+
30
+
31
        Op grond van de Rechtspositieregeling in de OER-regeling voor studenten van de UHasselt/tUL kan een student die oordeelt dat een ongunstige studievoortgangsbeslissing (omschreven in Art. 1.2 van de Rechtspositieregeling) aangetast is door een schending van het recht, intern beroep aantekenen, voor zover dit geen voorwerp was van een eerder beroep. Dit beroep wordt formeel ingediend bij de secretaris van de beroepscommissie conform Art. 1.3 lid 4 van de Rechtspositieregeling op volgend adres:
+
32
+
33
        Secretaris interne beroepscommissie
+
34
        Lien Mampaey
+
35
        Universiteit Hasselt
+
36
        Martelarenlaan 42
+
37
        B-3500 Hasselt
+
38
+
39
        Om administratieve redenen wordt de student verzocht om het beroep ook te melden op het volgende emailadres:intern.beroep@uhasselt.be. Het verzoekschrift wordt op straffe van niet-ontvankelijkheid ingediend per aangetekend schrijven binnen een vervaltermijn van 7 kalenderdagen, die ingaat op de dag na de kennisgeving van de genomen studievoortgangsbeslissing aan de student. Als datum van het beroep geldt de datum van het postmerk van de aangetekende zending. Het verzoekschrift van de student omvat op straffe van niet-ontvankelijkheid tenminste:
+
40
+
41
            De naam, een correspondentieadres en de handtekening van de student of zijn raadsman;
+
42
            een vermelding van de beslissing waartegen het beroep gericht is en alle relevante (bewijs)stukken;
+
43
            een omschrijving van de ingeroepen bezwaren.
+
44
    {% endblocktrans %}
+
45

+
46
+
47
+
48

administration/templates/administration/index.djhtml

59 additions and 0 deletions.

View changes Hide changes
+
1
{% load i18n %}
+
2
{% load humanize %}
+
3
{% load crispy_forms_tags %}
+
4
{% load joeni_org %}
+
5
+
6
{% block title %}
+
7
    {% trans "Administration" %} | ◀ Joeni /▶
+
8
{% endblock %}
+
9
+
10
{% block main %}
+
11
    {% include "administration/nav.djhtml" %}
+
12
    

{% trans "Administration" %}

+
13
    

+
14
        {% blocktrans %}
+
15
            Welcome to the administration website of Hasselt University. Here,
+
16
            you can find all links related to the university's services,
+
17
            events, messages, and so on.
+
18
        {% endblocktrans %}
+
19
    

+
20
+
21
    

{% trans "Education department bulletin board" %}

+
22
    {% for message in education_dept_messages %}
+
23
        

{{message.title}}

+
24
        
+
25
            {{ message.date|naturaltime }}
+
26
        
+
27
        

{{message.text|org}}

+
28
    {% empty %}
+
29
        

{% trans "There are no messages available." %}

+
30
    {% endfor %}
+
31
+
32
    

{% trans "Important telephone numbers and contact services" %}

+
33
    
+
34
        
{% trans "Student secretary during working hours" %}
+
35
        
(+32)11 26 81 00
+
36
        
{% trans "Student police | District office" %}
+
37
        
(+32)11 26 81 15
+
38
        
{% trans "Student police | District service" %}
+
39
        
(+32)11 32 33 00
+
40
    
+
41
    {% comment %}
+
42
        Psychosociale opvang binnen de kantooruren
+
43
        Studentenpsycholoog:studentenpsycholoog@uhasselt.be - tel.:011 26 90 48
+
44
        Maatschappelijk assistent: Liesbeth Huber
+
45
        Logistieke vragen buiten de kantooruren
+
46
        Dienst MAT Diepenbeek: 0475 94 30 02
+
47
        Dienst MAT Hasselt: 0493 59 38 33
+
48
        Studentenpolitie
+
49
        Wijkkantoor: 011 26 81 15
+
50
        Wijkdienst: 011 32 33 00
+
51
        0499 59 57 28
+
52
        Tele-Onthaal (24u/24u)
+
53
        106 
+
54
    #TODO
+
55
    {% endcomment %}
+
56
+
57
{% endblock main %}
+
58
+
59

administration/templates/administration/login.djhtml

53 additions and 0 deletions.

View changes Hide changes
+
1
{% load i18n %}
+
2
+
3
{% block title %}
+
4
    {% trans "Log in | ◀ Joeni /▶" %}
+
5
{% endblock %}
+
6
+
7
{% block main %}
+
8
    

{% trans "Authenticate with Joeni" %}

+
9
    

+
10
        {% blocktrans %}
+
11
            This page is only visible for students and personnel of Hasselt University.
+
12
            Please authenticate yourself with Joeni in order to continue.
+
13
        {% endblocktrans %}
+
14
    

+
15
+
16
+
43
+
44
    {% csrf_token %} {# This is necessary for forms, CSRF protection. #}
+
45
    
+
46
    
+
47
    
+
48
    
+
49
    
+
50
    
+
51
+
52
{% endblock main %}
+
53

administration/templates/administration/nav.djhtml

10 additions and 0 deletions.

View changes Hide changes
+
1
{% load i18n %}
+
2
+
3
    {% trans "Personal settings" %}
+
4
    {% trans "Curricula" %}
+
5
    {% trans "Course results" %}
+
6
    {% trans "Forms" %}
+
7
    {% trans "Rooms" %}
+
8
    {% trans "Personal Roster" %}
+
9
+
10

administration/templates/administration/public.djhtml

18 additions and 0 deletions.

View changes Hide changes
+
1
{# Redirect to this template when there is no account logged in #}
+
2
{% load i18n %}
+
3
+
4
{% block title %}
+
5
    {% trans "▶▶ Hasselt University | Administration" %}
+
6
{% endblock %}
+
7
+
8
{% block main %}
+
9
    {% include "administration/nav.djhtml" %}
+
10
    

{% trans "Administration" %}

+
11
    

+
12
        {% blocktrans %}
+
13
            Welcome to the administration system of Hasselt University.
+
14
        {% endblocktrans %}
+
15
    

+
16
{% endblock main %}
+
17
+
18

administration/templates/administration/roster.djhtml

12 additions and 0 deletions.

View changes Hide changes
+
1
{% load i18n %}
+
2
+
3
{% block title %}
+
4
    {% trans "Roster | ◀ Joeni /▶" %}
+
5
{% endblock %}
+
6
+
7
{% block main %}
+
8
    {% include "administration/nav.djhtml" %}
+
9
    

{% trans "" %}

+
10
{% endblock main %}
+
11
+
12

administration/templates/administration/settings.djhtml

34 additions and 0 deletions.

View changes Hide changes
+
1
{% load i18n %}
+
2
{% load humanize %}
+
3
+
4
{% block title %}
+
5
    {% trans "Settings" %} | ◀ Joeni /▶
+
6
{% endblock %}
+
7
+
8
{% block main %}
+
9
    {% include "administration/nav.djhtml" %}
+
10
    

{% trans "Settings" %}

+
11
    

{% trans "Guidelines" %}

+
12
    

+
13
        {% blocktrans %}
+
14
            On this screen, you can edit your personal details regarding your
+
15
            address, nationality, and other details.
+
16
        {% endblocktrans %}
+
17
    

+
18
    

+
19
        {% blocktrans %}
+
20
            The address data of where you study and the titularis address only
+
21
            need to be filled out if it differs from your home address. If not,
+
22
            you can leave those fields blank.
+
23
        {% endblocktrans %}
+
24
    

+
25
+
26
    {#{% crispy user_data_form %}#}
+
27
    
+
28
        {% csrf_token %} {# TODO I don't think that's necessary here #}
+
29
        {% include "joeni/form.djhtml" with form=user_data_form %}
+
30
        
+
31
    
+
32
{% endblock main %}
+
33
+
34

administration/urls.py

8 additions and 5 deletions.

View changes Hide changes
1
1
from . import views
+
2
from . import views
2
3
from django.utils.translation import ugettext_lazy as _
3
-
+
4
4
5
urlpatterns = [
5
-
    path('index', views.index, name='administration-index'),
6
-
    path(_('pre-registration'), views.pre_registration, name='administration-pre-registration'),
+
6
    path('', views.index, name='administration-index'),
+
7
    path(_('pre-registration'), views.pre_registration, name='administration-pre-registration'),
7
8
    path(_('settings'), views.settings, name='administration-settings'),
8
9
    path(_('curriculum'), views.curriculum, name='administration-curriculum'),
9
10
    path(_('results'), views.results, name='administration-results'),
10
11
    path(_('results/<slug:course>'), views.result, name='administration-results'),
11
12
    path(_('results/<int:student_id>'), views.result, name='administration-results'),
12
13
    path(_('forms'), views.forms, name='administration-forms'),  # In Dutch: "Attesten"
13
14
    path(_('forms/<str:form>'), views.forms, name='administration-forms'),
14
15
    path(_('rooms'), views.rooms, name='administration-rooms'),
15
16
    path(_('rooms/<str:room>'), views.rooms, name='administration-rooms'),
16
17
    path(_('rooms/reservate'), views.room-reservate, name='administration-room-reservate'),
17
-
    path(_('roster'), views.roster, name='administration-roster'),
+
18
    path(_('roster'), views.roster, name='administration-roster'),
18
19
    ]
19
-
+
20
    path('login', views.login, name='administration-login'),
+
21
    ])
+
22

administration/views.py

113 additions and 8 deletions.

View changes Hide changes
1
1
import datetime
2
2
from django.urls import reverse # Why?
3
3
from django.utils.translation import gettext as _
4
4
from .models import *
5
5
import administration
+
6
import administration
6
7
from django.contrib.auth.decorators import login_required
7
8
8
9
@login_required
9
10
def roster(begin=None, end=None):
10
-
    """Collects and renders the data that has to be displayed in the roster.
+
11
    """Collects and renders the data that has to be displayed in the roster.
11
12
12
13
    The begin and end date can be specified. Only roster points in that range
13
14
    will be included in the response. If no begin and end are specified, it will
14
15
    take the current week as begin and end point. If it's
15
16
    weekend, it will take next week."""
16
17
    if begin is None or end is None:
17
18
        today = date.today()
18
-
        if today.isoweekday() in {6,7}:  # Weekend
+
19
        if today.isoweekday() in {6,7}:  # Weekend
19
20
            begin = today + date.timedelta(days=8-today.isoweekday())
20
-
            end = today + date.timedelta(days=13-today.isoweekday())
21
-
        else:  # Same week
+
21
            end = today + datetime.timedelta(days=13-today.isoweekday())
+
22
        else:  # Same week
22
23
            begin = today - date.timedelta(days=today.weekday())
23
-
            end = today + date.timedelta(days=5-today.isoweekday())
24
-
25
-
    
26
-
+
24
            end = today + datetime.timedelta(days=5-today.isoweekday())
+
25
27
26
        return roster
28
27
+
28
+
29
def index(request):
+
30
    template = "administration/index.djhtml"
+
31
    context = {}
+
32
    return render(request, template, context)
+
33
+
34
    pass
+
35
+
36
def pre_registration(request):
+
37
    user_data_form = UserDataForm()
+
38
    template = "administration/pre_registration.djhtml"
+
39
    context = dict()
+
40
+
41
    if request.method == 'POST':
+
42
        user_data_form = UserDataForm(request.POST)
+
43
        context['user_data_form'] = user_data_form
+
44
        if user_data_form.is_valid():
+
45
            user_data_form.save()
+
46
            context['messsage'] = _("Your registration has been completed. You will receive an e-mail shortly.")
+
47
        else:
+
48
            context['messsage'] = _("The data you supplied had errors. Please review your submission.")
+
49
    else:
+
50
        context['user_data_form'] = UserDataForm(instance = user_data)
+
51
+
52
    return render(request, template, context)
+
53
    pass
+
54
+
55
@login_required
+
56
def settings(request):
+
57
    user_data = UserData.objects.get(user=request.user)
+
58
    user_data_form = UserDataForm(instance = user_data)
+
59
    template = "administration/settings.djhtml"
+
60
    context = dict()
+
61
+
62
    if request.method == 'POST':
+
63
        user_data_form = UserDataForm(request.POST, instance = user_data)
+
64
        context['user_data_form'] = user_data_form
+
65
        if user_data_form.is_valid():
+
66
            user_data_form.save()
+
67
            context['messsage'] = _("Your settings were successfully updated.")
+
68
        else:
+
69
            context['messsage'] = _("The data you supplied had errors. Please review your submission.")
+
70
    else:
+
71
        context['user_data_form'] = UserDataForm(instance = user_data)
+
72
+
73
    return render(request, template, context)
+
74
+
75
@login_required
+
76
def exam_commission_decisions(request):
+
77
    context = dict()
+
78
    context['decisions'] = ExamCommissionDecision.objects.filter(user=request.user)
+
79
    template = "administration/exam_commission.djhtml"
+
80
    return render(request, template, context)
+
81
+
82
+
83
def curriculum(request):
+
84
    pass
+
85
+
86
def result(request):
+
87
    pass
+
88
+
89
def results(request):
+
90
    pass
+
91
+
92
def forms(request):
+
93
    pass
+
94
+
95
def rooms(request):
+
96
    pass
+
97
+
98
def room_reservate(request):
+
99
    pass
+
100
+
101
def login(request):
+
102
    if request.method == "POST":
+
103
        name = request.POST['name']
+
104
        passphrase = request.POST['password']
+
105
        user = authenticate(username=name, password=passphrase)
+
106
        if user is not None: # The user was successfully authenticated.
+
107
            login(request, user)
+
108
            # Because of Leen, I now track when and where is logged in:
+
109
            loginRecord = Login()
+
110
            loginRecord.ip = request.META['REMOTE_ADDR']
+
111
            loginRecord.name = name
+
112
            loginRecord.save()
+
113
            return HttpResponseRedirect(reverse('ITdays-index'))
+
114
+
115
    template = 'administration/login.djhtml'
+
116
+
117
    footer_links = [
+
118
            ["Home", "/"],
+
119
            ["Contact", "mailto:maarten.vangeneugden@student.uhasselt.be"],
+
120
            ]
+
121
+
122
    context = {
+
123
            'materialDesign_color': "deep-purple",
+
124
            'materialDesign_accentColor': "amber",
+
125
            'navbar_title': "Authentication",
+
126
            'navbar_fixed': True,
+
127
            'navbar_backArrow': True,
+
128
            'footer_title': "Quotebook",
+
129
            'footer_description': "Een lijst van citaten uit 2BACH Informatica @ UHasselt.",
+
130
            'footer_links': footer_links,
+
131
            }
+
132
    return render(request, template, context)
+
133

courses/urls.py

1 addition and 1 deletion.

View changes Hide changes
1
1
from . import views
2
2
from django.utils.translation import ugettext_lazy as _
3
3
4
4
urlpatterns = [
5
5
    path('index', views.main, name='courses-index'),
6
-
    path('<slug:course_slug>', views.course, name='courses-course-index'),
+
6
    path('<slug:course_slug>', views.course, name='courses-course-index'),
7
7
    path(_('<slug:course_slug>/<int:assignment_id>/upload'), views.upload, name='courses-upload'),
8
8
    path(_('<slug:course_slug>/new-item'), views.new_item, name='courses-new-item'),
9
9
    path(_('<slug:course_slug>/groups'), views.groups, name='courses-groups'),
10
10
    ]
11
11

courses/views.py

8 additions and 0 deletions.

View changes Hide changes
1
1
import datetime
2
2
from django.urls import reverse # Why?
3
3
from django.utils.translation import gettext as _
4
4
from .models import *
5
5
import administration
6
6
from django.contrib.auth.decorators import login_required
7
7
8
8
def current_academic_year():
9
9
    """ Returns the current academic year. The year is determined as follows:
10
10
    - If today is before September 15 of the current year, the returned value
11
11
      is the current year - 1.
12
12
    - If today is after September 15 of the current year, but before January 1
13
13
      of the next year, it returns the current year as is.
14
14
    """
15
15
    today = datetime.datetime.now()
16
16
    switch = datetime.datetime.date(datetime.datetime.year, 9, 15)
17
17
    if today < switch:
18
18
        return today.year - 1
19
19
    else:
20
20
        return today.year
21
21
22
22
@login_required
23
23
def index(request):
24
24
    """ Starting page regarding the courses. This serves two specific groups:
25
25
    - Students: Displays all courses that this student has in his/her curriculum
26
26
                for this academic year. Requires the curriculum to be accepted.
27
27
    - Staff: Displays all courses in which the staff member is part of the
28
28
             educating team, or is otherwise related to the course.
29
29
    Users who are not logged in will be sent to the login page.
30
30
    """
31
31
    template = "courses/index.djhtml"
32
32
    courses = set()
33
33
    if request.user.is_student:
34
34
        curricula = administration.models.Curriculum.objects.filter(student=request.user)
35
35
        current_curriculum = curricula.filter(year__year=current_academic_year())
36
36
        courses = current_curriculum.courses
37
37
    elif request.user.is_staff:
38
38
        courses += adminstration.models.Course.filter(course_team__contains=request.user)
39
39
    else:
40
40
        raise django.exceptions.FieldError("User "+request.user.number+" is neither staff nor student")
41
41
42
42
    context = {
43
43
        'courses': courses,
44
44
        }
45
45
46
46
    return render(request, template, context)
47
47
48
48
@login_required
49
49
def course(request, course_slug):
50
50
    template = "courses/course.djhtml"
51
51
    course = Course.objects.get(slug_name=course_slug)
52
52
53
53
    # Check if user can see this page
54
54
    if request.user.is_student:
55
55
        curricula = administration.models.Curriculum.objects.filter(student=request.user)
56
56
        current_curriculum = curricula.filter(year__year=current_academic_year())
57
57
        if course not in current_curriculum.courses:
58
58
            """ I'm currently just redirecting to the index page, but maybe it's
59
59
            just as good to make an announcement that this course cannot be
60
60
            used by this user. """
61
61
            return index(request)
62
62
63
63
64
64
65
65
    context = {
66
66
        'course': course,
67
67
        'announcements': Announcement.objects.filter(course=course),
68
68
        'assignments': Assignment.objects.filter(course=course),
69
69
        'course-items': CourseItem.objects.filter(course=course),
70
70
        'study-groups': StudyGroup.objects.filter(course=course),
71
71
        'uploads': Upload.objects.filter(course=course).filter(student=request.user)
72
72
        }
73
73
74
74
    return render(request, template, context)
75
75
76
76
# TODO: Find a way to see if it's possible to require some permissions and to
77
77
# put them in a decorator
78
78
#@permission_required
79
79
@login_required
80
80
def new_item(request, course_slug):
81
81
    template = "courses/new_item.djhtml"
82
82
    course = Course.objects.get(slug_name=course_slug)
83
83
84
84
    if request.user.is_student or request.user not in course.course_team:
85
85
        # Students can't add new items. Redirect to index
86
86
        # Also redirect people who are not part of the course team
87
87
        redirect('courses-index')
88
88
    # Now able to assume user is allowed to add items to this course
89
89
90
90
    context = {
91
91
        'course': course,
92
92
        'announcements': Announcement.objects.filter(course=course),
93
93
        'assignments': Assignment.objects.filter(course=course),
94
94
        'course-items': CourseItem.objects.filter(course=course),
95
95
        'study-groups': StudyGroup.objects.filter(course=course),
96
96
        'uploads': Upload.objects.filter(course=course)
97
97
        }
98
98
99
99
    return render(request, template, context)
100
100
101
101
@login_required
102
102
def remove(request, type, id):
103
103
    pass
104
104
+
105
@login_required
+
106
def upload(request):
+
107
    pass
+
108
+
109
@login_required
+
110
def groups(request):
+
111
    pass
+
112

joeni/settings.py

8 additions and 0 deletions.

View changes Hide changes
1
1
Django settings for Joeni project.
2
2
3
3
Generated by 'django-admin startproject' using Django 2.0b1.
4
4
5
5
For more information on this file, see
6
6
https://docs.djangoproject.com/en/dev/topics/settings/
7
7
8
8
For the full list of settings and their values, see
9
9
https://docs.djangoproject.com/en/dev/ref/settings/
10
10
"""
11
11
12
12
import os
13
13
14
14
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
15
15
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
16
16
17
17
18
18
# Quick-start development settings - unsuitable for production
19
19
# See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/
20
20
21
21
# SECURITY WARNING: keep the secret key used in production secret!
22
22
SECRET_KEY = '!2634qc=b*lp0=helzcmvb3+1_wcl!6z@mhzi%p(vg7odq&gfz'
23
23
24
24
# SECURITY WARNING: don't run with debug turned on in production!
25
25
DEBUG = True
26
26
27
27
# Crispy settings
28
28
CRISPY_FAIL_SILENTLY = not DEBUG  # For debugging info for Crispy
29
29
CRISPY_TEMPLATE_PACK = "hasselt-university"
30
30
31
31
32
32
ALLOWED_HOSTS = []
33
33
34
34
35
35
# Application definition
36
36
37
37
INSTALLED_APPS = [
38
38
    'django.contrib.admin',
39
39
    'django.contrib.auth',
40
40
    'django.contrib.contenttypes',
41
41
    'django.contrib.sessions',
+
42
    'django.contrib.sessions',
42
43
    'django.contrib.messages',
43
44
    'django.contrib.staticfiles',
44
45
    'administration',
45
46
    'agora',
46
47
    'courses',
47
48
    'joeni',
48
49
]
49
50
50
51
MIDDLEWARE = [
51
52
    'django.middleware.security.SecurityMiddleware',
52
53
    'django.middleware.locale.LocaleMiddleware',
53
54
    'django.middleware.common.CommonMiddleware',
54
55
    'django.middleware.csrf.CsrfViewMiddleware',
55
56
    'django.contrib.sessions.middleware.SessionMiddleware',
56
57
    'django.contrib.auth.middleware.AuthenticationMiddleware',
57
58
    'django.contrib.sessions.middleware.SessionMiddleware',
58
59
    'django.contrib.messages.middleware.MessageMiddleware',
59
60
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
60
61
    'django.middleware.security.SecurityMiddleware',
61
62
    # Caching middleware
62
63
    'django.middleware.cache.UpdateCacheMiddleware',
63
64
    'django.middleware.common.CommonMiddleware',
64
65
    'django.middleware.cache.FetchFromCacheMiddleware',
65
66
]
66
67
67
68
# Caching settings
68
69
CACHE_MIDDLEWARE_ALIAS = 'default'
69
70
CACHE_MIDDLEWARE_SECONDS = 300
70
71
CACHE_MIDDLEWARE_KEY_PREFIX = ''
71
72
72
73
ROOT_URLCONF = 'joeni.urls'
73
74
74
75
TEMPLATES = [
75
76
    {
76
77
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
77
78
        'DIRS': [],
78
79
        'APP_DIRS': True,
79
80
        'OPTIONS': {
80
81
            'context_processors': [
81
82
                'django.template.context_processors.debug',
82
83
                'django.template.context_processors.request',
83
84
                'django.contrib.auth.context_processors.auth',
84
85
                'django.contrib.messages.context_processors.messages',
85
86
            ],
86
87
        },
87
88
    },
88
89
]
89
90
90
91
#WSGI_APPLICATION = 'joeni.wsgi.application'
91
92
92
93
93
94
# Database
94
95
# https://docs.djangoproject.com/en/dev/ref/settings/#databases
95
96
96
97
DATABASES = {
97
98
    'default': {
98
99
        'ENGINE': 'django.db.backends.sqlite3',
99
100
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
100
101
    }
101
102
}
102
103
103
104
# Custom User model
+
105
LOGIN_URL = 'administration/login'
+
106
+
107
# Custom User model
104
108
AUTH_USER_MODEL = 'administration.User'
105
109
106
110
# Password validation
107
111
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
108
112
109
113
AUTH_PASSWORD_VALIDATORS = [
110
114
    {
111
115
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
112
116
    },
113
117
    {
114
118
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
115
119
    },
116
120
    {
117
121
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
118
122
    },
119
123
    {
120
124
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
121
125
    },
122
126
]
123
127
124
128
CACHES = {
125
129
    'default': {
126
130
        'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
127
131
    }
128
132
}
129
133
130
134
# Internationalization
131
135
# https://docs.djangoproject.com/en/dev/topics/i18n/
132
136
133
137
LANGUAGE_CODE = 'en-us'
134
138
135
139
TIME_ZONE = 'UTC'
136
140
137
141
USE_I18N = True
138
142
139
143
USE_L10N = True
140
144
141
145
USE_TZ = True
142
146
143
147
144
148
# Static files (CSS, JavaScript, Images)
145
149
# https://docs.djangoproject.com/en/dev/howto/static-files/
146
150
147
151
STATIC_URL = '/static/'
+
152
    BASE_DIR + "/static",
+
153
    ]
+
154
#STATIC_ROOT = BASE_DIR + '/static/'
+
155
STATIC_URL = '/static/'
148
156
MEDIA_URL = '/media/'
149
157

joeni/templates/joeni/base.djhtml

3 additions and 1 deletion.

View changes Hide changes
1
1
{% load i18n %}
2
2
{% get_current_language as LANGUAGE_CODE %}
3
3
{% get_language_info for LANGUAGE_CODE as lang %}
4
4
{% load static %}
5
5
{% get_media_prefix as media %}
6
6
7
7
8
8
9
9
    
10
10
        </pre></td>
        </tr>
        
        
        <tr class="nochange">
            <td id="joeni/templates/joeni/base.djhtml-A11" class="line-number">
                <a href="#joeni/templates/joeni/base.djhtml-A11" class="accent-on-hover-only"><pre>11</pre>
            <td id="joeni/templates/joeni/base.djhtml-B11" class="line-number">
                <a href="#joeni/templates/joeni/base.djhtml-B11" class="accent-on-hover-only"><pre>11</pre>
            </a></td>
        
            <td style="padding-left: 1em;"><pre>            {% block title %}</pre></td>
        </tr>
        
        
        <tr class="nochange">
            <td id="joeni/templates/joeni/base.djhtml-A12" class="line-number">
                <a href="#joeni/templates/joeni/base.djhtml-A12" class="accent-on-hover-only"><pre>12</pre>
            <td id="joeni/templates/joeni/base.djhtml-B12" class="line-number">
                <a href="#joeni/templates/joeni/base.djhtml-B12" class="accent-on-hover-only"><pre>12</pre>
            </a></td>
        
            <td style="padding-left: 1em;"><pre>                ◀ Joeni /▶ | ▶▶ UHasselt</pre></td>
        </tr>
        
        
        <tr class="nochange">
            <td id="joeni/templates/joeni/base.djhtml-A13" class="line-number">
                <a href="#joeni/templates/joeni/base.djhtml-A13" class="accent-on-hover-only"><pre>13</pre>
            <td id="joeni/templates/joeni/base.djhtml-B13" class="line-number">
                <a href="#joeni/templates/joeni/base.djhtml-B13" class="accent-on-hover-only"><pre>13</pre>
            </a></td>
        
            <td style="padding-left: 1em;"><pre>            {% endblock title %}</pre></td>
        </tr>
        
        
        <tr class="nochange">
            <td id="joeni/templates/joeni/base.djhtml-A14" class="line-number">
                <a href="#joeni/templates/joeni/base.djhtml-A14" class="accent-on-hover-only"><pre>14</pre>
            <td id="joeni/templates/joeni/base.djhtml-B14" class="line-number">
                <a href="#joeni/templates/joeni/base.djhtml-B14" class="accent-on-hover-only"><pre>14</pre>
            </a></td>
        
            <td style="padding-left: 1em;"><pre>        
15
15
16
16
        {% block stylesheets %}
17
17
            
+
18
            
+
19
            
18
20
            
23
25
        {% endblock stylesheets %}
24
26
25
27
        {% block metaflags %}
26
28
            {# This is standard for all web pages and doesn't require changing. #}
27
29
            {# UTF-8, always #}
28
30
            
29
31
            {##}
30
32
            {# Indicates this page is suited for mobile devices #}
31
33
            
32
34
            
33
35
                name="description"
34
36
                content="{% block description %}
35
37
                    {% trans "The digital platform of Hasselt University" %}
36
38
                {% endblock description %}" />
37
39
        {% endblock metaflags %}
38
40
    
39
41
40
42
    
41
43
        
42
44
                {% block header %}
43
45
                    {% include "joeni/navbar.djhtml" %}
44
-
                {% endblock header %}
+
46
                {% endblock header %}
45
47
        
46
48
47
49
        
48
50
            {% block main %}
49
51
            {% endblock main %}
50
52
        
51
53
52
54
        
53
55
            {% block footer %}
54
56
                {% include "joeni/footer.djhtml" %}
55
57
            {% endblock footer %}
56
58
        
57
59
58
60