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 |
|
+ |
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 |
|
+ |
22 |
{% for message in education_dept_messages %} |
+ |
23 |
|
+ |
24 |
|
+ |
25 |
{{ message.date|naturaltime }} |
+ |
26 |
|
+ |
27 |
|
+ |
28 |
{% empty %} |
+ |
29 |
|
+ |
30 |
{% endfor %} |
+ |
31 |
|
+ |
32 |
|
+ |
33 |
|
+ |
34 |
|
+ |
35 |
|
+ |
36 |
|
+ |
37 |
|
+ |
38 |
|
+ |
39 |
|
+ |
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 |
|
+ |
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 |
|
+ |
17 |
/* label color */ |
+ |
18 |
.input-field label { |
+ |
19 |
color: #9e9e9e; |
+ |
20 |
} |
+ |
21 |
/* label focus color */ |
+ |
22 |
.input-field input[type=password]:focus + label { |
+ |
23 |
color: #ffc107; |
+ |
24 |
} |
+ |
25 |
/* label underline focus color */ |
+ |
26 |
.input-field input[type=password]:focus { |
+ |
27 |
border-bottom: 1px solid #ffc107; |
+ |
28 |
box-shadow: 0 1px 0 0 #ffc107; |
+ |
29 |
} |
+ |
30 |
.input-field input[type=text]:focus + label { |
+ |
31 |
color: #ffc107; |
+ |
32 |
} |
+ |
33 |
/* label underline focus color */ |
+ |
34 |
.input-field input[type=text]:focus { |
+ |
35 |
border-bottom: 1px solid #ffc107; |
+ |
36 |
box-shadow: 0 1px 0 0 #ffc107; |
+ |
37 |
} |
+ |
38 |
/* icon prefix focus color */ |
+ |
39 |
.input-field .prefix.active { |
+ |
40 |
color: #ffc107; |
+ |
41 |
} |
+ |
42 |
|
+ |
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 |
|
+ |
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.
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 |
|
+ |
11 |
|
+ |
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 |
|
11 |
11 |
{% block title %} |
12 |
12 |
◀ Joeni /▶ | ▶▶ UHasselt |
13 |
13 |
{% endblock title %} |
14 |
14 |
|
15 |
15 |
|
16 |
16 |
{% block stylesheets %} |
17 |
17 |
|
+ |
18 |
|
+ |
19 |
|
18 |
20 |
|
19 |
21 |
header, footer { |
20 |
22 |
background-color: #{{ user.account.settings.color|default:"UHASSELT" }}; |
21 |
23 |
} |
22 |
24 |
|
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 |