Finish creating database tables
- Author
- Maarten 'Vngngdn' Vangeneugden
- Date
- Nov. 18, 2017, 8:17 p.m.
- Hash
- fd0e170b5e330e5cdcdc6a4ceb04f88c6231ff40
- Parent
- b2c65ea82b9bc3245a2713b3105ef0deb132edb2
- Modified files
- administration/models.py
- agora/models.py
- courses/models.py
administration/models.py ¶
201 additions and 1 deletion.
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 |
|
5 |
5 |
class PersonalDetails(models.Model): |
6 |
6 |
user = models.OneToOneField( |
7 |
7 |
'joeni.User', |
8 |
8 |
on_delete=models.CASCADE, |
9 |
9 |
) |
10 |
10 |
|
11 |
11 |
class Curriculum(models.Model): |
12 |
12 |
""" The curriculum of a particular student. |
13 |
13 |
Every academic year, a student has to hand in a curriculum (s)he wishes to |
14 |
14 |
follow. This is then reviewed by a committee. A curriculum exists of all the |
15 |
15 |
courses one wants to partake in in a certain year. """ |
16 |
16 |
student = models.ForeignKey( |
17 |
17 |
"joeni.User", |
18 |
18 |
on_delete=models.CASCADE, |
19 |
19 |
limit_choices_to={'is_student': True}, |
20 |
20 |
null=False, |
21 |
21 |
editable=False, |
22 |
22 |
unique_for_year="year", # Only 1 curriculum per year |
23 |
23 |
) |
24 |
24 |
year = models.DateField( |
25 |
25 |
auto_now_add=True, |
26 |
26 |
db_index=True, |
27 |
27 |
help_text=_("The academic year for which this curriculum is."), |
28 |
28 |
) |
29 |
29 |
last_modified = models.DateTimeField( |
30 |
30 |
auto_now=True, |
31 |
31 |
help_text=_("The last timestamp that this was updated."), |
32 |
32 |
) |
33 |
33 |
courses = models.ManyToManyField( |
34 |
34 |
"courses.Course", |
35 |
35 |
null=False, |
36 |
36 |
help_text=_("All the courses included in this curriculum."), |
37 |
37 |
) |
38 |
38 |
approved = models.NullBooleanField( |
39 |
39 |
default=None, |
40 |
40 |
help_text=_("Indicates if this curriculum has been approved. If true, " |
41 |
41 |
"that means the responsible committee has reviewed and " |
42 |
42 |
"approved the student for this curriculum. False otherwise. " |
43 |
43 |
"If review is still pending, the value is NULL. Modifying " |
44 |
44 |
"the curriculum implies this setting is set to NULL again."), |
45 |
45 |
) |
46 |
46 |
note = models.TextField( |
47 |
47 |
blank=True, |
48 |
48 |
help_text=_("Additional notes regarding this curriculum. This has " |
49 |
49 |
"multiple uses. For the student, it is used to clarify " |
50 |
50 |
"any questions, or to motivate why (s)he wants to take a " |
51 |
51 |
"course for which the requirements were not met. " |
52 |
52 |
"The reviewing committee can use this field to argument " |
53 |
53 |
"their decision, especially for when the curriculum is " |
54 |
54 |
"denied."), |
55 |
55 |
) |
56 |
56 |
|
57 |
57 |
def curriculum_type(self): |
58 |
58 |
""" Returns the type of this curriculum. At the moment, this is |
59 |
59 |
either a standard programme, or an individualized programme. """ |
60 |
60 |
# Currently: A standard programme means: All courses are from the |
61 |
61 |
# same study, ánd from the same year. Additionally, all courses |
62 |
62 |
# from that year must've been taken. |
63 |
63 |
# FIXME: Need a way to determine what is the standard programme. |
64 |
64 |
# If not possible, make this a charfield with options or something |
65 |
65 |
pass |
66 |
66 |
|
67 |
67 |
def __str__(self): |
68 |
68 |
year = self.year.year |
69 |
69 |
if self.year.month < 7: |
70 |
70 |
return str(self.student) +" | "+ str(year-1) +"-"+ str(year) |
71 |
71 |
else: |
72 |
72 |
return str(self.student) +" | "+ str(year) +"-"+ str(year+1) |
73 |
73 |
|
74 |
74 |
|
75 |
75 |
class CourseResult(models.Model): |
76 |
76 |
""" A student has to obtain a certain course result. These are stored here, |
77 |
77 |
together with all the appropriate information. """ |
78 |
78 |
# TODO: Validate that a course programme for a student can only be made once per year for each course, if possible. |
79 |
79 |
CRED = _("Credit acquired") |
80 |
80 |
FAIL = _("Credit not acquired") |
81 |
81 |
TLRD = _("Tolerated") |
82 |
82 |
ITLD = _("Tolerance used") |
83 |
83 |
# Possible to add more in the future |
84 |
84 |
|
85 |
85 |
student = models.ForeignKey( |
86 |
86 |
"joeni.User", |
87 |
87 |
on_delete=models.CASCADE, |
88 |
88 |
limit_choices_to={'is_student': True}, |
89 |
89 |
null=False, |
90 |
90 |
) |
91 |
91 |
course_programme = models.ForeignKey( |
92 |
92 |
"courses.ProgrammeInformation", |
93 |
93 |
on_delete=models.PROTECT, |
94 |
94 |
null=False, |
95 |
95 |
) |
96 |
96 |
released = models.DateField( |
97 |
97 |
auto_now=True, |
98 |
98 |
help_text=_("The date that this result was last updated."), |
99 |
99 |
) |
100 |
100 |
first_score = models.PositiveSmallIntegerField( |
101 |
101 |
null=True, # It's possible a score does not exist. |
102 |
102 |
validators=[MaxValueValidator( |
103 |
103 |
20, |
104 |
104 |
_("%(score)s mustn't be higher than 20."), |
105 |
105 |
params={'score': score}, |
106 |
106 |
)], |
107 |
107 |
) |
108 |
108 |
second_score = models.PositiveSmallIntegerField( |
109 |
109 |
null=True, |
110 |
110 |
validators=[MaxValueValidator( |
111 |
111 |
20, |
112 |
112 |
_("%(score)s mustn't be higher than 20."), |
113 |
113 |
params={'score': score}, |
114 |
114 |
)], |
115 |
115 |
) |
116 |
116 |
result = models.CharField( |
117 |
117 |
max_length=10, |
118 |
118 |
choices = ( |
119 |
119 |
("CRED", CRED), |
120 |
120 |
("FAIL", FAIL), |
121 |
121 |
("TLRD", TLRD), |
122 |
122 |
("ITLD", ITLD), |
123 |
123 |
), |
124 |
124 |
blank=False, |
125 |
125 |
help_text=_("The final result this record constitutes."), |
126 |
126 |
) |
127 |
127 |
|
128 |
128 |
def __str__(self): |
129 |
129 |
stdnum = str(self.student.number) |
130 |
130 |
result = self.result |
131 |
131 |
if result == "CRED": |
132 |
132 |
if self.first_score < 10: |
133 |
133 |
result = "C" + self.first_score + "1" |
134 |
134 |
else: |
135 |
135 |
result = "C" + self.second_score + "2" |
136 |
136 |
course = str(self.course_programme.course) |
137 |
137 |
return stdnum +" ("+ result +") | "+ course |
138 |
138 |
|
139 |
139 |
class PreRegistration(models.Model): |
140 |
140 |
""" At the beginning of the new academic year, students can register |
141 |
141 |
themselves at the university. Online, they can do a preregistration already. |
142 |
142 |
These records are stored here and can later be retrieved for the actual |
143 |
143 |
registration process. |
144 |
144 |
Note: The current system in use at Hasselt University provides a password system. |
145 |
145 |
That will be eliminated here. Just make sure that the entered details are correct. |
146 |
146 |
Should there be an error, and the same email address is used to update something, |
147 |
147 |
a mail will be sent to that address to verify this was a genuine update.""" |
148 |
148 |
created = models.DateField(auto_now_add=True) |
149 |
149 |
first_name = models.CharField(max_length=64, blank=False, help_text=_("Your first name.")) |
150 |
150 |
last_name = models.CharField(max_length=64, blank=False, help_text=_("Your last name.")) |
151 |
151 |
additional_names = models.CharField(max_length=64, blank=True, help_text=_("Any additional names.")) |
152 |
152 |
title = models.CharField( |
153 |
153 |
max_length=64, |
154 |
154 |
blank=True, |
155 |
155 |
help_text=_("Any additional titles, prefixes, ..."), |
156 |
156 |
) |
157 |
157 |
DOB = models.DateField( |
158 |
158 |
blank=False, |
159 |
159 |
editable=False, |
160 |
160 |
help_text=_("Your date of birth."), |
161 |
161 |
) |
162 |
162 |
POB = models.CharField( |
163 |
163 |
max_length=64, |
164 |
164 |
blank=False, |
165 |
165 |
editable=False, |
166 |
166 |
help_text=_("The place you were born."), |
167 |
167 |
) |
168 |
168 |
nationality = models.CharField( |
169 |
169 |
max_length=64, |
170 |
170 |
blank=False, |
171 |
171 |
help_text=_("Your current nationality."), |
172 |
172 |
) |
173 |
173 |
national_registry_number = models.BigIntegerField( |
174 |
174 |
null=True, |
175 |
175 |
help_text=_("If you have one, your national registry number."), |
176 |
176 |
) |
177 |
177 |
civil_status = models.CharField( |
178 |
178 |
choices = ( |
179 |
179 |
("Single", _("Single")), |
180 |
180 |
("Married", _("Married")), |
181 |
181 |
("Divorced", _("Divorced")), |
182 |
182 |
("Widowed", _("Widowed")), |
183 |
183 |
("Partnership", _("Partnership")), |
184 |
184 |
), |
185 |
185 |
blank=False, |
186 |
186 |
# There may be more; consult http://www.aantrekkingskracht.com/trefwoord/burgerlijke-staat |
187 |
187 |
# for more information. |
188 |
188 |
help_text=_("Your civil/marital status."), |
189 |
189 |
) |
190 |
190 |
email = models.EmailField( |
191 |
191 |
blank=False, |
192 |
192 |
unique=True, |
193 |
193 |
help_text=_("The e-mail address we will use to communicate until your actual registration."), |
194 |
194 |
) |
195 |
195 |
study = models.ForeignKey( |
196 |
196 |
"courses.Study", |
197 |
197 |
on_delete=models.PROTECT, |
198 |
198 |
null=False, |
199 |
199 |
help_text=_("The study you wish to follow. Be sure to provide all legal" |
200 |
200 |
"documents that are required for this study with this " |
201 |
201 |
"application, or bring them with you to the final registration."), |
202 |
202 |
) |
203 |
203 |
study_type = models.CharField( |
204 |
204 |
max_length=32, |
205 |
205 |
choices = ( |
206 |
206 |
("Diplom contract", _("Diplom contract")), |
207 |
207 |
("Exam contract", _("Exam contract")), |
208 |
208 |
("Credit contract", _("Credit contract")), |
209 |
209 |
), |
210 |
210 |
blank=False, |
211 |
211 |
help_text=_("The type of study contract you wish to follow."), |
212 |
212 |
) |
213 |
213 |
document = models.FileField( |
214 |
214 |
upload_to="pre-enrollment/%Y", |
215 |
215 |
help_text=_("Any legal documents regarding your enrollment."), |
216 |
216 |
) |
217 |
217 |
# XXX: If the database in production is PostgreSQL, comment document, and |
218 |
218 |
# uncomment the next column. |
219 |
219 |
"""documents = models.ArrayField( |
220 |
220 |
models.FileField(upload_to="pre-enrollment/%Y"), |
221 |
221 |
help_text=_("Any legal documents regarding your enrollment."), |
222 |
222 |
)""" |
223 |
223 |
|
224 |
224 |
def __str__(self): |
225 |
225 |
name = self.last_name +" "+ self.first_name |
226 |
226 |
dob = self.DOB.strftime("%d/%m/%Y") |
227 |
227 |
return name +" | "+ dob |
228 |
228 |
|
229 |
229 |
|
230 |
230 |
# Planning and organization related tables |
231 |
231 |
class RoomReservation(models.Model): |
+ |
232 |
""" Represents a room in the university. |
+ |
233 |
Rooms can have a number of properties, which are stored in the database. |
+ |
234 |
""" |
+ |
235 |
# Types of rooms |
+ |
236 |
LABORATORY = _("Laboratory") # Chemistry/Physics equipped rooms |
+ |
237 |
CLASS_ROOM = _("Class room") # Simple class rooms |
+ |
238 |
AUDITORIUM = _("Auditorium") # Large rooms with ample seating and equipment for lectures |
+ |
239 |
PC_ROOM = _("PC room" ) # Rooms equipped for executing PC related tasks |
+ |
240 |
PUBLIC_ROOM= _("Public room") # Restaurants, restrooms, ... general public spaces |
+ |
241 |
OFFICE = _("Office" ) # Private offices for staff |
+ |
242 |
PRIVATE_ROOM = _("Private room") # Rooms accessible for a limited public; cleaning cupboards, kitchens, ... |
+ |
243 |
WORKSHOP = _("Workshop" ) # Rooms with hardware equipment to build and work on materials |
+ |
244 |
OTHER = _("Other" ) # Rooms that do not fit in any other category |
+ |
245 |
|
+ |
246 |
|
+ |
247 |
name = models.CharField( |
+ |
248 |
max_length=20, |
+ |
249 |
primary_key=True, |
+ |
250 |
blank=False, |
+ |
251 |
help_text=_("The name of this room. If more appropriate, this can be the colloquial name."), |
+ |
252 |
) |
+ |
253 |
seats = models.PositiveSmallIntegerField( |
+ |
254 |
help_text=_("The amount of available seats in this room. This can be handy for exams for example."), |
+ |
255 |
) |
+ |
256 |
wheelchair_accessible = models.BooleanField(default=True) |
+ |
257 |
exams_equipped = models.BooleanField( |
+ |
258 |
default=True, |
+ |
259 |
help_text=_("Indicates if exams can reasonably be held in this room."), |
+ |
260 |
) |
+ |
261 |
computers_available = models.PositiveSmallIntegerField( |
+ |
262 |
default=False, |
+ |
263 |
help_text=_("Indicates how many computers are available in this room."), |
+ |
264 |
) |
+ |
265 |
projector_available = models.BooleanField( |
+ |
266 |
default=False, |
+ |
267 |
help_text=_("Indicates if a projector is available at this room."), |
+ |
268 |
) |
+ |
269 |
blackboards_available = models.PositiveSmallIntegerField( |
+ |
270 |
help_text=_("The amount of blackboards available in this room."), |
+ |
271 |
) |
+ |
272 |
whiteboards_available = models.PositiveSmallIntegerField( |
+ |
273 |
help_text=_("The amount of whiteboards available in this room."), |
+ |
274 |
) |
+ |
275 |
category = models.CharField( |
+ |
276 |
max_length=16, |
+ |
277 |
blank=False, |
+ |
278 |
choices = ( |
+ |
279 |
("LABORATORY", LABORATORY), |
+ |
280 |
("CLASS_ROOM", CLASS_ROOM), |
+ |
281 |
("AUDITORIUM", AUDITORIUM), |
+ |
282 |
("PC_ROOM", PC_ROOM), |
+ |
283 |
("PUBLIC_ROOM", PUBLIC_ROOM), |
+ |
284 |
("OFFICE", OFFICE), |
+ |
285 |
("PRIVATE_ROOM", PRIVATE_ROOM), |
+ |
286 |
("WORKSHOP", WORKSHOP), |
+ |
287 |
("OTHER", OTHER), |
+ |
288 |
), |
+ |
289 |
help_text=_("The category that best suits the character of this room."), |
+ |
290 |
) |
+ |
291 |
reservable = models.BooleanField( |
+ |
292 |
default=True, |
+ |
293 |
help_text=_("Indicates if this room can be reserved for something."), |
+ |
294 |
) |
+ |
295 |
note = models.TextField( |
+ |
296 |
blank=True, |
+ |
297 |
help_text=_("If some additional info is required for this room, like a " |
+ |
298 |
"characteristic property (e.g. 'Usually occupied by 2BACH " |
+ |
299 |
"informatics'), state it here."), |
+ |
300 |
) |
+ |
301 |
# TODO: Add a campus/building field or not? |
+ |
302 |
|
+ |
303 |
def reservation_possible(self, begin, end, seats=None): |
+ |
304 |
""" Returns a boolean indicating if reservating during the given time |
+ |
305 |
is possible. If the begin overlaps with a reservation's end or vice versa, |
+ |
306 |
this is regarded as possible. |
+ |
307 |
Takes seats as optional argument. If not specified, it is assumed the entire |
+ |
308 |
room has to be reserved. """ |
+ |
309 |
if self.reservable is False: |
+ |
310 |
return False |
+ |
311 |
if seats is not None and seats < 0: raise ValueError(_("seats ∈ ℕ")) |
+ |
312 |
|
+ |
313 |
reservations = RoomReservation.objects.filter(room=self) |
+ |
314 |
for reservation in reservations: |
+ |
315 |
if reservation.end <= begin or reservation.begin >= end: |
+ |
316 |
continue # Can be trivially skipped, no overlap here |
+ |
317 |
elif seats is None or reservation.seats is None: |
+ |
318 |
return False # The whole room cannot be reserved -> False |
+ |
319 |
elif seats + reservation.seats > self.seats: |
+ |
320 |
return False # Total amount of seats exceeds the available amount -> False |
+ |
321 |
return True # No overlappings found -> True |
+ |
322 |
|
+ |
323 |
def __str__(self): |
+ |
324 |
return self.name |
+ |
325 |
|
+ |
326 |
class RoomReservation(models.Model): |
232 |
327 |
pass |
233 |
- | |
+ |
328 |
by externals, for something else, and whatnot. That is stored in this table. |
+ |
329 |
""" |
+ |
330 |
room = models.ForeignKey( |
+ |
331 |
"Room", |
+ |
332 |
on_delete=models.CASCADE, |
+ |
333 |
null=False, |
+ |
334 |
editable=False, |
+ |
335 |
db_index=True, |
+ |
336 |
limit_choices_to={"reservable": True}, |
+ |
337 |
help_text=_("The room that is being reserved at this point."), |
+ |
338 |
) |
+ |
339 |
reservator = models.ForeignKey( |
+ |
340 |
"joeni.User", |
+ |
341 |
on_delete=models.CASCADE, |
+ |
342 |
null=False, |
+ |
343 |
editable=False, |
+ |
344 |
help_text=_("The person that made the reservation (and thus responsible)."), |
+ |
345 |
) |
+ |
346 |
timestamp = models.DateTimeField(auto_now_add=True) |
+ |
347 |
start_time = models.DateTimeField( |
+ |
348 |
null=False, |
+ |
349 |
help_text=_("The time that this reservation starts."), |
+ |
350 |
) |
+ |
351 |
end_time = models.DateTimeField( |
+ |
352 |
null=False, |
+ |
353 |
help_text=_("The time that this reservation ends."), |
+ |
354 |
) |
+ |
355 |
seats = models.PositiveSmallIntegerField( |
+ |
356 |
null=True, |
+ |
357 |
help_text=_("Indicates how many seats are required. If this is left null, " |
+ |
358 |
"it is assumed the entire room has to be reserved."), |
+ |
359 |
) |
+ |
360 |
reason = models.CharField( |
+ |
361 |
max_length=64, |
+ |
362 |
blank=True, |
+ |
363 |
help_text=_("The reason for this reservation, if useful."), |
+ |
364 |
) |
+ |
365 |
note = models.TextField( |
+ |
366 |
blank=True, |
+ |
367 |
help_text=_("If some additional info is required for this reservation, " |
+ |
368 |
"state it here."), |
+ |
369 |
) |
+ |
370 |
|
+ |
371 |
def __str__(self): |
+ |
372 |
start = self.start_time.strftime("%H:%M") |
+ |
373 |
end = self.end_time.strftime("%H:%M") |
+ |
374 |
return str(self.room) +" | "+ start +"-"+ end |
+ |
375 |
|
+ |
376 |
class Degree(models.Model): |
+ |
377 |
""" Contains all degrees that were achieved at this university. |
+ |
378 |
There are no foreign keys in this field. This allows system |
+ |
379 |
administrators to safely remove accounts from alumni, without |
+ |
380 |
the risk of breaking referential integrity or accidentally removing |
+ |
381 |
degrees. |
+ |
382 |
While keeping some fields editable that look like they shouldn't be |
+ |
383 |
(e.g. first_name), this makes it possible for alumni to have a name change |
+ |
384 |
later in their life, and still being able to get a copy of their degree. """ |
+ |
385 |
""" Reason for an ID field for every degree: |
+ |
386 |
This system allows for employers to verify that a certain applicant has indeed, |
+ |
387 |
achieved the degrees (s)he proclaims to have. Because of privacy concerns, |
+ |
388 |
a university cannot disclose information about alumni. |
+ |
389 |
That's where the degree ID comes in. This ID can be printed on all future |
+ |
390 |
degrees. The employer can then visit the university's website, and simply |
+ |
391 |
enter the ID. The website will then simply print what study is attached to |
+ |
392 |
this degree, but not disclose names or anything identifiable. This strikes |
+ |
393 |
thé perfect balance between (easy and digital) degree verification for employers, and maintaining |
+ |
394 |
alumni privacy to the highest extent possible. """ |
+ |
395 |
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) |
+ |
396 |
first_name = models.CharField( |
+ |
397 |
max_length=64, |
+ |
398 |
blank=False, |
+ |
399 |
) |
+ |
400 |
last_name = models.CharField( |
+ |
401 |
max_length=64, |
+ |
402 |
blank=False, |
+ |
403 |
) |
+ |
404 |
additional_names = models.CharField( |
+ |
405 |
max_length=64, |
+ |
406 |
) |
+ |
407 |
DOB = models.DateField(editable=False, null=False) # This can't be changed, of course |
+ |
408 |
POB = models.CharField( |
+ |
409 |
max_length=64, |
+ |
410 |
blank=False, |
+ |
411 |
editable=False, |
+ |
412 |
) |
+ |
413 |
# The study also has to be a charfield, because if a study is removed, |
+ |
414 |
# The information will be lost. |
+ |
415 |
study = models.CharField( |
+ |
416 |
max_length=64, |
+ |
417 |
blank=False, |
+ |
418 |
editable=False, |
+ |
419 |
) |
+ |
420 |
achieved = models.DateField(editable=False, null=False) |
+ |
421 |
user = models.ForeignKey( |
+ |
422 |
"joeni.User", |
+ |
423 |
on_delete=models.SET_NULL, |
+ |
424 |
null=True, |
+ |
425 |
help_text=_("The person that achieved this degree, if (s)he still has " |
+ |
426 |
"an account at this university. If the account is deleted " |
+ |
427 |
"at a later date, this field will be set to NULL, but the " |
+ |
428 |
"other fields will be retained."), |
+ |
429 |
) |
+ |
430 |
|
+ |
431 |
def __str__(self): |
+ |
432 |
return self.first_name +" "+ self.last_name +" | "+ self.study |
+ |
433 |
agora/models.py ¶
72 additions and 18 deletions.
View changes Hide changes
1 |
1 |
from django.utils.translation import ugettext_lazy as _ |
2 |
2 |
from joeni import constants |
3 |
3 |
|
4 |
4 |
class Account(models.Model): |
5 |
5 |
user = models.OneToOneField( |
6 |
6 |
'joeni.User', |
7 |
7 |
on_delete=models.CASCADE, |
8 |
8 |
primary_key=True, |
9 |
9 |
) |
10 |
10 |
alias = models.CharField(max_length=64, unique=True) |
11 |
11 |
|
12 |
12 |
def __str__(self): |
13 |
13 |
return self.alias |
14 |
14 |
|
15 |
15 |
def account_user_directory(instance, filename): |
16 |
16 |
return '{0}/account/settings/{1}'.format(instace.account.alias, filename) |
17 |
17 |
|
18 |
18 |
class AccountSettings(models.Model): |
19 |
19 |
account = models.OneToOneField( |
20 |
20 |
'Account', |
21 |
21 |
on_delete=models.CASCADE, |
22 |
22 |
) |
23 |
23 |
# TODO: Build validator for primary_color to make sure what is given is a |
24 |
24 |
# valid hexadecimal RGB value. |
25 |
25 |
color = models.CharField( |
26 |
26 |
max_length=6, |
27 |
27 |
help_text=_("The hexadecimal code of the color for this account."), |
28 |
28 |
default = constants.COLORS["UHasselt default"], |
29 |
29 |
blank=False, |
30 |
30 |
validators=[validate_hex_color], |
31 |
31 |
) |
32 |
32 |
account_page_banner = models.ImageField( # Requires the Pillow library! |
33 |
33 |
upload_to=account_user_directory, |
34 |
34 |
help_text=_("The banner image to be shown on this account's homepage."), |
35 |
35 |
) |
36 |
36 |
avatar = models.ImageField( |
37 |
37 |
upload_to=account_user_directory, |
38 |
38 |
help_text=_("The avatar image of this account."), |
39 |
39 |
) |
40 |
40 |
|
41 |
41 |
def __str__(self): |
42 |
42 |
return str(self.account) |
43 |
43 |
|
44 |
44 |
class Post(models.Model): |
45 |
45 |
timestamp = models.DateTimeField(auto_now_add=True) |
46 |
46 |
title = models.CharField( |
47 |
47 |
max_length=64, |
48 |
48 |
blank=True, |
49 |
49 |
help_text=_("The title for this post."), |
50 |
50 |
) |
51 |
51 |
text = models.TextField( |
52 |
52 |
blank=True, |
53 |
53 |
help_text=_("A text message for this post. May be left blank."), |
54 |
54 |
) |
55 |
55 |
author = models.ForeignKey( |
56 |
56 |
"Account", |
57 |
57 |
on_delete=models.CASCADE, |
58 |
58 |
null=False, # There must be an author |
59 |
59 |
editable=False, # It makes no sense to change the author after creation |
60 |
60 |
help_text=_("The authoring account of this post."), |
61 |
61 |
) |
62 |
62 |
response_to = models.ForeignKey( |
63 |
63 |
"self", |
64 |
64 |
on_delete=models.CASCADE, |
65 |
65 |
null=True, # If this is null, this post is not a response, but a beginning post |
66 |
66 |
editable=False, # This cannot be changed after creation, wouldn't make sense |
67 |
67 |
help_text=_("The post to which this was a response, if applicable."), |
68 |
68 |
) |
69 |
69 |
placed_on = models.ForeignKey( |
70 |
70 |
"Page", |
71 |
71 |
on_delete=models.CASCADE, |
72 |
72 |
null=False, |
73 |
73 |
editable=False, |
74 |
74 |
help_text=_("The page on which this post was placed."), |
75 |
75 |
) |
76 |
76 |
# Voting fields |
77 |
77 |
allow_votes = models.BooleanField( |
78 |
78 |
default=True, |
79 |
79 |
help_text=_("Decide whether to allow voting or disable it for this post."), |
80 |
80 |
) |
81 |
81 |
allow_responses = models.BooleanField( |
82 |
82 |
default=True, |
83 |
83 |
help_text=_("Decide if other people can respond to this post or not. " |
84 |
84 |
"This does not influence what people allow on their posts."), |
85 |
85 |
) |
86 |
86 |
|
87 |
87 |
def __str__(self): |
88 |
88 |
return str(self.timestamp) + " | " + str(self.author) |
89 |
89 |
# TODO Add a way to attach geographical data to a post, which could |
90 |
90 |
# then be used with OpenStreetMap or something |
91 |
91 |
|
92 |
92 |
class FilePost(Post): |
93 |
93 |
""" A special type of Post, which has a file linked with it. |
94 |
94 |
The poster can specify how to treat this file. """ |
95 |
95 |
image = _("Image") |
96 |
96 |
video = _("Video") |
97 |
97 |
music = _("Sound") |
98 |
98 |
text = _("Text" ) |
99 |
99 |
other = _("Other") |
100 |
100 |
file = models.FileField( |
101 |
101 |
upload_to="agora/posts/%Y/%m/%d/", |
102 |
102 |
null=False, |
103 |
103 |
editable=False, |
104 |
104 |
help_text=_("The file you wish to share."), |
105 |
105 |
) |
106 |
106 |
file_type = models.CharField( |
107 |
107 |
max_length=16, |
108 |
108 |
blank=False, |
109 |
109 |
choices = ( |
110 |
110 |
('image', image), |
111 |
111 |
('video', video), |
112 |
112 |
('music', music), |
113 |
113 |
('text' , text ), |
114 |
114 |
('other', other), |
115 |
115 |
), |
116 |
116 |
help_text=_("How this file should be seen as."), |
117 |
117 |
) |
118 |
118 |
|
119 |
119 |
class Page(models.Model): |
120 |
120 |
""" In the university, people can create pages for everything they want and |
121 |
121 |
then some. These pages are put in the database through this table. """ |
122 |
122 |
name = models.CharField( |
123 |
123 |
max_length=64, |
124 |
124 |
primary_key=True, |
125 |
125 |
blank=False, |
126 |
126 |
help_text=_("The name of this page."), |
127 |
127 |
) |
128 |
128 |
created = models.DateTimeField(auto_now_add=True) |
129 |
129 |
hidden = models.BooleanField( |
130 |
130 |
default=False, |
131 |
131 |
help_text=_("Determines if this page can be found without a direct link."), |
132 |
132 |
) |
133 |
133 |
main_content = models.TextField( |
134 |
134 |
blank=True, |
135 |
135 |
help_text=_("If you want to put some text on this page, " |
136 |
136 |
"you can put it here. You can use Orgmode-syntax to " |
137 |
137 |
"get as much out of your page as possible. While doing so, " |
138 |
138 |
"be aware of the limitations imposed by the code of conduct."), |
139 |
139 |
) |
140 |
140 |
public_posting = models.BooleanField( |
141 |
141 |
default=True, |
142 |
142 |
help_text=_("Determines if everyone can post on this page, or only the " |
143 |
143 |
"people that are linked with it. Know that if a post is made " |
144 |
144 |
"and responding is allowed, everyone can respond to that post."), |
145 |
145 |
) |
146 |
146 |
|
147 |
147 |
class Meta: |
148 |
148 |
abstract=True |
149 |
149 |
|
150 |
150 |
class AccountPage(Page): |
151 |
151 |
""" Every account has its own homepage. This is that page. |
152 |
152 |
This page can only be edited by the account holder, or staff members. """ |
153 |
153 |
# TODO: Find a way to auto-create one of these every time a new account is created |
154 |
154 |
# TODO: Require that changes can only occur by either the account holder or staff |
155 |
155 |
account = models.OneToOneField( |
156 |
156 |
"Account", |
157 |
157 |
null=False, |
158 |
158 |
on_delete=models.CASCADE, |
159 |
159 |
) |
160 |
160 |
|
161 |
161 |
class GroupPage(Page): |
162 |
162 |
""" A page where a group can present itself to the university. |
163 |
163 |
This page can only be edited by group members or staff members. """ |
164 |
164 |
# TODO: Find a way to auto-create one of these every time a new group is created |
165 |
165 |
# TODO: Require that changes can only occur by either the group or staff |
166 |
166 |
group = models.ForeignKey( |
167 |
167 |
"Group", |
168 |
168 |
null=False, |
169 |
169 |
on_delete=models.CASCADE, |
170 |
170 |
) |
171 |
171 |
|
172 |
172 |
class CoursePage(Page): |
173 |
173 |
""" A page that serves as a course's main entry point. |
174 |
174 |
This page can only be edited by the course's educating team or staff members. """ |
175 |
175 |
# TODO: Find a way to auto-create one of these every time a new course is created |
176 |
176 |
# TODO: Require that changes can only occur by either the course team or staff |
177 |
177 |
course = models.OneToOneField( |
178 |
178 |
"courses.Course", |
179 |
179 |
null=False, |
180 |
180 |
on_delete=models.CASCADE, |
181 |
181 |
) |
182 |
182 |
|
183 |
183 |
class Group(models.Model): |
184 |
184 |
""" It is imperative that everyone can come together with other people. |
185 |
185 |
A Group record is the way to accomplish this. """ |
186 |
186 |
name = models.CharField( |
187 |
187 |
max_length=64, |
188 |
188 |
primary_key=True, # So be unique I'd say |
189 |
189 |
blank=False, |
190 |
190 |
help_text=_("The name of your group."), |
191 |
191 |
) |
192 |
192 |
color = models.CharField( |
193 |
193 |
max_length=6, |
194 |
194 |
help_text=_("The hexadecimal code of the color for this group."), |
195 |
195 |
default = constants.COLORS["UHasselt default"], |
196 |
196 |
blank=False, |
197 |
197 |
validators=[validate_hex_color], |
198 |
198 |
) |
199 |
199 |
members = models.ManyToManyField( |
200 |
200 |
"Account", |
201 |
201 |
help_text=_("The members of this group."), |
202 |
202 |
) |
203 |
203 |
invite_only = models.BooleanField( |
204 |
204 |
default=True, |
205 |
205 |
help_text=_("Determines if everyone can join this group, or if " |
206 |
206 |
"only members can invite others."), |
207 |
207 |
) |
208 |
208 |
private = models.BooleanField( |
209 |
209 |
default=True, |
210 |
210 |
help_text=_("Determines if this group is visible to non-members."), |
211 |
211 |
) |
212 |
212 |
|
213 |
213 |
def __str__(self): |
214 |
214 |
return self.name |
215 |
215 |
|
216 |
216 |
|
217 |
217 |
class AccountCollection(models.Model): |
218 |
218 |
""" Every account can make a collection in which (s)he can list accounts |
219 |
219 |
at his/her wish. This can be a collection of Friends, study collegues, |
220 |
220 |
project partners, and so on. |
221 |
221 |
Accounts that are in a certain collection are not notified of this. |
222 |
222 |
However, there is one exception: |
223 |
223 |
If both accounts have a collection named "Friends" (or the localized |
224 |
224 |
equivalent), and both feature each other in that collection, then |
225 |
225 |
this is shared between the two accounts. """ |
226 |
226 |
account = models.ForeignKey( |
227 |
227 |
"Account", |
228 |
228 |
null=False, |
229 |
229 |
editable=False, |
230 |
230 |
on_delete=models.CASCADE, |
231 |
231 |
help_text=_("The account that created this collection."), |
232 |
232 |
) |
233 |
233 |
name = models.CharField( |
234 |
234 |
max_length=32, |
235 |
235 |
blank=False, |
236 |
236 |
help_text=_("The name of this collection."), |
237 |
237 |
) |
238 |
238 |
accounts = models.ManyToManyField( |
239 |
239 |
"Account", |
240 |
240 |
help_text=_("All accounts that are part of this collection."), |
241 |
241 |
) |
242 |
242 |
visible_to_public = models.BooleanField( |
243 |
243 |
default=False, |
244 |
244 |
help_text=_("Make this collection visible to everybody."), |
245 |
245 |
) |
246 |
246 |
visible_to_collection = models.BooleanField( |
247 |
247 |
default=True, |
248 |
248 |
help_text=_("Make this collection visible to the accounts in this collection. Other collections are not affected by this."), |
249 |
249 |
) |
250 |
250 |
|
251 |
251 |
def __str__(self): |
252 |
252 |
return str(self.account) + " | " + self.name |
253 |
253 |
|
254 |
254 |
class Vote(models.Model): |
255 |
255 |
""" Accounts can vote on posts (using ▲, which is funny because UHasselt). |
256 |
256 |
These votes are registered in this table. """ |
257 |
257 |
voter = models.ForeignKey( |
258 |
258 |
"Account", |
259 |
259 |
null=False, |
260 |
260 |
editable=False, |
261 |
261 |
on_delete=models.CASCADE, |
262 |
262 |
) |
263 |
263 |
post = models.ForeignKey( |
264 |
264 |
"Post", |
265 |
265 |
null=False, # Duh. |
266 |
266 |
editable=False, # Transferring votes doesn't make sense |
267 |
267 |
on_delete=models.CASCADE, |
268 |
268 |
) |
269 |
269 |
|
270 |
270 |
class SharedFile(models.Model): |
271 |
271 |
""" Groups and people can share files with each other, through a chat system. |
272 |
272 |
These files are represented here. """ |
273 |
273 |
chat = models.ForeignKey( |
274 |
- | "Chat", |
275 |
- | on_delete=models.CASCADE, |
276 |
- | null=False, |
277 |
- | editable=False, |
278 |
- | help_text=_("The chat where this file is being shared in."), |
279 |
- | ) |
280 |
- | timestamp = models.DateTimeField(auto_now_add=True) |
281 |
274 |
file = models.FileField( |
282 |
275 |
upload_to="agora/chat/%Y/%m/%d/", |
283 |
276 |
null=False, |
284 |
277 |
editable=False, |
285 |
278 |
help_text=_("The file you want to share."), |
286 |
279 |
) |
287 |
280 |
uploader = models.ForeignKey( |
288 |
281 |
"Account", |
289 |
282 |
on_delete=models.CASCADE, |
290 |
283 |
null=False, |
291 |
284 |
editable=False, |
292 |
285 |
help_text=_("The account that uploaded this file."), |
293 |
286 |
) |
294 |
287 |
# TODO __str__ |
295 |
- | |
+ |
288 |
|
296 |
289 |
class Message(models.Model): |
297 |
290 |
""" Everyone can communicate with someone else using private messages. |
298 |
291 |
These messages are recorded here. """ |
299 |
292 |
chat = models.ForeignKey( |
300 |
- | "Chat", |
301 |
- | on_delete=models.CASCADE, |
302 |
- | null=False, |
303 |
- | editable=False, |
304 |
- | help_text=_("The chat where this message is being shared in."), |
305 |
- | ) |
306 |
- | timestamp = models.DateTimeField(auto_now_add=True) |
307 |
293 |
text = models.TextField( |
308 |
294 |
blank=False, |
309 |
295 |
) |
310 |
296 |
sender = models.ForeignKey( |
311 |
297 |
"Account", |
312 |
298 |
on_delete=models.CASCADE, |
313 |
299 |
null=False, |
314 |
300 |
editable=False, |
315 |
301 |
help_text=_("The account that sent this message."), |
316 |
302 |
) |
317 |
303 |
# TODO __str__ |
318 |
304 |
|
319 |
305 |
|
320 |
306 |
class Chat(models.Model): |
321 |
307 |
""" Chats can happen between a group, or between two people in private. |
322 |
308 |
These messages are connected to a particular chat. """ |
323 |
309 |
class Meta: |
+ |
310 |
"Message", |
+ |
311 |
help_text=_("All messages that were shared in this chat."), |
+ |
312 |
) |
+ |
313 |
shared_files = models.ManyToManyField( |
+ |
314 |
"SharedFile", |
+ |
315 |
help_text=_("The files that are shared in this chat."), |
+ |
316 |
) |
+ |
317 |
class Meta: |
324 |
318 |
abstract=True |
325 |
319 |
|
326 |
320 |
class GroupChat(Chat): |
327 |
321 |
pass |
328 |
- | class PrivateChat(Chat): |
+ |
322 |
group = models.ForeignKey( |
+ |
323 |
"Group", |
+ |
324 |
on_delete=models.CASCADE, |
+ |
325 |
null=False, |
+ |
326 |
editable=False, |
+ |
327 |
) |
+ |
328 |
|
+ |
329 |
def __str__(self): |
+ |
330 |
return str(self.group) |
+ |
331 |
|
+ |
332 |
class PrivateChat(Chat): |
329 |
333 |
pass |
330 |
- | |
+ |
334 |
# FIXME: It's theoretically possible to start a chat by one person, and have |
+ |
335 |
# the other person start the same as well. Find a reliable way to block that. |
+ |
336 |
account1 = models.ForeignKey( |
+ |
337 |
"Account", |
+ |
338 |
on_delete=models.CASCADE, |
+ |
339 |
null=False, |
+ |
340 |
editable=False, |
+ |
341 |
) |
+ |
342 |
account2 = models.ForeignKey( |
+ |
343 |
"Account", |
+ |
344 |
on_delete=models.CASCADE, |
+ |
345 |
null=False, |
+ |
346 |
editable=False, |
+ |
347 |
) |
+ |
348 |
def __str__(self): |
+ |
349 |
return str(self.account1) +" - "+ str(self.account2) |
+ |
350 |
|
331 |
351 |
class GroupInvite(models.Model): |
332 |
352 |
pass |
333 |
- | |
+ |
353 |
not limited to private groups. Group invitations are stored here, as well as their outcome. """ |
+ |
354 |
inviter = models.ForeignKey( |
+ |
355 |
"Account", |
+ |
356 |
on_delete=models.CASCADE, |
+ |
357 |
null=False, |
+ |
358 |
editable=False, |
+ |
359 |
) |
+ |
360 |
invitee = models.ForeignKey( |
+ |
361 |
"Account", |
+ |
362 |
on_delete=models.CASCADE, |
+ |
363 |
null=False, |
+ |
364 |
editable=False, |
+ |
365 |
help_text=_("The account which will receive the invitation."), |
+ |
366 |
) |
+ |
367 |
group = models.ForeignKey( |
+ |
368 |
"Group", |
+ |
369 |
on_delete=models.CASCADE, |
+ |
370 |
null=False, |
+ |
371 |
editable=False, |
+ |
372 |
db_index=True, |
+ |
373 |
help_text=_("The group for which this invitation is."), |
+ |
374 |
) |
+ |
375 |
accepted = models.NullBooleanField( |
+ |
376 |
default=None, |
+ |
377 |
help_text=_("Indicates if the invitation was accepted, rejected, or " |
+ |
378 |
"pending an answer. if somebody rejects the invitation, " |
+ |
379 |
"that group can no longer send an invitation to the invitee, " |
+ |
380 |
"unless (s)he removes the answer from her history. Also, " |
+ |
381 |
"a person can not reject an invitation, and accept it later. " |
+ |
382 |
"For that, a new invitation must be received."), |
+ |
383 |
) |
+ |
384 |
|
+ |
385 |
def __str__(self): |
+ |
386 |
return str(self.invitee) +" | "+ str(self.group) |
+ |
387 |
courses/models.py ¶
107 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 |
contact_person = models.ForeignKey( |
16 |
16 |
"joeni.user", |
17 |
17 |
on_delete=models.PROTECT, # A course must have a contact person |
18 |
18 |
limit_choices_to={'is_staff': True}, |
19 |
19 |
null=False, |
20 |
20 |
help_text=_("The person to contact regarding this course."), |
21 |
21 |
) |
22 |
22 |
coordinator = models.ForeignKey( |
23 |
23 |
"joeni.user", |
24 |
24 |
on_delete=models.PROTECT, # A course must have a coordinator |
25 |
25 |
limit_choices_to={'is_staff': True}, |
26 |
26 |
null=False, |
27 |
27 |
help_text=_("The person whom's the coordinator of this course."), |
28 |
28 |
) |
29 |
29 |
educating_team = models.ManyToManyField( |
30 |
30 |
"joeni.user", |
31 |
31 |
# No on_delete, since M->M cannot be required at database level |
32 |
32 |
limit_choices_to={'is_staff': True}, |
33 |
33 |
null=False, |
34 |
34 |
help_text=_("The team members of this course."), |
35 |
35 |
) |
36 |
36 |
language = models.CharField( |
37 |
37 |
max_length=64, |
38 |
38 |
choices = ( |
39 |
39 |
('NL', _("Dutch")), |
40 |
40 |
('EN', _("English")), |
41 |
41 |
('FR', _("French")), |
42 |
42 |
), |
43 |
43 |
null=False, |
44 |
44 |
help_text=_("The language in which this course is given."), |
45 |
45 |
) |
46 |
46 |
requirements = models.ManyToManyField() |
47 |
47 |
|
48 |
48 |
def __str__(self): |
49 |
49 |
number = str(self.number) |
50 |
50 |
for i in [10,100,1000]: |
51 |
51 |
if self.number < i: |
52 |
52 |
number = "0" + number |
53 |
53 |
return "(" + number + ") " + self.name |
54 |
54 |
|
55 |
55 |
|
56 |
56 |
class Prerequisites(models.Model): |
57 |
57 |
""" Represents a collection of prerequisites a student must have obtained |
58 |
58 |
before being allowed to partake in this course. |
59 |
59 |
It's possible that, if a student has obtained credits in a certain set of |
60 |
60 |
courses, a certain part of the prerequisites do not have to be obtained. |
61 |
61 |
Because of this, make a different record for each different set. In other |
62 |
62 |
words: If one set of prerequisites is obtained, and another one isn't, BUT |
63 |
63 |
they point to the same course, the student is allowed to partake. """ |
64 |
64 |
course = models.ForeignKey( |
65 |
65 |
"Course", |
66 |
66 |
on_delete=models.CASCADE, |
67 |
67 |
null=False, |
68 |
68 |
help_text=_("The course that these prerequisites are for."), |
69 |
69 |
) |
70 |
70 |
name = models.ForeignKey( |
71 |
71 |
blank=True, |
72 |
72 |
help_text=_("To specify a name for this set, if necessary."), |
73 |
73 |
) |
74 |
74 |
sequentialities = models.ManyToManyField( |
75 |
75 |
"Course", |
76 |
76 |
help_text=_("All courses for which a credit must've been received in order to follow the course."), |
77 |
77 |
) |
78 |
78 |
in_curriculum = models.ManyToManyField( |
79 |
79 |
"Course", |
80 |
80 |
help_text=_("All courses that have to be in the curriculum to follow this. If a credit was achieved, that course can be omitted."), |
81 |
81 |
) |
82 |
82 |
required_study = models.ForeignKey( |
83 |
83 |
"Study", |
84 |
84 |
on_delete=models.CASCADE, |
85 |
85 |
null=True, |
86 |
86 |
help_text=_("If one must have a certain amount of obtained ECTS points for a particular course, state that course here."), |
87 |
87 |
) |
88 |
88 |
ECTS_for_required_study = models.PositiveSmallIntegerField( |
89 |
89 |
null=True, |
90 |
90 |
help_text=_("The amount of obtained ECTS points for the required course, if any."), |
91 |
91 |
) |
92 |
92 |
|
93 |
93 |
def __str__(self): |
94 |
94 |
if self.name == "": |
95 |
95 |
return _("Prerequisites for %(course)s") % {'course': str(self.course)} |
96 |
96 |
else: |
97 |
97 |
return self.name + " | " + str(self.course) |
98 |
98 |
|
99 |
99 |
|
100 |
100 |
class ProgrammeInformation(models.Model): |
101 |
101 |
""" It's possible that a course is taught in multiple degree programmes; For |
102 |
102 |
example: Calculus can easily be taught to physics and mathematics students |
103 |
103 |
alike. In this table, these relations are set up, and the related properties |
104 |
104 |
are defined as well. """ |
105 |
105 |
study = models.ForeignKey( |
106 |
106 |
"Study", |
107 |
107 |
on_delete=models.CASCADE, |
108 |
108 |
null=False, |
109 |
109 |
help_text=_("The study in which the course is taught."), |
110 |
110 |
) |
111 |
111 |
course = models.ForeignKey( |
112 |
112 |
"Course", |
113 |
113 |
on_delete=models.CASCADE, |
114 |
114 |
null=False, |
115 |
115 |
help_text=_("The course that this information is for."), |
116 |
116 |
) |
117 |
117 |
study_programme = models.ForeignKey( |
118 |
118 |
"StudyProgramme", |
119 |
119 |
on_delete=models.CASCADE, |
120 |
120 |
null=False, |
121 |
121 |
help_text=_("The study programme that this course belongs to."), |
122 |
122 |
) |
123 |
123 |
programme_type = models.CharField( |
124 |
124 |
max_length=1, |
125 |
125 |
blank=False, |
126 |
126 |
choices = ( |
127 |
127 |
('C', _("Compulsory")), |
128 |
128 |
('O', _("Optional")), |
129 |
129 |
), |
130 |
130 |
help_text=_("Type of this course for this study."), |
131 |
131 |
) |
132 |
132 |
study_hours = models.PositiveSmallIntegerField( |
133 |
133 |
blank=False, |
134 |
134 |
help_text=_("The required amount of hours to study this course."), |
135 |
135 |
) |
136 |
136 |
ECTS = models.PositiveSmallIntegerField( |
137 |
137 |
blank=False, |
138 |
138 |
help_text=_("The amount of ECTS points attached to this course."), |
139 |
139 |
) |
140 |
140 |
semester = models.PositiveSmallIntegerField( |
141 |
141 |
blank=False, |
142 |
142 |
choices = ( |
143 |
143 |
(1, _("First semester")), |
144 |
144 |
(2, _("Second semester")), |
145 |
145 |
(3, _("Full year course")), |
146 |
146 |
(4, _("Taught in first quarter")), |
147 |
147 |
(5, _("Taught in second quarter")), |
148 |
148 |
(6, _("Taught in third quarter")), |
149 |
149 |
(7, _("Taught in fourth quarter")), |
150 |
150 |
), |
151 |
151 |
help_text=_("The period in which this course is being taught in this study."), |
152 |
152 |
) |
153 |
153 |
year = models.PositiveSmallIntegerField( |
154 |
154 |
blank=False, |
155 |
155 |
help_text=_("The year in which this course is taught for this study."), |
156 |
156 |
) |
157 |
157 |
second_chance = models.BooleanField( |
158 |
158 |
default=True, |
159 |
159 |
help_text=_("Defines if a second chance exam is planned for this course."), |
160 |
160 |
) |
161 |
161 |
tolerable = models.BooleanField( |
162 |
162 |
default=True, |
163 |
163 |
help_text=_("Defines if a failed result can be tolerated."), |
164 |
164 |
) |
165 |
165 |
scoring = models.CharField( |
166 |
166 |
max_length=2, |
167 |
167 |
choices = ( |
168 |
168 |
('N', _("Numerical")), |
169 |
169 |
('FP', _("Fail/Pass")), |
170 |
170 |
), |
171 |
171 |
default='N', |
172 |
172 |
blank=False, |
173 |
173 |
help_text=_("How the obtained score for this course is given."), |
174 |
174 |
) |
175 |
175 |
|
176 |
176 |
def __str__(self): |
177 |
177 |
return str(self.study) + " - " + str(self.course) |
178 |
178 |
|
179 |
179 |
class Study(models.Model): |
180 |
180 |
""" Defines a certain study that can be followed at the university. |
181 |
181 |
This also includes abridged study programmes, like transition programmes. |
182 |
182 |
Other information, such as descriptions, are kept in the template file |
183 |
183 |
of this study, which can be manually edited. Joeni searches for a file |
184 |
184 |
with the exact name as the study + ".html". So if the study is called |
185 |
185 |
"Bachelor of Informatics", it will search for "Bachelor of Informatics.html". |
186 |
186 |
""" |
187 |
187 |
# Degree types |
188 |
188 |
BSc = _("Bachelor of Science") |
189 |
189 |
Msc = _("Master of Science") |
190 |
190 |
LLB = _("Bachelor of Laws") |
191 |
191 |
LLM = _("Master of Laws") |
192 |
192 |
ir = _("Engineer") |
193 |
193 |
ing = _("Technological Engineer") |
194 |
194 |
# Faculties |
195 |
195 |
FoMaLS = _("Faculty of Medicine and Life Sciences") |
196 |
196 |
Fos = _("Faculty of Sciences") |
197 |
197 |
FoTS = _("Faculty of Transportation Sciences") |
198 |
198 |
FoAaA = _("Faculty of Architecture and Arts") |
199 |
199 |
FoBE = _("Faculty of Business Economics") |
200 |
200 |
FoET = _("Faculty of Engineering Technology") |
201 |
201 |
FoL = _("Faculty of Law") |
202 |
202 |
|
203 |
203 |
name = models.CharField( |
204 |
204 |
max_length=128, |
205 |
205 |
blank=False, |
206 |
206 |
unique=True, |
207 |
207 |
help_text=_("The full name of this study, in the language it's taught in."), |
208 |
208 |
) |
209 |
209 |
degree_type = models.CharField( |
210 |
210 |
max_length=64, |
211 |
211 |
choices = ( |
212 |
212 |
('BSc', Bsc), |
213 |
213 |
('MSc', Msc), |
214 |
214 |
('LL.B', LLB), |
215 |
215 |
('LL.M', LLM), |
216 |
216 |
('ir.', ir ), |
217 |
217 |
('ing.',ing), |
218 |
218 |
), |
219 |
219 |
blank=False, |
220 |
220 |
help_text=_("The type of degree one obtains upon passing this study."), |
221 |
221 |
) |
222 |
222 |
language = models.CharField( |
223 |
223 |
max_length=64, |
224 |
224 |
choices = ( |
225 |
225 |
('NL', _("Dutch")), |
226 |
226 |
('EN', _("English")), |
227 |
227 |
('FR', _("French")), |
228 |
228 |
), |
229 |
229 |
null=False, |
230 |
230 |
help_text=_("The language in which this study is given."), |
231 |
231 |
) |
232 |
232 |
# Information about exam committee |
233 |
233 |
chairman = models.ForeignKey( |
234 |
234 |
"Joeni.users", |
235 |
235 |
on_delete=models.PROTECT, |
236 |
236 |
null=False, |
237 |
237 |
limit_choices_to={'is_staff': True}, |
238 |
238 |
help_text=_("The chairman of this study."), |
239 |
239 |
) |
240 |
240 |
vice_chairman = models.ForeignKey( |
241 |
241 |
"Joeni.users", |
242 |
242 |
on_delete=models.PROTECT, |
243 |
243 |
null=False, |
244 |
244 |
help_text=_("The vice-chairman of this study."), |
245 |
245 |
limit_choices_to={'is_staff': True}, |
246 |
246 |
) |
247 |
247 |
secretary = models.ForeignKey( |
248 |
248 |
"Joeni.users", |
249 |
249 |
on_delete=models.PROTECT, |
250 |
250 |
null=False, |
251 |
251 |
help_text=_("The secretary of this study."), |
252 |
252 |
limit_choices_to={'is_staff': True}, |
253 |
253 |
) |
254 |
254 |
ombuds = models.ForeignKey( |
255 |
255 |
"Joeni.users", |
256 |
256 |
on_delete=models.PROTECT, |
257 |
257 |
null=False, |
258 |
258 |
help_text=_("The ombuds person of this study."), |
259 |
259 |
limit_choices_to={'is_staff': True}, |
260 |
260 |
) |
261 |
261 |
vice_ombuds = models.ForeignKey( |
262 |
262 |
"Joeni.users", |
263 |
263 |
on_delete=models.PROTECT, |
264 |
264 |
null=False, |
265 |
265 |
help_text=_("The (replacing) ombuds person of this study."), |
266 |
266 |
limit_choices_to={'is_staff': True}, |
267 |
267 |
) |
268 |
268 |
additional_members = models.ManyToManyField( |
269 |
269 |
"Joeni.users", |
270 |
270 |
help_text=_("All the other members of the exam committee."), |
271 |
271 |
limit_choices_to={'is_staff': True}, |
272 |
272 |
) |
273 |
273 |
faculty = models.CharField( |
274 |
274 |
max_length=6, |
275 |
275 |
choices = ( |
276 |
276 |
('FoS', FoS), |
277 |
277 |
('FoTS', FoTS), |
278 |
278 |
('FoAaA', FoAaA), |
279 |
279 |
('FoBE', FoBE), |
280 |
280 |
('FoMaLS', FoMaLS), |
281 |
281 |
('FoET', FoET), |
282 |
282 |
('FoL', FoL), |
283 |
283 |
), |
284 |
284 |
blank=False, |
285 |
285 |
help_text=_("The faculty where this study belongs to."), |
286 |
286 |
) |
287 |
287 |
|
288 |
288 |
#def study_points(self): |
289 |
289 |
""" Returns the amount of study points for this year. |
290 |
290 |
This value is inferred based on the study programme information |
291 |
291 |
records that lists this study as their foreign key. """ |
292 |
292 |
#total_ECTS = 0 |
293 |
293 |
#for course in ProgrammeInformation.objects.filter(study=self): |
294 |
294 |
#total_ECTS += course.ECTS |
295 |
295 |
#return total_ECTS |
296 |
296 |
# XXX: Commented because this is actually something for the StudyProgramme |
297 |
297 |
def years(self): |
298 |
298 |
""" Returns the amount of years this study takes. |
299 |
299 |
This value is inferred based on the study programme information |
300 |
300 |
records that lists this study as their foreign key. """ |
301 |
301 |
highest_year = 0 |
302 |
302 |
for course in ProgrammeInformation.objects.filter(study=self): |
303 |
303 |
highest_year = max(highest_year, course.year) |
304 |
304 |
return highest_year |
305 |
305 |
|
306 |
306 |
def __str__(self): |
307 |
307 |
return self.name |
308 |
308 |
|
309 |
309 |
class StudyProgramme(models.Model): |
310 |
310 |
""" Represents a programme within a certain study. |
311 |
311 |
A good example for this is the different specializations, minors, majors, ... |
312 |
312 |
one can follow within the same study. Nevertheless, they're all made of |
313 |
313 |
a certain set of courses. This table collects all these, and allows one to name |
314 |
314 |
them, so they're distinct from one another. """ |
315 |
315 |
def name = models.CharField( |
316 |
316 |
max_length=64, |
317 |
317 |
blank=False, |
318 |
318 |
help_text=_("The name of this programme."), |
319 |
319 |
) |
320 |
320 |
|
321 |
321 |
def courses(self): |
322 |
322 |
""" All courses that are part of this study programme. """ |
323 |
323 |
programmes = ProgrammeInformation.objects.filter(study_programme=self) |
324 |
324 |
courses = {} |
325 |
325 |
for program in programmes: |
326 |
326 |
courses.add(program.course) |
327 |
327 |
return courses |
328 |
328 |
|
329 |
329 |
def study_points(self, year=None): |
330 |
330 |
""" Returns the amount of study points this programme contains. |
331 |
331 |
Accepts year as an optional argument. If not given, the study points |
332 |
332 |
of all years are returned. """ |
333 |
333 |
programmes = ProgrammeInformation.objects.filter(study_programme=self) |
334 |
334 |
ECTS = 0 |
335 |
335 |
for program in programmes: |
336 |
336 |
if year is None or program.year == year: |
337 |
337 |
# XXX: This only works if the used implementation does lazy |
338 |
338 |
# evaluation, otherwise this is a type error! |
339 |
339 |
ECTS += program.ECTS |
340 |
340 |
return ECTS |
341 |
341 |
|
342 |
342 |
def __str__(self): |
343 |
343 |
return self.name |
344 |
344 |
|
345 |
345 |
# Tables about things related to the courses: |
346 |
346 |
|
347 |
347 |
class HomeworkTask(models.Model): |
348 |
- | pass |
349 |
- | class Announcement(models.Model): |
+ |
348 |
""" For courses, it's possible to set up tasks. These tasks are recorded |
+ |
349 |
here. """ |
+ |
350 |
# TODO: Require that only the course team can create assignments for a team. |
+ |
351 |
course = models.ForeignKey( |
+ |
352 |
"Course", |
+ |
353 |
on_delete=models.CASCADE, |
+ |
354 |
null=False, |
+ |
355 |
editable=False, |
+ |
356 |
db_index=True, |
+ |
357 |
help_text=_("The course for which this task is assigned."), |
+ |
358 |
) |
+ |
359 |
information = models.TextField( |
+ |
360 |
help_text=_("Any additional information regarding the assignment. Orgmode syntax available."), |
+ |
361 |
) |
+ |
362 |
deadline = models.DateTimeField( |
+ |
363 |
null=False, |
+ |
364 |
help_text=_("The date and time this task is due."), |
+ |
365 |
) |
+ |
366 |
posted = models.DateField(auto_now_add=True) |
+ |
367 |
digital_task = models.BooleanField( |
+ |
368 |
default=True, |
+ |
369 |
help_text=_("This determines whether this assignment requires handing " |
+ |
370 |
"in a digital file."), |
+ |
371 |
) |
+ |
372 |
|
+ |
373 |
def __str__(self): |
+ |
374 |
return str(self.course) +" | "+ str(self.posted) |
+ |
375 |
|
+ |
376 |
class Announcement(models.Model): |
350 |
377 |
pass |
351 |
- | class Upload(models.Model): |
+ |
378 |
course = models.ForeignKey( |
+ |
379 |
"Course", |
+ |
380 |
on_delete=models.CASCADE, |
+ |
381 |
null=False, |
+ |
382 |
editable=False, |
+ |
383 |
db_index=True, |
+ |
384 |
help_text=_("The course for which this announcement is made."), |
+ |
385 |
) |
+ |
386 |
title = models.CharField( |
+ |
387 |
max_length=20, # Keep It Short & Simple® |
+ |
388 |
help_text=_("A quick title for what this is about."), |
+ |
389 |
) |
+ |
390 |
text = models.TextField( |
+ |
391 |
blank=False, |
+ |
392 |
help_text=_("The announcement itself. Orgmode syntax available."), |
+ |
393 |
) |
+ |
394 |
posted = models.DateTimeField(auto_now_add=True) |
+ |
395 |
|
+ |
396 |
def __str__(self): |
+ |
397 |
return str(self.course) +" | "+ self.posted.strftime("%m/%d") |
+ |
398 |
|
+ |
399 |
class Upload(models.Model): |
352 |
400 |
pass |
353 |
- | class StudyGroup(models.Model): |
+ |
401 |
ins are recorded per student in this table. """ |
+ |
402 |
assignment = models.ForeignKey( |
+ |
403 |
"Assignment", |
+ |
404 |
on_delete=models.CASCADE, |
+ |
405 |
null=False, |
+ |
406 |
editable=False, |
+ |
407 |
db_index=True, |
+ |
408 |
limit_choices_to={"digital_task": True}, |
+ |
409 |
help_text=_("For which assignment this upload is."), |
+ |
410 |
) |
+ |
411 |
# TODO: Try to find a way to require that, if the upload is made, |
+ |
412 |
# only students that have this course in their curriculum can upload. |
+ |
413 |
student = models.ForeignKey( |
+ |
414 |
"joeni.User", |
+ |
415 |
on_delete=models.CASCADE, |
+ |
416 |
null=False, |
+ |
417 |
editable=False, |
+ |
418 |
limit_choices_to={"is_student": True}, |
+ |
419 |
help_text=_("The student who handed this in."), |
+ |
420 |
) |
+ |
421 |
upload_time = models.DateTimeField(auto_now_add=True) |
+ |
422 |
comment = models.TextField( |
+ |
423 |
blank=True, |
+ |
424 |
help_text=_("If you wish to add an additional comment, state it here."), |
+ |
425 |
) |
+ |
426 |
|
+ |
427 |
def __str__(self): |
+ |
428 |
deadline = self.assignment.deadline |
+ |
429 |
if deadline < self.upload_time |
+ |
430 |
return str(self.assignment.course) +" | "+ str(self.student.number) + _("(OVERDUE)") |
+ |
431 |
else: |
+ |
432 |
return str(self.assignment.course) +" | "+ str(self.student.number) |
+ |
433 |
|
+ |
434 |
class StudyGroup(models.Model): |
354 |
435 |
# Should foreignkey with Agora groups and stuff |
355 |
- | pass |
356 |
- | |
+ |
436 |
are recorded here, and blend in seamlessly with the Groups from Agora. |
+ |
437 |
Groups that are recorded as a StudyGroup, are given official course status, |
+ |
438 |
and thus, cannot be removed until the status of StudyGroup is lifted. """ |
+ |
439 |
course = models.ForeignKey( |
+ |
440 |
"Course", |
+ |
441 |
on_delete=models.CASCADE, |
+ |
442 |
null=False, |
+ |
443 |
editable=False, |
+ |
444 |
db_index=True, |
+ |
445 |
help_text=_("The course for which this group is."), |
+ |
446 |
) |
+ |
447 |
group = models.ForeignKey( |
+ |
448 |
"agora.Group", |
+ |
449 |
on_delete=models.PROTECT, # See class documentation |
+ |
450 |
null=False, |
+ |
451 |
editable=False, # Keep the same group |
+ |
452 |
help_text=_("The group that will be seen as the study group."), |
+ |
453 |
) |
+ |
454 |
|
+ |
455 |
def __str__(self): |
+ |
456 |
return str(self.course) +" | "+ str(self.group) |
+ |
457 |