Minor model changes and new templates
I've made some slight changes to the models, more specifically; a name change and a slight change in how the foreign keys work. Additionally, the first view has been made, and a couple of new templates have been added to the tracker. These are not complete yet, but they're already pretty functional for what they're supposed to do. The stylesheets are still in their infancy, because they're not very important. Currently, it's more important to make the templates convey a meaning on their own.
- Author
- Maarten 'Vngngdn' Vangeneugden
- Date
- Nov. 22, 2017, 9:45 p.m.
- Hash
- 4c969f3a0dea77b4409e9cf91891ddf1c75181db
- Parent
- c35535e1d10591e4d7f7ddd9102e58355df3ae81
- Modified files
- administration/models.py
- courses/models.py
- courses/templates/courses/index.djhtml
- courses/views.py
- joeni/templates/joeni/base.djhtml
- joeni/templates/joeni/footer.djhtml
administration/models.py ¶
16 additions and 4 deletions.
View changes Hide changes
1 |
1 |
from django.core.exceptions import ValidationError |
2 |
2 |
from django.core.validators import MaxValueValidator |
3 |
3 |
from django.utils.translation import ugettext_lazy as _ |
4 |
4 |
from django.contrib.auth.models import AbstractUser |
5 |
5 |
import datetime |
6 |
6 |
import os |
7 |
7 |
import uuid |
8 |
8 |
|
9 |
9 |
def validate_IBAN(value): |
10 |
10 |
""" Validates if the given value qualifies as a valid IBAN number. |
11 |
11 |
This validator checks if the structure is valid, and calculates the control |
12 |
12 |
number if the structure is correct. If the control number fails, or the |
13 |
13 |
structure is invalid, a ValidationError will be raised. In that case, |
14 |
14 |
the Error will specify whether the structure is incorrect, or the control |
15 |
15 |
number is not valid. |
16 |
16 |
""" |
17 |
17 |
# FIXME: This function is not complete. When there's time, implement |
18 |
18 |
# as specified at https://nl.wikipedia.org/wiki/International_Bank_Account_Number#Structuur |
19 |
19 |
if False: |
20 |
20 |
raise ValidationError( |
21 |
21 |
_('%(value)s is not a valid IBAN number.'), |
22 |
22 |
params={'value': value},) |
23 |
23 |
def validate_BIC(value): |
24 |
24 |
""" Same functionality as validate_IBAN, but for BIC-codes. """ |
25 |
25 |
# FIXME: This function is not complete. When there's time, implement |
26 |
26 |
# as specified at https://nl.wikipedia.org/wiki/Business_Identifier_Code |
27 |
27 |
pass |
28 |
28 |
|
29 |
29 |
class User(AbstractUser): |
30 |
30 |
""" Replacement for the standard Django User model. """ |
31 |
31 |
number = models.AutoField( |
32 |
32 |
primary_key=True, |
33 |
33 |
help_text=_("The number assigned to this user."), |
34 |
34 |
) |
35 |
35 |
created = models.DateField(auto_now_add=True) |
36 |
36 |
first_name = models.CharField(max_length=64, blank=False) |
37 |
37 |
last_name = models.CharField(max_length=64, blank=False) |
38 |
38 |
title = models.CharField( |
39 |
39 |
max_length=64, |
40 |
40 |
blank=True, |
41 |
41 |
help_text=_("The academic title of this user, if applicable."), |
42 |
42 |
) |
43 |
43 |
DOB = models.DateField( |
44 |
44 |
blank=False, |
45 |
45 |
#editable=False, |
46 |
46 |
help_text=_("The date of birth of this user."), |
47 |
47 |
) |
48 |
48 |
POB = models.CharField( |
49 |
49 |
max_length=64, |
50 |
50 |
blank=False, |
51 |
51 |
#editable=False, |
52 |
52 |
help_text=_("The place of birth of this user."), |
53 |
53 |
) |
54 |
54 |
nationality = models.CharField( |
55 |
55 |
max_length=64, |
56 |
56 |
blank=False, |
57 |
57 |
help_text=_("The current nationality of this user."), |
58 |
58 |
) |
59 |
59 |
# XXX: What if this starts with zeros? |
60 |
60 |
national_registry_number = models.BigIntegerField( |
61 |
61 |
unique=True, |
62 |
62 |
#editable=False, |
63 |
63 |
help_text=_("The assigned national registry number of this user."), |
64 |
64 |
) |
65 |
65 |
civil_status = models.CharField( |
66 |
66 |
max_length=32, |
67 |
67 |
choices = ( |
68 |
68 |
("Single", _("Single")), |
69 |
69 |
("Married", _("Married")), |
70 |
70 |
("Divorced", _("Divorced")), |
71 |
71 |
("Widowed", _("Widowed")), |
72 |
72 |
("Partnership", _("Partnership")), |
73 |
73 |
), |
74 |
74 |
blank=False, |
75 |
75 |
# There may be more; consult http://www.aantrekkingskracht.com/trefwoord/burgerlijke-staat |
76 |
76 |
# for more information. |
77 |
77 |
help_text=_("The civil/marital status of the user."), |
78 |
78 |
) |
79 |
79 |
|
80 |
80 |
is_staff = models.BooleanField( |
81 |
81 |
default=False, |
82 |
82 |
help_text=_("Determines if this user is part of the university's staff."), |
83 |
83 |
) |
84 |
84 |
is_student = models.BooleanField( |
85 |
85 |
default=True, |
86 |
86 |
help_text=_("Indicates if this user is a student at the university."), |
87 |
87 |
) |
88 |
88 |
|
89 |
89 |
# Home address |
90 |
90 |
home_street = models.CharField(max_length=64, blank=False) |
91 |
91 |
home_number = models.PositiveSmallIntegerField(blank=False) |
92 |
92 |
home_bus = models.PositiveSmallIntegerField(null=True) |
93 |
93 |
home_postal_code = models.PositiveSmallIntegerField(blank=False) |
94 |
94 |
home_country = models.CharField(max_length=64, blank=False) |
95 |
95 |
home_telephone = models.CharField( |
96 |
96 |
max_length=64, |
97 |
97 |
help_text=_("The telephone number for the house address. Prefix 0 can be presented with the national call code in the system."), |
98 |
98 |
) |
99 |
99 |
# Study address |
100 |
100 |
study_street = models.CharField(max_length=64, blank=False) |
101 |
101 |
study_number = models.PositiveSmallIntegerField(blank=False) |
102 |
102 |
study_bus = models.PositiveSmallIntegerField(null=True) |
103 |
103 |
study_postal_code = models.PositiveSmallIntegerField(blank=False) |
104 |
104 |
study_country = models.CharField(max_length=64, blank=False) |
105 |
105 |
study_telephone = models.CharField( |
106 |
106 |
max_length=64, |
107 |
107 |
help_text=_("The telephone number for the study address. Prefix 0 can be presented with the national call code in the system."), |
108 |
108 |
) |
109 |
109 |
study_cellphone = models.CharField( |
110 |
110 |
max_length=64, |
111 |
111 |
help_text=_("The cellphone number of the person. Prefix 0 can be presented with then national call code in the system."), |
112 |
112 |
) |
113 |
113 |
# Titularis address |
114 |
114 |
# XXX: These fields are only required if this differs from the user itself. |
115 |
115 |
titularis_street = models.CharField(max_length=64, null=True) |
116 |
116 |
titularis_number = models.PositiveSmallIntegerField(null=True) |
117 |
117 |
titularis_bus = models.PositiveSmallIntegerField(null=True) |
118 |
118 |
titularis_postal_code = models.PositiveSmallIntegerField(null=True) |
119 |
119 |
titularis_country = models.CharField(max_length=64, null=True) |
120 |
120 |
titularis_telephone = models.CharField( |
121 |
121 |
max_length=64, |
122 |
122 |
help_text=_("The telephone number of the titularis. Prefix 0 can be presented with the national call code in the system."), |
123 |
123 |
null=True, |
124 |
124 |
) |
125 |
125 |
|
126 |
126 |
# Financial details |
127 |
127 |
bank_account_number = models.CharField( |
128 |
128 |
max_length=34, # Max length of all IBAN account numbers |
129 |
129 |
validators=[validate_IBAN], |
130 |
130 |
help_text=_("The IBAN of this user. No spaces!"), |
131 |
131 |
) |
132 |
132 |
BIC = models.CharField( |
133 |
133 |
max_length=11, |
134 |
134 |
validators=[validate_BIC], |
135 |
135 |
help_text=_("The BIC of this user's bank."), |
136 |
136 |
) |
137 |
137 |
|
138 |
138 |
""" NOTE: What about all the other features that should be in the administration? |
139 |
139 |
While there are a lot of things to cover, as of now, I have no way to know which |
140 |
140 |
ones are still valid, which are deprecated, and so on... |
141 |
141 |
Additionally, every feature may have a different set of requirements, data, |
142 |
142 |
and it's very likely making an abstract class won't do any good. Thus I have |
143 |
143 |
decided to postpone making additional tables and forms for these features until |
144 |
144 |
I have clearance about certain aspects. """ |
145 |
145 |
|
146 |
146 |
class Curriculum(models.Model): |
147 |
147 |
""" The curriculum of a particular student. |
148 |
148 |
Every academic year, a student has to hand in a curriculum (s)he wishes to |
149 |
149 |
follow. This is then reviewed by a committee. A curriculum exists of all the |
150 |
150 |
courses one wants to partake in in a certain year. """ |
151 |
151 |
student = models.ForeignKey( |
152 |
152 |
"User", |
153 |
153 |
on_delete=models.CASCADE, |
154 |
154 |
limit_choices_to={'is_student': True}, |
155 |
155 |
null=False, |
156 |
156 |
#editable=False, |
157 |
157 |
unique_for_year="year", # Only 1 curriculum per year |
158 |
158 |
) |
159 |
159 |
year = models.DateField( |
160 |
160 |
auto_now_add=True, |
161 |
161 |
db_index=True, |
162 |
162 |
help_text=_("The academic year for which this curriculum is."), |
163 |
- | ) |
+ |
163 |
"If this field is equal to 2008, then that means " |
+ |
164 |
"this curriculum is for the academic year " |
+ |
165 |
"2008-2009."), |
+ |
166 |
) |
164 |
167 |
last_modified = models.DateTimeField( |
165 |
168 |
auto_now=True, |
166 |
169 |
help_text=_("The last timestamp that this was updated."), |
167 |
170 |
) |
168 |
171 |
courses = models.ManyToManyField( |
169 |
- | "courses.Course", |
170 |
- | null=False, |
+ |
172 |
"courses.CourseProgramme", |
+ |
173 |
null=False, |
171 |
174 |
help_text=_("All the courses included in this curriculum."), |
172 |
- | ) |
+ |
175 |
) |
173 |
176 |
approved = models.NullBooleanField( |
174 |
177 |
default=None, |
175 |
178 |
help_text=_("Indicates if this curriculum has been approved. If true, " |
176 |
179 |
"that means the responsible committee has reviewed and " |
177 |
180 |
"approved the student for this curriculum. False otherwise. " |
178 |
181 |
"If review is still pending, the value is NULL. Modifying " |
179 |
182 |
"the curriculum implies this setting is set to NULL again."), |
180 |
183 |
) |
181 |
184 |
note = models.TextField( |
182 |
185 |
blank=True, |
183 |
186 |
help_text=_("Additional notes regarding this curriculum. This has " |
184 |
187 |
"multiple uses. For the student, it is used to clarify " |
185 |
188 |
"any questions, or to motivate why (s)he wants to take a " |
186 |
189 |
"course for which the requirements were not met. " |
187 |
190 |
"The reviewing committee can use this field to argument " |
188 |
191 |
"their decision, especially for when the curriculum is " |
189 |
192 |
"denied."), |
190 |
193 |
) |
191 |
194 |
|
192 |
195 |
def curriculum_type(self): |
+ |
196 |
""" Returns a set of all the courses that are in this curriculum. |
+ |
197 |
This is not the same as CourseProgrammes, as these can differ depending |
+ |
198 |
on which study one follows. """ |
+ |
199 |
course_set = set() |
+ |
200 |
for course_programme in self.course_programmes: |
+ |
201 |
course_set.add(course_programme.course) |
+ |
202 |
return course_set |
+ |
203 |
|
+ |
204 |
def curriculum_type(self): |
193 |
205 |
""" Returns the type of this curriculum. At the moment, this is |
194 |
206 |
either a standard programme, or an individualized programme. """ |
195 |
207 |
# Currently: A standard programme means: All courses are from the |
196 |
208 |
# same study, ánd from the same year. Additionally, all courses |
197 |
209 |
# from that year must've been taken. |
198 |
210 |
# FIXME: Need a way to determine what is the standard programme. |
199 |
211 |
# If not possible, make this a charfield with options or something |
200 |
212 |
pass |
201 |
213 |
|
202 |
214 |
def __str__(self): |
203 |
215 |
year = self.year.year |
204 |
216 |
if self.year.month < 7: |
205 |
217 |
return str(self.student) +" | "+ str(year-1) +"-"+ str(year) |
206 |
218 |
else: |
207 |
219 |
return str(self.student) +" | "+ str(year) +"-"+ str(year+1) |
208 |
220 |
|
209 |
221 |
|
210 |
222 |
class CourseResult(models.Model): |
211 |
223 |
""" A student has to obtain a certain course result. These are stored here, |
212 |
224 |
together with all the appropriate information. """ |
213 |
225 |
# TODO: Validate that a course programme for a student can only be made once per year for each course, if possible. |
214 |
226 |
CRED = _("Credit acquired") |
215 |
227 |
FAIL = _("Credit not acquired") |
216 |
228 |
TLRD = _("Tolerated") |
217 |
229 |
ITLD = _("Tolerance used") |
218 |
230 |
# Possible to add more in the future |
219 |
231 |
|
220 |
232 |
student = models.ForeignKey( |
221 |
233 |
"User", |
222 |
234 |
on_delete=models.CASCADE, |
223 |
235 |
limit_choices_to={'is_student': True}, |
224 |
236 |
null=False, |
225 |
237 |
) |
226 |
238 |
course_programme = models.ForeignKey( |
227 |
239 |
"courses.ProgrammeInformation", |
228 |
240 |
on_delete=models.PROTECT, |
229 |
241 |
null=False, |
230 |
242 |
) |
231 |
243 |
released = models.DateField( |
232 |
244 |
auto_now=True, |
233 |
245 |
help_text=_("The date that this result was last updated."), |
234 |
246 |
) |
235 |
247 |
first_score = models.PositiveSmallIntegerField( |
236 |
248 |
null=True, # It's possible a score does not exist. |
237 |
249 |
validators=[MaxValueValidator( |
238 |
250 |
20, |
239 |
251 |
_("The score mustn't be higher than 20."), |
240 |
252 |
)], |
241 |
253 |
) |
242 |
254 |
second_score = models.PositiveSmallIntegerField( |
243 |
255 |
null=True, |
244 |
256 |
validators=[MaxValueValidator( |
245 |
257 |
20, |
246 |
258 |
_("The score mustn't be higher than 20."), |
247 |
259 |
)], |
248 |
260 |
) |
249 |
261 |
result = models.CharField( |
250 |
262 |
max_length=10, |
251 |
263 |
choices = ( |
252 |
264 |
("CRED", CRED), |
253 |
265 |
("FAIL", FAIL), |
254 |
266 |
("TLRD", TLRD), |
255 |
267 |
("ITLD", ITLD), |
256 |
268 |
), |
257 |
269 |
blank=False, |
258 |
270 |
help_text=_("The final result this record constitutes."), |
259 |
271 |
) |
260 |
272 |
|
261 |
273 |
def __str__(self): |
262 |
274 |
stdnum = str(self.student.number) |
263 |
275 |
result = self.result |
264 |
276 |
if result == "CRED": |
265 |
277 |
if self.first_score < 10: |
266 |
278 |
result = "C" + self.first_score + "1" |
267 |
279 |
else: |
268 |
280 |
result = "C" + self.second_score + "2" |
269 |
281 |
course = str(self.course_programme.course) |
270 |
282 |
return stdnum +" ("+ result +") | "+ course |
271 |
283 |
|
272 |
284 |
class PreRegistration(models.Model): |
273 |
285 |
""" At the beginning of the new academic year, students can register |
274 |
286 |
themselves at the university. Online, they can do a preregistration already. |
275 |
287 |
These records are stored here and can later be retrieved for the actual |
276 |
288 |
registration process. |
277 |
289 |
Note: The current system in use at Hasselt University provides a password system. |
278 |
290 |
That will be eliminated here. Just make sure that the entered details are correct. |
279 |
291 |
Should there be an error, and the same email address is used to update something, |
280 |
292 |
a mail will be sent to that address to verify this was a genuine update.""" |
281 |
293 |
created = models.DateField(auto_now_add=True) |
282 |
294 |
first_name = models.CharField(max_length=64, blank=False, help_text=_("Your first name.")) |
283 |
295 |
last_name = models.CharField(max_length=64, blank=False, help_text=_("Your last name.")) |
284 |
296 |
additional_names = models.CharField(max_length=64, blank=True, help_text=_("Any additional names.")) |
285 |
297 |
title = models.CharField( |
286 |
298 |
max_length=64, |
287 |
299 |
blank=True, |
288 |
300 |
help_text=_("Any additional titles, prefixes, ..."), |
289 |
301 |
) |
290 |
302 |
DOB = models.DateField( |
291 |
303 |
blank=False, |
292 |
304 |
#editable=False, |
293 |
305 |
help_text=_("Your date of birth."), |
294 |
306 |
) |
295 |
307 |
POB = models.CharField( |
296 |
308 |
max_length=64, |
297 |
309 |
blank=False, |
298 |
310 |
#editable=False, |
299 |
311 |
help_text=_("The place you were born."), |
300 |
312 |
) |
301 |
313 |
nationality = models.CharField( |
302 |
314 |
max_length=64, |
303 |
315 |
blank=False, |
304 |
316 |
help_text=_("Your current nationality."), |
305 |
317 |
) |
306 |
318 |
national_registry_number = models.BigIntegerField( |
307 |
319 |
null=True, |
308 |
320 |
help_text=_("If you have one, your national registry number."), |
309 |
321 |
) |
310 |
322 |
civil_status = models.CharField( |
311 |
323 |
max_length=32, |
312 |
324 |
choices = ( |
313 |
325 |
("Single", _("Single")), |
314 |
326 |
("Married", _("Married")), |
315 |
327 |
("Divorced", _("Divorced")), |
316 |
328 |
("Widowed", _("Widowed")), |
317 |
329 |
("Partnership", _("Partnership")), |
318 |
330 |
), |
319 |
331 |
blank=False, |
320 |
332 |
# There may be more; consult http://www.aantrekkingskracht.com/trefwoord/burgerlijke-staat |
321 |
333 |
# for more information. |
322 |
334 |
help_text=_("Your civil/marital status."), |
323 |
335 |
) |
324 |
336 |
email = models.EmailField( |
325 |
337 |
blank=False, |
326 |
338 |
unique=True, |
327 |
339 |
help_text=_("The e-mail address we will use to communicate until your actual registration."), |
328 |
340 |
) |
329 |
341 |
study = models.ForeignKey( |
330 |
342 |
"courses.Study", |
331 |
343 |
on_delete=models.PROTECT, |
332 |
344 |
null=False, |
333 |
345 |
help_text=_("The study you wish to follow. Be sure to provide all legal" |
334 |
346 |
"documents that are required for this study with this " |
335 |
347 |
"application, or bring them with you to the final registration."), |
336 |
348 |
) |
337 |
349 |
study_type = models.CharField( |
338 |
350 |
max_length=32, |
339 |
351 |
choices = ( |
340 |
352 |
("Diplom contract", _("Diplom contract")), |
341 |
353 |
("Exam contract", _("Exam contract")), |
342 |
354 |
("Credit contract", _("Credit contract")), |
343 |
355 |
), |
344 |
356 |
blank=False, |
345 |
357 |
help_text=_("The type of study contract you wish to follow."), |
346 |
358 |
) |
347 |
359 |
document = models.FileField( |
348 |
360 |
upload_to="pre-enrollment/%Y", |
349 |
361 |
help_text=_("Any legal documents regarding your enrollment."), |
350 |
362 |
) |
351 |
363 |
# XXX: If the database in production is PostgreSQL, comment document, and |
352 |
364 |
# uncomment the next column. |
353 |
365 |
"""documents = models.ArrayField( |
354 |
366 |
models.FileField(upload_to="pre-enrollment/%Y"), |
355 |
367 |
help_text=_("Any legal documents regarding your enrollment."), |
356 |
368 |
)""" |
357 |
369 |
|
358 |
370 |
def __str__(self): |
359 |
371 |
name = self.last_name +" "+ self.first_name |
360 |
372 |
dob = self.DOB.strftime("%d/%m/%Y") |
361 |
373 |
return name +" | "+ dob |
362 |
374 |
|
363 |
375 |
|
364 |
376 |
# Planning and organization related tables |
365 |
377 |
class Room(models.Model): |
366 |
378 |
""" Represents a room in the university. |
367 |
379 |
Rooms can have a number of properties, which are stored in the database. |
368 |
380 |
""" |
369 |
381 |
# Types of rooms |
370 |
382 |
LABORATORY = _("Laboratory") # Chemistry/Physics equipped rooms |
371 |
383 |
CLASS_ROOM = _("Class room") # Simple class rooms |
372 |
384 |
AUDITORIUM = _("Auditorium") # Large rooms with ample seating and equipment for lectures |
373 |
385 |
PC_ROOM = _("PC room" ) # Rooms equipped for executing PC related tasks |
374 |
386 |
PUBLIC_ROOM= _("Public room") # Restaurants, restrooms, ... general public spaces |
375 |
387 |
OFFICE = _("Office" ) # Private offices for staff |
376 |
388 |
PRIVATE_ROOM = _("Private room") # Rooms accessible for a limited public; cleaning cupboards, kitchens, ... |
377 |
389 |
WORKSHOP = _("Workshop" ) # Rooms with hardware equipment to build and work on materials |
378 |
390 |
OTHER = _("Other" ) # Rooms that do not fit in any other category |
379 |
391 |
|
380 |
392 |
|
381 |
393 |
name = models.CharField( |
382 |
394 |
max_length=20, |
383 |
395 |
primary_key=True, |
384 |
396 |
blank=False, |
385 |
397 |
help_text=_("The name of this room. If more appropriate, this can be the colloquial name."), |
386 |
398 |
) |
387 |
399 |
seats = models.PositiveSmallIntegerField( |
388 |
400 |
help_text=_("The amount of available seats in this room. This can be handy for exams for example."), |
389 |
401 |
) |
390 |
402 |
wheelchair_accessible = models.BooleanField(default=True) |
391 |
403 |
exams_equipped = models.BooleanField( |
392 |
404 |
default=True, |
393 |
405 |
help_text=_("Indicates if exams can reasonably be held in this room."), |
394 |
406 |
) |
395 |
407 |
computers_available = models.PositiveSmallIntegerField( |
396 |
408 |
default=False, |
397 |
409 |
help_text=_("Indicates how many computers are available in this room."), |
398 |
410 |
) |
399 |
411 |
projector_available = models.BooleanField( |
400 |
412 |
default=False, |
401 |
413 |
help_text=_("Indicates if a projector is available at this room."), |
402 |
414 |
) |
403 |
415 |
blackboards_available = models.PositiveSmallIntegerField( |
404 |
416 |
help_text=_("The amount of blackboards available in this room."), |
405 |
417 |
) |
406 |
418 |
whiteboards_available = models.PositiveSmallIntegerField( |
407 |
419 |
help_text=_("The amount of whiteboards available in this room."), |
408 |
420 |
) |
409 |
421 |
category = models.CharField( |
410 |
422 |
max_length=16, |
411 |
423 |
blank=False, |
412 |
424 |
choices = ( |
413 |
425 |
("LABORATORY", LABORATORY), |
414 |
426 |
("CLASS_ROOM", CLASS_ROOM), |
415 |
427 |
("AUDITORIUM", AUDITORIUM), |
416 |
428 |
("PC_ROOM", PC_ROOM), |
417 |
429 |
("PUBLIC_ROOM", PUBLIC_ROOM), |
418 |
430 |
("OFFICE", OFFICE), |
419 |
431 |
("PRIVATE_ROOM", PRIVATE_ROOM), |
420 |
432 |
("WORKSHOP", WORKSHOP), |
421 |
433 |
("OTHER", OTHER), |
422 |
434 |
), |
423 |
435 |
help_text=_("The category that best suits the character of this room."), |
424 |
436 |
) |
425 |
437 |
reservable = models.BooleanField( |
426 |
438 |
default=True, |
427 |
439 |
help_text=_("Indicates if this room can be reserved for something."), |
428 |
440 |
) |
429 |
441 |
note = models.TextField( |
430 |
442 |
blank=True, |
431 |
443 |
help_text=_("If some additional info is required for this room, like a " |
432 |
444 |
"characteristic property (e.g. 'Usually occupied by 2BACH " |
433 |
445 |
"informatics'), state it here."), |
434 |
446 |
) |
435 |
447 |
# TODO: Add a campus/building field or not? |
436 |
448 |
|
437 |
449 |
def reservation_possible(self, begin, end, seats=None): |
438 |
450 |
""" Returns a boolean indicating if reservating during the given time |
439 |
451 |
is possible. If the begin overlaps with a reservation's end or vice versa, |
440 |
452 |
this is regarded as possible. |
441 |
453 |
Takes seats as optional argument. If not specified, it is assumed the entire |
442 |
454 |
room has to be reserved. """ |
443 |
455 |
if self.reservable is False: |
444 |
456 |
return False |
445 |
457 |
if seats is not None and seats < 0: raise ValueError(_("seats ∈ ℕ")) |
446 |
458 |
|
447 |
459 |
reservations = RoomReservation.objects.filter(room=self) |
448 |
460 |
for reservation in reservations: |
449 |
461 |
if reservation.end <= begin or reservation.begin >= end: |
450 |
462 |
continue # Can be trivially skipped, no overlap here |
451 |
463 |
elif seats is None or reservation.seats is None: |
452 |
464 |
return False # The whole room cannot be reserved -> False |
453 |
465 |
elif seats + reservation.seats > self.seats: |
454 |
466 |
return False # Total amount of seats exceeds the available amount -> False |
455 |
467 |
return True # No overlappings found -> True |
456 |
468 |
|
457 |
469 |
def __str__(self): |
458 |
470 |
return self.name |
459 |
471 |
|
460 |
472 |
class RoomReservation(models.Model): |
461 |
473 |
""" Rooms are to be reserved from time to time. They can be reserved |
462 |
474 |
by externals, for something else, and whatnot. That is stored in this table. |
463 |
475 |
""" |
464 |
476 |
room = models.ForeignKey( |
465 |
477 |
"Room", |
466 |
478 |
on_delete=models.CASCADE, |
467 |
479 |
null=False, |
468 |
480 |
#editable=False, |
469 |
481 |
db_index=True, |
470 |
482 |
limit_choices_to={"reservable": True}, |
471 |
483 |
help_text=_("The room that is being reserved at this point."), |
472 |
484 |
) |
473 |
485 |
reservator = models.ForeignKey( |
474 |
486 |
"User", |
475 |
487 |
on_delete=models.CASCADE, |
476 |
488 |
null=False, |
477 |
489 |
#editable=False, |
478 |
490 |
help_text=_("The person that made the reservation (and thus responsible)."), |
479 |
491 |
) |
480 |
492 |
timestamp = models.DateTimeField(auto_now_add=True) |
481 |
493 |
start_time = models.DateTimeField( |
482 |
494 |
null=False, |
483 |
495 |
help_text=_("The time that this reservation starts."), |
484 |
496 |
) |
485 |
497 |
end_time = models.DateTimeField( |
486 |
498 |
null=False, |
487 |
499 |
help_text=_("The time that this reservation ends."), |
488 |
500 |
) |
489 |
501 |
seats = models.PositiveSmallIntegerField( |
490 |
502 |
null=True, |
491 |
503 |
help_text=_("Indicates how many seats are required. If this is left null, " |
492 |
504 |
"it is assumed the entire room has to be reserved."), |
493 |
505 |
) |
494 |
506 |
reason = models.CharField( |
495 |
507 |
max_length=64, |
496 |
508 |
blank=True, |
497 |
509 |
help_text=_("The reason for this reservation, if useful."), |
498 |
510 |
) |
499 |
511 |
note = models.TextField( |
500 |
512 |
blank=True, |
501 |
513 |
help_text=_("If some additional info is required for this reservation, " |
502 |
514 |
"state it here."), |
503 |
515 |
) |
504 |
516 |
|
505 |
517 |
def __str__(self): |
506 |
518 |
start = self.start_time.strftime("%H:%M") |
507 |
519 |
end = self.end_time.strftime("%H:%M") |
508 |
520 |
return str(self.room) +" | "+ start +"-"+ end |
509 |
521 |
|
510 |
522 |
class Degree(models.Model): |
511 |
523 |
""" Contains all degrees that were achieved at this university. |
512 |
524 |
There are no foreign keys in this field. This allows system |
513 |
525 |
administrators to safely remove accounts from alumni, without |
514 |
526 |
the risk of breaking referential integrity or accidentally removing |
515 |
527 |
degrees. |
516 |
528 |
While keeping some fields editable that look like they shouldn't be |
517 |
529 |
(e.g. first_name), this makes it possible for alumni to have a name change |
518 |
530 |
later in their life, and still being able to get a copy of their degree. """ |
519 |
531 |
""" Reason for an ID field for every degree: |
520 |
532 |
This system allows for employers to verify that a certain applicant has indeed, |
521 |
533 |
achieved the degrees (s)he proclaims to have. Because of privacy concerns, |
522 |
534 |
a university cannot disclose information about alumni. |
523 |
535 |
That's where the degree ID comes in. This ID can be printed on all future |
524 |
536 |
degrees. The employer can then visit the university's website, and simply |
525 |
537 |
enter the ID. The website will then simply print what study is attached to |
526 |
538 |
this degree, but not disclose names or anything identifiable. This strikes |
527 |
539 |
thé perfect balance between (easy and digital) degree verification for employers, and maintaining |
528 |
540 |
alumni privacy to the highest extent possible. """ |
529 |
541 |
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) |
530 |
542 |
first_name = models.CharField( |
531 |
543 |
max_length=64, |
532 |
544 |
blank=False, |
533 |
545 |
) |
534 |
546 |
last_name = models.CharField( |
535 |
547 |
max_length=64, |
536 |
548 |
blank=False, |
537 |
549 |
) |
538 |
550 |
additional_names = models.CharField( |
539 |
551 |
max_length=64, |
540 |
552 |
blank=True, |
541 |
553 |
) |
542 |
554 |
DOB = models.DateField(null=False)#editable=False, null=False) # This can't be changed, of course |
543 |
555 |
POB = models.CharField( |
544 |
556 |
max_length=64, |
545 |
557 |
blank=False, |
546 |
558 |
#editable=False, |
547 |
559 |
) |
548 |
560 |
# The study also has to be a charfield, because if a study is removed, |
549 |
561 |
# The information will be lost. |
550 |
562 |
study = models.CharField( |
551 |
563 |
max_length=64, |
552 |
564 |
blank=False, |
553 |
565 |
#editable=False, |
554 |
566 |
) |
555 |
567 |
achieved = models.DateField(null=False)#editable=False, null=False) |
556 |
568 |
user = models.ForeignKey( |
557 |
569 |
"User", |
558 |
570 |
on_delete=models.SET_NULL, |
559 |
571 |
null=True, |
560 |
572 |
help_text=_("The person that achieved this degree, if (s)he still has " |
561 |
573 |
"an account at this university. If the account is deleted " |
562 |
574 |
"at a later date, this field will be set to NULL, but the " |
563 |
575 |
"other fields will be retained."), |
564 |
576 |
) |
565 |
577 |
|
566 |
578 |
def __str__(self): |
567 |
579 |
return self.first_name +" "+ self.last_name +" | "+ self.study |
568 |
580 |
courses/models.py ¶
14 additions and 6 deletions.
View changes Hide changes
1 |
1 |
from django.utils.translation import ugettext_lazy as _ |
2 |
2 |
|
3 |
3 |
class Course(models.Model): |
4 |
4 |
""" Represents a course that is taught at the university. """ |
5 |
5 |
number = models.PositiveSmallIntegerField( |
6 |
6 |
primary_key=True, |
7 |
7 |
blank=False, |
8 |
8 |
help_text=_("The number associated with this course. A leading '0' will be added if the number is smaller than 1000."), |
9 |
9 |
) |
10 |
10 |
name = models.CharField( |
11 |
11 |
max_length=64, |
12 |
12 |
blank=False, |
13 |
13 |
help_text=_("The name of this course, in the language that it is taught. Translations are for the appropriate template."), |
14 |
14 |
) |
15 |
15 |
slug_name = models.SlugField( |
+ |
16 |
max_length=6, |
+ |
17 |
blank=False, |
+ |
18 |
default=constants.COLORS['uhasselt-default'], |
+ |
19 |
help_text=_("The color for this course. Must be an hexadecimal code."), |
+ |
20 |
validators=['validate_hex_color'], |
+ |
21 |
) |
+ |
22 |
slug_name = models.SlugField( |
16 |
23 |
blank=False, |
17 |
24 |
allow_unicode=True, |
18 |
25 |
unique=True, |
19 |
26 |
help_text=_("A so-called 'slug name' for this course."), |
20 |
27 |
) |
21 |
28 |
contact_person = models.ForeignKey( |
+ |
29 |
contact_person = models.ForeignKey( |
22 |
30 |
"administration.User", |
23 |
31 |
on_delete=models.PROTECT, # A course must have a contact person |
24 |
32 |
limit_choices_to={'is_staff': True}, |
25 |
33 |
null=False, |
26 |
34 |
help_text=_("The person to contact regarding this course."), |
27 |
35 |
related_name="contact_person", |
28 |
36 |
) |
29 |
37 |
coordinator = models.ForeignKey( |
30 |
38 |
"administration.User", |
31 |
39 |
on_delete=models.PROTECT, # A course must have a coordinator |
32 |
40 |
limit_choices_to={'is_staff': True}, |
33 |
41 |
null=False, |
34 |
42 |
help_text=_("The person whom's the coordinator of this course."), |
35 |
43 |
related_name="coordinator", |
36 |
44 |
) |
37 |
45 |
educating_team = models.ManyToManyField( |
38 |
46 |
"administration.User", |
39 |
47 |
# No on_delete, since M->M cannot be required at database level |
40 |
48 |
limit_choices_to={'is_staff': True}, |
41 |
49 |
#null=False, # Useless on a M->M |
42 |
50 |
help_text=_("The team members of this course."), |
43 |
51 |
related_name="educating_team", |
44 |
52 |
) |
45 |
53 |
language = models.CharField( |
46 |
54 |
max_length=64, |
47 |
55 |
choices = ( |
48 |
56 |
('NL', _("Dutch")), |
49 |
57 |
('EN', _("English")), |
50 |
58 |
('FR', _("French")), |
51 |
59 |
), |
52 |
60 |
null=False, |
53 |
61 |
help_text=_("The language in which this course is given."), |
54 |
62 |
) |
55 |
63 |
|
56 |
64 |
def __str__(self): |
57 |
65 |
number = str(self.number) |
58 |
66 |
for i in [10,100,1000]: |
59 |
67 |
if self.number < i: |
60 |
68 |
number = "0" + number |
61 |
69 |
return "(" + number + ") " + self.name |
62 |
70 |
|
63 |
71 |
|
64 |
72 |
class Prerequisite(models.Model): |
65 |
73 |
""" Represents a collection of prerequisites a student must have obtained |
66 |
74 |
before being allowed to partake in this course. |
67 |
75 |
It's possible that, if a student has obtained credits in a certain set of |
68 |
76 |
courses, a certain part of the prerequisites do not have to be obtained. |
69 |
77 |
Because of this, make a different record for each different set. In other |
70 |
78 |
words: If one set of prerequisites is obtained, and another one isn't, BUT |
71 |
79 |
they point to the same course, the student is allowed to partake. """ |
72 |
80 |
course = models.ForeignKey( |
73 |
81 |
"Course", |
74 |
82 |
on_delete=models.CASCADE, |
75 |
83 |
null=False, |
76 |
84 |
help_text=_("The course that these prerequisites are for."), |
77 |
85 |
related_name="prerequisite_course", |
78 |
86 |
) |
79 |
87 |
name = models.CharField( |
80 |
88 |
max_length=64, |
81 |
89 |
blank=True, |
82 |
90 |
help_text=_("To specify a name for this set, if necessary."), |
83 |
91 |
) |
84 |
92 |
sequentialities = models.ManyToManyField( |
85 |
93 |
"Course", |
86 |
94 |
help_text=_("All courses for which a credit must've been received in order to follow the course."), |
87 |
95 |
related_name="sequentialities", |
88 |
96 |
) |
89 |
97 |
in_curriculum = models.ManyToManyField( |
90 |
98 |
"Course", |
91 |
99 |
help_text=_("All courses that have to be in the curriculum to follow this. If a credit was achieved, that course can be omitted."), |
92 |
100 |
related_name="in_curriculum", |
93 |
101 |
) |
94 |
102 |
required_study = models.ForeignKey( |
95 |
103 |
"Study", |
96 |
104 |
on_delete=models.CASCADE, |
97 |
105 |
null=True, |
98 |
106 |
help_text=_("If one must have a certain amount of obtained ECTS points for a particular course, state that course here."), |
99 |
107 |
) |
100 |
108 |
ECTS_for_required_study = models.PositiveSmallIntegerField( |
101 |
109 |
null=True, |
102 |
110 |
help_text=_("The amount of obtained ECTS points for the required course, if any."), |
103 |
111 |
) |
104 |
112 |
|
105 |
113 |
def __str__(self): |
106 |
114 |
if self.name == "": |
107 |
115 |
return _("Prerequisites for %(course)s") % {'course': str(self.course)} |
108 |
116 |
else: |
109 |
117 |
return self.name + " | " + str(self.course) |
110 |
118 |
|
111 |
119 |
|
112 |
120 |
class ProgrammeInformation(models.Model): |
113 |
- | """ It's possible that a course is taught in multiple degree programmes; For |
+ |
121 |
""" It's possible that a course is taught in multiple degree programmes; For |
114 |
122 |
example: Calculus can easily be taught to physics and mathematics students |
115 |
123 |
alike. In this table, these relations are set up, and the related properties |
116 |
124 |
are defined as well. """ |
117 |
125 |
study = models.ForeignKey( |
118 |
126 |
"Study", |
119 |
127 |
on_delete=models.CASCADE, |
120 |
128 |
null=False, |
121 |
129 |
help_text=_("The study in which the course is taught."), |
122 |
130 |
) |
123 |
131 |
course = models.ForeignKey( |
124 |
132 |
"Course", |
125 |
133 |
on_delete=models.CASCADE, |
126 |
134 |
null=False, |
127 |
135 |
help_text=_("The course that this information is for."), |
128 |
- | ) |
+ |
136 |
) |
129 |
137 |
study_programme = models.ForeignKey( |
130 |
138 |
"StudyProgramme", |
131 |
139 |
on_delete=models.CASCADE, |
132 |
140 |
null=False, |
133 |
141 |
help_text=_("The study programme that this course belongs to."), |
134 |
142 |
) |
135 |
143 |
programme_type = models.CharField( |
136 |
144 |
max_length=1, |
137 |
145 |
blank=False, |
138 |
146 |
choices = ( |
139 |
147 |
('C', _("Compulsory")), |
140 |
148 |
('O', _("Optional")), |
141 |
149 |
), |
142 |
150 |
help_text=_("Type of this course for this study."), |
143 |
151 |
) |
144 |
152 |
study_hours = models.PositiveSmallIntegerField( |
145 |
153 |
blank=False, |
146 |
154 |
help_text=_("The required amount of hours to study this course."), |
147 |
155 |
) |
148 |
156 |
ECTS = models.PositiveSmallIntegerField( |
149 |
157 |
blank=False, |
150 |
158 |
help_text=_("The amount of ECTS points attached to this course."), |
151 |
159 |
) |
152 |
160 |
semester = models.PositiveSmallIntegerField( |
153 |
161 |
blank=False, |
154 |
162 |
choices = ( |
155 |
163 |
(1, _("First semester")), |
156 |
164 |
(2, _("Second semester")), |
157 |
165 |
(3, _("Full year course")), |
158 |
166 |
(4, _("Taught in first quarter")), |
159 |
167 |
(5, _("Taught in second quarter")), |
160 |
168 |
(6, _("Taught in third quarter")), |
161 |
169 |
(7, _("Taught in fourth quarter")), |
162 |
170 |
), |
163 |
171 |
help_text=_("The period in which this course is being taught in this study."), |
164 |
172 |
) |
165 |
173 |
year = models.PositiveSmallIntegerField( |
166 |
174 |
blank=False, |
167 |
175 |
help_text=_("The year in which this course is taught for this study."), |
168 |
176 |
) |
169 |
177 |
second_chance = models.BooleanField( |
170 |
178 |
default=True, |
171 |
179 |
help_text=_("Defines if a second chance exam is planned for this course."), |
172 |
180 |
) |
173 |
181 |
tolerable = models.BooleanField( |
174 |
182 |
default=True, |
175 |
183 |
help_text=_("Defines if a failed result can be tolerated."), |
176 |
184 |
) |
177 |
185 |
scoring = models.CharField( |
178 |
186 |
max_length=2, |
179 |
187 |
choices = ( |
180 |
188 |
('N', _("Numerical")), |
181 |
189 |
('FP', _("Fail/Pass")), |
182 |
190 |
), |
183 |
191 |
default='N', |
184 |
192 |
blank=False, |
185 |
193 |
help_text=_("How the obtained score for this course is given."), |
186 |
194 |
) |
187 |
195 |
|
188 |
196 |
def __str__(self): |
189 |
197 |
return str(self.study) + " - " + str(self.course) |
190 |
198 |
|
191 |
199 |
class Study(models.Model): |
192 |
200 |
""" Defines a certain study that can be followed at the university. |
193 |
201 |
This also includes abridged study programmes, like transition programmes. |
194 |
202 |
Other information, such as descriptions, are kept in the template file |
195 |
203 |
of this study, which can be manually edited. Joeni searches for a file |
196 |
204 |
with the exact name as the study + ".html". So if the study is called |
197 |
205 |
"Bachelor of Informatics", it will search for "Bachelor of Informatics.html". |
198 |
206 |
""" |
199 |
207 |
# Degree types |
200 |
208 |
BSc = _("Bachelor of Science") |
201 |
209 |
MSc = _("Master of Science") |
202 |
210 |
LLB = _("Bachelor of Laws") |
203 |
211 |
LLM = _("Master of Laws") |
204 |
212 |
ir = _("Engineer") |
205 |
213 |
ing = _("Technological Engineer") |
206 |
214 |
# Faculties |
207 |
215 |
FoMaLS = _("Faculty of Medicine and Life Sciences") |
208 |
216 |
FoS = _("Faculty of Sciences") |
209 |
217 |
FoTS = _("Faculty of Transportation Sciences") |
210 |
218 |
FoAaA = _("Faculty of Architecture and Arts") |
211 |
219 |
FoBE = _("Faculty of Business Economics") |
212 |
220 |
FoET = _("Faculty of Engineering Technology") |
213 |
221 |
FoL = _("Faculty of Law") |
214 |
222 |
|
215 |
223 |
name = models.CharField( |
216 |
224 |
max_length=128, |
217 |
225 |
blank=False, |
218 |
226 |
unique=True, |
219 |
227 |
help_text=_("The full name of this study, in the language it's taught in."), |
220 |
228 |
) |
221 |
229 |
degree_type = models.CharField( |
222 |
230 |
max_length=64, |
223 |
231 |
choices = ( |
224 |
232 |
('BSc', BSc), |
225 |
233 |
('MSc', MSc), |
226 |
234 |
('LL.B', LLB), |
227 |
235 |
('LL.M', LLM), |
228 |
236 |
('ir.', ir ), |
229 |
237 |
('ing.',ing), |
230 |
238 |
), |
231 |
239 |
blank=False, |
232 |
240 |
help_text=_("The type of degree one obtains upon passing this study."), |
233 |
241 |
) |
234 |
242 |
language = models.CharField( |
235 |
243 |
max_length=64, |
236 |
244 |
choices = ( |
237 |
245 |
('NL', _("Dutch")), |
238 |
246 |
('EN', _("English")), |
239 |
247 |
('FR', _("French")), |
240 |
248 |
), |
241 |
249 |
null=False, |
242 |
250 |
help_text=_("The language in which this study is given."), |
243 |
251 |
) |
244 |
252 |
# Information about exam committee |
245 |
253 |
chairman = models.ForeignKey( |
246 |
254 |
"administration.User", |
247 |
255 |
on_delete=models.PROTECT, |
248 |
256 |
null=False, |
249 |
257 |
limit_choices_to={'is_staff': True}, |
250 |
258 |
help_text=_("The chairman of this study."), |
251 |
259 |
related_name="chairman", |
252 |
260 |
) |
253 |
261 |
vice_chairman = models.ForeignKey( |
254 |
262 |
"administration.User", |
255 |
263 |
on_delete=models.PROTECT, |
256 |
264 |
null=False, |
257 |
265 |
help_text=_("The vice-chairman of this study."), |
258 |
266 |
limit_choices_to={'is_staff': True}, |
259 |
267 |
related_name="vice_chairman", |
260 |
268 |
) |
261 |
269 |
secretary = models.ForeignKey( |
262 |
270 |
"administration.User", |
263 |
271 |
on_delete=models.PROTECT, |
264 |
272 |
null=False, |
265 |
273 |
help_text=_("The secretary of this study."), |
266 |
274 |
limit_choices_to={'is_staff': True}, |
267 |
275 |
related_name="secretary", |
268 |
276 |
) |
269 |
277 |
ombuds = models.ForeignKey( |
270 |
278 |
"administration.User", |
271 |
279 |
on_delete=models.PROTECT, |
272 |
280 |
null=False, |
273 |
281 |
help_text=_("The ombuds person of this study."), |
274 |
282 |
limit_choices_to={'is_staff': True}, |
275 |
283 |
related_name="ombuds", |
276 |
284 |
) |
277 |
285 |
vice_ombuds = models.ForeignKey( |
278 |
286 |
"administration.User", |
279 |
287 |
on_delete=models.PROTECT, |
280 |
288 |
null=False, |
281 |
289 |
help_text=_("The (replacing) ombuds person of this study."), |
282 |
290 |
limit_choices_to={'is_staff': True}, |
283 |
291 |
related_name="vice_ombuds", |
284 |
292 |
) |
285 |
293 |
additional_members = models.ManyToManyField( |
286 |
294 |
"administration.User", |
287 |
295 |
help_text=_("All the other members of the exam committee."), |
288 |
296 |
limit_choices_to={'is_staff': True}, |
289 |
297 |
related_name="additional_members", |
290 |
298 |
) |
291 |
299 |
faculty = models.CharField( |
292 |
300 |
max_length=6, |
293 |
301 |
choices = ( |
294 |
302 |
('FoS', FoS), |
295 |
303 |
('FoTS', FoTS), |
296 |
304 |
('FoAaA', FoAaA), |
297 |
305 |
('FoBE', FoBE), |
298 |
306 |
('FoMaLS', FoMaLS), |
299 |
307 |
('FoET', FoET), |
300 |
308 |
('FoL', FoL), |
301 |
309 |
), |
302 |
310 |
blank=False, |
303 |
311 |
help_text=_("The faculty where this study belongs to."), |
304 |
312 |
) |
305 |
313 |
|
306 |
314 |
#def study_points(self): |
307 |
315 |
""" Returns the amount of study points for this year. |
308 |
316 |
This value is inferred based on the study programme information |
309 |
317 |
records that lists this study as their foreign key. """ |
310 |
318 |
#total_ECTS = 0 |
311 |
319 |
#for course in ProgrammeInformation.objects.filter(study=self): |
312 |
- | #total_ECTS += course.ECTS |
+ |
320 |
#total_ECTS += course.ECTS |
313 |
321 |
#return total_ECTS |
314 |
322 |
# XXX: Commented because this is actually something for the StudyProgramme |
315 |
323 |
def years(self): |
316 |
324 |
""" Returns the amount of years this study takes. |
317 |
325 |
This value is inferred based on the study programme information |
318 |
326 |
records that lists this study as their foreign key. """ |
319 |
327 |
highest_year = 0 |
320 |
328 |
for course in ProgrammeInformation.objects.filter(study=self): |
321 |
- | highest_year = max(highest_year, course.year) |
+ |
329 |
highest_year = max(highest_year, course.year) |
322 |
330 |
return highest_year |
323 |
331 |
|
324 |
332 |
def students(self): |
325 |
333 |
""" Cross references the information stored in the database, and |
326 |
334 |
returns all the students that are following this study in this |
327 |
335 |
academic year. """ |
328 |
336 |
return 0 # TODO |
329 |
337 |
|
330 |
338 |
|
331 |
339 |
def __str__(self): |
332 |
340 |
return self.name |
333 |
341 |
|
334 |
342 |
class StudyProgramme(models.Model): |
335 |
343 |
""" Represents a programme within a certain study. |
336 |
344 |
A good example for this is the different specializations, minors, majors, ... |
337 |
345 |
one can follow within the same study. Nevertheless, they're all made of |
338 |
346 |
a certain set of courses. This table collects all these, and allows one to name |
339 |
347 |
them, so they're distinct from one another. """ |
340 |
348 |
name = models.CharField( |
341 |
349 |
max_length=64, |
342 |
350 |
blank=False, |
343 |
351 |
help_text=_("The name of this programme."), |
344 |
352 |
) |
345 |
353 |
|
346 |
354 |
def courses(self): |
347 |
355 |
""" All courses that are part of this study programme. """ |
348 |
356 |
programmes = ProgrammeInformation.objects.filter(study_programme=self) |
349 |
- | courses = {} |
+ |
357 |
courses = {} |
350 |
358 |
for program in programmes: |
351 |
359 |
courses.add(program.course) |
352 |
360 |
return courses |
353 |
361 |
|
354 |
362 |
def study_points(self, year=None): |
355 |
363 |
""" Returns the amount of study points this programme contains. |
356 |
364 |
Accepts year as an optional argument. If not given, the study points |
357 |
365 |
of all years are returned. """ |
358 |
366 |
programmes = ProgrammeInformation.objects.filter(study_programme=self) |
359 |
- | ECTS = 0 |
+ |
367 |
ECTS = 0 |
360 |
368 |
for program in programmes: |
361 |
369 |
if year is None or program.year == year: |
362 |
370 |
# XXX: This only works if the used implementation does lazy |
363 |
371 |
# evaluation, otherwise this is a type error! |
364 |
372 |
ECTS += program.ECTS |
365 |
373 |
return ECTS |
366 |
374 |
|
367 |
375 |
def __str__(self): |
368 |
376 |
return self.name |
369 |
377 |
|
370 |
378 |
# Tables about things related to the courses: |
371 |
379 |
|
372 |
380 |
class Assignment(models.Model): |
373 |
381 |
""" For courses, it's possible to set up tasks. These tasks are recorded |
374 |
382 |
here. """ |
375 |
383 |
# TODO: Require that only the course team can create assignments for a team. |
376 |
384 |
course = models.ForeignKey( |
377 |
385 |
"Course", |
378 |
386 |
on_delete=models.CASCADE, |
379 |
387 |
null=False, |
380 |
388 |
#editable=False, |
381 |
389 |
db_index=True, |
382 |
390 |
help_text=_("The course for which this task is assigned."), |
383 |
391 |
) |
384 |
392 |
information = models.TextField( |
385 |
393 |
help_text=_("Any additional information regarding the assignment. Orgmode syntax available."), |
386 |
394 |
) |
387 |
395 |
deadline = models.DateTimeField( |
388 |
396 |
null=False, |
389 |
397 |
help_text=_("The date and time this task is due."), |
390 |
398 |
) |
391 |
399 |
posted = models.DateField(auto_now_add=True) |
392 |
400 |
digital_task = models.BooleanField( |
393 |
401 |
default=True, |
394 |
402 |
help_text=_("This determines whether this assignment requires handing " |
395 |
403 |
"in a digital file."), |
396 |
404 |
) |
397 |
405 |
|
398 |
406 |
def __str__(self): |
399 |
407 |
return str(self.course) +" | "+ str(self.posted) |
400 |
408 |
|
401 |
409 |
class Announcement(models.Model): |
402 |
410 |
""" Courses sometimes have to make announcements for the students. """ |
403 |
411 |
course = models.ForeignKey( |
404 |
412 |
"Course", |
405 |
413 |
on_delete=models.CASCADE, |
406 |
414 |
null=False, |
407 |
415 |
#editable=False, |
408 |
416 |
db_index=True, |
409 |
417 |
help_text=_("The course for which this announcement is made."), |
410 |
418 |
) |
411 |
419 |
title = models.CharField( |
412 |
420 |
max_length=20, # Keep It Short & Simple® |
413 |
421 |
help_text=_("A quick title for what this is about."), |
414 |
422 |
) |
415 |
423 |
text = models.TextField( |
416 |
424 |
blank=False, |
417 |
425 |
help_text=_("The announcement itself. Orgmode syntax available."), |
418 |
426 |
) |
419 |
427 |
posted = models.DateTimeField(auto_now_add=True) |
420 |
428 |
|
421 |
429 |
def __str__(self): |
422 |
430 |
return str(self.course) +" | "+ self.posted.strftime("%m/%d") |
423 |
431 |
|
424 |
432 |
class Upload(models.Model): |
425 |
433 |
""" For certain assignments, digital hand-ins may be required. These hand |
426 |
434 |
ins are recorded per student in this table. """ |
427 |
435 |
assignment = models.ForeignKey( |
428 |
436 |
"Assignment", |
429 |
437 |
on_delete=models.CASCADE, |
430 |
438 |
null=False, |
431 |
439 |
#editable=False, |
432 |
440 |
db_index=True, |
433 |
441 |
limit_choices_to={"digital_task": True}, |
434 |
442 |
help_text=_("For which assignment this upload is."), |
435 |
443 |
) |
436 |
444 |
# TODO: Try to find a way to require that, if the upload is made, |
437 |
445 |
# only students that have this course in their curriculum can upload. |
438 |
446 |
student = models.ForeignKey( |
439 |
447 |
"administration.User", |
440 |
448 |
on_delete=models.CASCADE, |
441 |
449 |
null=False, |
442 |
450 |
#editable=False, |
443 |
451 |
limit_choices_to={"is_student": True}, |
444 |
452 |
help_text=_("The student who handed this in."), |
445 |
453 |
) |
446 |
454 |
upload_time = models.DateTimeField(auto_now_add=True) |
447 |
455 |
comment = models.TextField( |
448 |
456 |
blank=True, |
449 |
457 |
help_text=_("If you wish to add an additional comment, state it here."), |
450 |
458 |
) |
451 |
459 |
file = models.FileField( |
452 |
460 |
upload_to="assignments/uploads/%Y/%m/", |
453 |
461 |
null=False, |
454 |
462 |
#editable=False, |
455 |
463 |
help_text=_("The file you want to upload for this assignment."), |
456 |
464 |
) |
457 |
465 |
|
458 |
466 |
|
459 |
467 |
def __str__(self): |
460 |
468 |
deadline = self.assignment.deadline |
461 |
469 |
if deadline < self.upload_time: |
462 |
470 |
return str(self.assignment.course) +" | "+ str(self.student.number) + _("(OVERDUE)") |
463 |
471 |
else: |
464 |
472 |
return str(self.assignment.course) +" | "+ str(self.student.number) |
465 |
473 |
|
466 |
474 |
def item_upload_directory(instance, filename): |
467 |
475 |
return "courses/" + instance.course.slug_name + "/" |
468 |
476 |
class CourseItem(models.Model): |
469 |
477 |
""" Reprensents study material for a course that is being shared by the |
470 |
478 |
course's education team. """ |
471 |
479 |
course = models.ForeignKey( |
472 |
480 |
Course, |
473 |
481 |
on_delete=models.CASCADE, |
474 |
482 |
null=False, |
475 |
483 |
#editable=False, |
476 |
484 |
) |
477 |
485 |
file = models.FileField( |
478 |
486 |
upload_to=item_upload_directory, |
479 |
487 |
null=False, |
480 |
488 |
#editable=False, |
481 |
489 |
help_text=_("The file you wish to upload."), |
482 |
490 |
) |
483 |
491 |
timestamp = models.DateTimeField(auto_now_add=True) |
484 |
492 |
note = models.TextField( |
485 |
493 |
blank=True, |
486 |
494 |
help_text=_("If you want to state some additional information about " |
487 |
495 |
"this upload, state it here."), |
488 |
496 |
) |
489 |
497 |
|
490 |
498 |
class StudyGroup(models.Model): |
491 |
499 |
""" It may be necessary to make study groups regarding a course. These |
492 |
500 |
are recorded here, and blend in seamlessly with the Groups from Agora. |
493 |
501 |
Groups that are recorded as a StudyGroup, are given official course status, |
494 |
502 |
and thus, cannot be removed until the status of StudyGroup is lifted. """ |
495 |
503 |
course = models.ForeignKey( |
496 |
504 |
"Course", |
497 |
505 |
on_delete=models.CASCADE, |
498 |
506 |
null=False, |
499 |
507 |
#editable=False, |
500 |
508 |
db_index=True, |
501 |
509 |
help_text=_("The course for which this group is."), |
502 |
510 |
) |
503 |
511 |
group = models.ForeignKey( |
504 |
512 |
"agora.Group", |
505 |
513 |
on_delete=models.PROTECT, # See class documentation |
506 |
514 |
null=False, |
507 |
515 |
#editable=False, # Keep the same group |
508 |
516 |
help_text=_("The group that will be seen as the study group."), |
509 |
517 |
) |
510 |
518 |
|
511 |
519 |
def __str__(self): |
512 |
520 |
return str(self.course) +" | "+ str(self.group) |
513 |
521 |
courses/templates/courses/index.djhtml ¶
24 additions and 0 deletions.
View changes Hide changes
+ |
1 |
{% load i18n %} |
+ |
2 |
|
+ |
3 |
{% block title %} |
+ |
4 |
{% trans "Joeni | Courses" %} |
+ |
5 |
{% endblock %} |
+ |
6 |
|
+ |
7 |
{% block main %} |
+ |
8 |
|
+ |
9 |
|
+ |
10 |
{% for course in courses %} |
+ |
11 |
|
+ |
12 |
|
+ |
13 |
{{ course.name|lower|capfirst }} |
+ |
14 |
{% if course.number < 10 %}(000{{ course.number }}) |
+ |
15 |
{% elif course.number < 100 %}(00{{ course.number }}) |
+ |
16 |
{% elif course.number < 1000 %}(0{{ course.number }}) |
+ |
17 |
{% else %}({{ course.number }}) |
+ |
18 |
|
+ |
19 |
|
+ |
20 |
{% endfor %} |
+ |
21 |
|
+ |
22 |
{% endblock main %} |
+ |
23 |
|
+ |
24 |
courses/views.py ¶
47 additions and 0 deletions.
View changes Hide changes
1 |
1 |
|
+ |
2 |
from django.core.urlresolvers import reverse # Why? |
+ |
3 |
from django.utils.translation import ugettext as _ |
+ |
4 |
from .models import * |
+ |
5 |
import joeni.administration |
+ |
6 |
|
+ |
7 |
def current_academic_year(): |
+ |
8 |
""" Returns the current academic year. The year is determined as follows: |
+ |
9 |
- If today is before September 15 of the current year, the returned value |
+ |
10 |
is the current year - 1. |
+ |
11 |
- If today is after September 15 of the current year, but before January 1 |
+ |
12 |
of the next year, it returns the current year as is. |
+ |
13 |
""" |
+ |
14 |
today = datetime.datetime.now() |
+ |
15 |
switch = datetime.datetime.date(datetime.datetime.year, 9, 15) |
+ |
16 |
if today < switch: |
+ |
17 |
return today.year - 1 |
+ |
18 |
else: |
+ |
19 |
return today.year |
+ |
20 |
|
+ |
21 |
@login_required |
+ |
22 |
def index(request): |
+ |
23 |
""" Starting page regarding the courses. This serves two specific groups: |
+ |
24 |
- Students: Displays all courses that this student has in his/her curriculum |
+ |
25 |
for this academic year. Requires the curriculum to be accepted. |
+ |
26 |
- Staff: Displays all courses in which the staff member is part of the |
+ |
27 |
educating team, or is otherwise related to the course. |
+ |
28 |
Users who are not logged in will be sent to the login page. |
+ |
29 |
""" |
+ |
30 |
template = "courses/index.djhtml" |
+ |
31 |
courses = set() |
+ |
32 |
if request.user.is_student: |
+ |
33 |
curricula = administration.models.Curriculum.objects.filter(student=request.user) |
+ |
34 |
current_curriculum = curricula.filter(year__year=current_academic_year()) |
+ |
35 |
courses = current_curriculum.courses |
+ |
36 |
elif request.user.is_staff: |
+ |
37 |
courses += adminstration.models.Course.filter(contact_person=request.user) |
+ |
38 |
courses += adminstration.models.Course.filter(coordinator=request.user) |
+ |
39 |
courses += adminstration.models.Course.filter(educating_team__contains=request.user) |
+ |
40 |
else: |
+ |
41 |
raise django.exceptions.FieldError("User "+request.user.number+" is neither staff nor student") |
+ |
42 |
|
+ |
43 |
context = { |
+ |
44 |
'courses': courses, |
+ |
45 |
} |
+ |
46 |
|
+ |
47 |
return render(request, template, context) |
+ |
48 |
joeni/templates/joeni/base.djhtml ¶
72 additions and 0 deletions.
View changes Hide changes
+ |
1 |
{% load i18n %} |
+ |
2 |
{% get_current_language as LANGUAGE_CODE %} |
+ |
3 |
{% get_language_info for LANGUAGE_CODE as lang %} |
+ |
4 |
{% load static %} |
+ |
5 |
{% get_media_prefix as media %} |
+ |
6 |
|
+ |
7 |
|
+ |
8 |
|
+ |
9 |
|
+ |
10 |
|
+ |
11 |
{% block title %} |
+ |
12 |
◀ Joeni /▶ | ▶▶ UHasselt |
+ |
13 |
{% endblock title %} |
+ |
14 |
|
+ |
15 |
|
+ |
16 |
{% block stylesheets %} |
+ |
17 |
|
+ |
18 |
|
+ |
19 |
header, footer { |
+ |
20 |
background-color: #{{ user.account.settings.color|default:"UHASSELT" }}; |
+ |
21 |
} |
+ |
22 |
|
+ |
23 |
{% endblock stylesheets %} |
+ |
24 |
|
+ |
25 |
{% block metaflags %} |
+ |
26 |
{# This is standard for all web pages and doesn't require changing. #} |
+ |
27 |
{# UTF-8, always #} |
+ |
28 |
|
+ |
29 |
{##} |
+ |
30 |
{# Indicates this page is suited for mobile devices #} |
+ |
31 |
|
+ |
32 |
|
+ |
33 |
name="description" |
+ |
34 |
content="{% block description %} |
+ |
35 |
{% trans "The digital platform of Hasselt University" %} |
+ |
36 |
{% endblock description %}" /> |
+ |
37 |
{% endblock metaflags %} |
+ |
38 |
|
+ |
39 |
|
+ |
40 |
|
+ |
41 |
|
+ |
42 |
{% block header %} |
+ |
43 |
{% include "joeni/navbar.djhtml" %} |
+ |
44 |
{% endblock header %} |
+ |
45 |
|
+ |
46 |
|
+ |
47 |
|
+ |
48 |
{% block main %} |
+ |
49 |
{% endblock main %} |
+ |
50 |
|
+ |
51 |
|
+ |
52 |
|
+ |
53 |
{% block footer %} |
+ |
54 |
{% include "joeni/footer.djhtml" %} |
+ |
55 |
{% endblock footer %} |
+ |
56 |
|
+ |
57 |
|
+ |
58 |
|
+ |
59 |
{% block JavaScript %} |
+ |
60 |
{% comment JavaScript %} |
+ |
61 |
My website does not require JavaScript for basic actions. However, it may be |
+ |
62 |
used to make certain things look better, or to spice up some actions for the |
+ |
63 |
user. Should it be necessary, add the appropriate JavaScript code in this |
+ |
64 |
block. |
+ |
65 |
The reason this block is at the bottom? |
+ |
66 |
Remember to always put JavaScript on the bottom to reduce load time. |
+ |
67 |
Otherwise it just errors like a fucktard, which is really the only thing |
+ |
68 |
you can always expect from JavaScript. |
+ |
69 |
{% endcomment %} |
+ |
70 |
{% endblock JavaScript %} |
+ |
71 |
|
+ |
72 |
joeni/templates/joeni/footer.djhtml ¶
26 additions and 0 deletions.
View changes Hide changes
+ |
1 |
{% load static %} |
+ |
2 |
{% get_media_prefix as media %} |
+ |
3 |
|
+ |
4 |
|
+ |
5 |
{% blocktrans %} |
+ |
6 |
|
+ |
7 |
Joeni is the digital platform of Hasselt University, designed with an |
+ |
8 |
emphasis on the day to day needs of its students and staff. |
+ |
9 |
|
+ |
10 |
{% endblocktrans %} |
+ |
11 |
|
+ |
12 |
|
+ |
13 |
|
+ |
14 |
Agora |
+ |
15 |
|
+ |
16 |
|
+ |
17 |
{% trans "Administration" %} |
+ |
18 |
|
+ |
19 |
|
+ |
20 |
{% trans "Courses" %} |
+ |
21 |
|
+ |
22 |
|
+ |
23 |
|
+ |
24 |
|
+ |
25 |
|
+ |
26 |