Add URL patters and new tables
The new URL patters are ready for translation. The tables are not finished, but I will do that tomorrow.
- Author
- Maarten 'Vngngdn' Vangeneugden
- Date
- Nov. 18, 2017, 12:02 a.m.
- Hash
- b2c65ea82b9bc3245a2713b3105ef0deb132edb2
- Parent
- ca91ec6e5f2ff3a7e709170fe42e1fe882ea819d
- Modified files
- administration/models.py
- agora/models.py
- agora/urls.py
- courses/models.py
- joeni/templates/joeni/index.html
administration/models.py ¶
4 additions and 0 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 |
|
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 |
# Planning and organization related tables |
+ |
231 |
class RoomReservation(models.Model): |
+ |
232 |
pass |
+ |
233 |
agora/models.py ¶
156 additions and 12 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 |
) |
+ |
31 |
) |
31 |
32 |
account_page_banner = models.ImageField( # Requires the Pillow library! |
32 |
33 |
upload_to=account_user_directory, |
33 |
34 |
help_text=_("The banner image to be shown on this account's homepage."), |
34 |
35 |
) |
35 |
36 |
avatar = models.ImageField( |
36 |
37 |
upload_to=account_user_directory, |
37 |
38 |
help_text=_("The avatar image of this account."), |
38 |
39 |
) |
39 |
40 |
|
40 |
41 |
def __str__(self): |
41 |
42 |
return str(self.account) |
42 |
43 |
|
43 |
44 |
class Post(models.Model): |
44 |
45 |
""" An abstract model that represents a post. """ |
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 |
|
+ |
82 |
default=True, |
+ |
83 |
help_text=_("Decide if other people can respond to this post or not. " |
+ |
84 |
"This does not influence what people allow on their posts."), |
+ |
85 |
) |
+ |
86 |
|
82 |
87 |
def __str__(self): |
83 |
88 |
return str(self.timestamp) + " | " + str(self.author) |
84 |
89 |
|
+ |
90 |
# then be used with OpenStreetMap or something |
+ |
91 |
|
85 |
92 |
class VideoPost(Post): |
86 |
- | pass |
87 |
- | |
88 |
- | class ImagePost(Post): |
89 |
- | pass |
90 |
- | |
91 |
- | class MusicPost(Post): |
92 |
- | pass |
93 |
- | |
94 |
- | class DocumentPost(Post): |
95 |
- | pass |
96 |
- | |
+ |
93 |
""" A special type of Post, which has a file linked with it. |
+ |
94 |
The poster can specify how to treat this file. """ |
+ |
95 |
image = _("Image") |
+ |
96 |
video = _("Video") |
+ |
97 |
music = _("Sound") |
+ |
98 |
text = _("Text" ) |
+ |
99 |
other = _("Other") |
+ |
100 |
file = models.FileField( |
+ |
101 |
upload_to="agora/posts/%Y/%m/%d/", |
+ |
102 |
null=False, |
+ |
103 |
editable=False, |
+ |
104 |
help_text=_("The file you wish to share."), |
+ |
105 |
) |
+ |
106 |
file_type = models.CharField( |
+ |
107 |
max_length=16, |
+ |
108 |
blank=False, |
+ |
109 |
choices = ( |
+ |
110 |
('image', image), |
+ |
111 |
('video', video), |
+ |
112 |
('music', music), |
+ |
113 |
('text' , text ), |
+ |
114 |
('other', other), |
+ |
115 |
), |
+ |
116 |
help_text=_("How this file should be seen as."), |
+ |
117 |
) |
+ |
118 |
|
97 |
119 |
class Page(models.Model): |
98 |
120 |
""" In the university, people can create pages for everything they want and |
99 |
121 |
then some. These pages are put in the database through this table. """ |
100 |
122 |
name = models.CharField( |
101 |
123 |
max_length=64, |
102 |
124 |
primary_key=True, |
103 |
125 |
blank=False, |
104 |
126 |
help_text=_("The name of this page."), |
105 |
127 |
) |
106 |
128 |
created = models.DateTimeField(auto_now_add=True) |
107 |
129 |
hidden = models.BooleanField( |
108 |
130 |
default=False, |
109 |
131 |
help_text=_("Determines if this page can be found without a direct link."), |
110 |
132 |
) |
111 |
133 |
|
+ |
134 |
blank=True, |
+ |
135 |
help_text=_("If you want to put some text on this page, " |
+ |
136 |
"you can put it here. You can use Orgmode-syntax to " |
+ |
137 |
"get as much out of your page as possible. While doing so, " |
+ |
138 |
"be aware of the limitations imposed by the code of conduct."), |
+ |
139 |
) |
+ |
140 |
public_posting = models.BooleanField( |
+ |
141 |
default=True, |
+ |
142 |
help_text=_("Determines if everyone can post on this page, or only the " |
+ |
143 |
"people that are linked with it. Know that if a post is made " |
+ |
144 |
"and responding is allowed, everyone can respond to that post."), |
+ |
145 |
) |
+ |
146 |
|
112 |
147 |
class Meta: |
113 |
148 |
abstract=True |
114 |
149 |
|
115 |
150 |
class AccountPage(Page): |
116 |
151 |
account = models.OneToOneField( |
+ |
152 |
This page can only be edited by the account holder, or staff members. """ |
+ |
153 |
# TODO: Find a way to auto-create one of these every time a new account is created |
+ |
154 |
# TODO: Require that changes can only occur by either the account holder or staff |
+ |
155 |
account = models.OneToOneField( |
117 |
156 |
"Account", |
118 |
157 |
null=False, |
119 |
158 |
on_delete=models.CASCADE, |
120 |
159 |
) |
121 |
160 |
|
122 |
161 |
class GroupPage(Page): |
123 |
162 |
group = models.ForeignKey( |
+ |
163 |
This page can only be edited by group members or staff members. """ |
+ |
164 |
# TODO: Find a way to auto-create one of these every time a new group is created |
+ |
165 |
# TODO: Require that changes can only occur by either the group or staff |
+ |
166 |
group = models.ForeignKey( |
124 |
167 |
"Group", |
125 |
168 |
null=False, |
126 |
169 |
on_delete=models.CASCADE, |
127 |
170 |
) |
128 |
171 |
|
129 |
172 |
class CoursePage(Page): |
130 |
173 |
course = models.OneToOneField( |
+ |
174 |
This page can only be edited by the course's educating team or staff members. """ |
+ |
175 |
# TODO: Find a way to auto-create one of these every time a new course is created |
+ |
176 |
# TODO: Require that changes can only occur by either the course team or staff |
+ |
177 |
course = models.OneToOneField( |
131 |
178 |
"courses.Course", |
132 |
179 |
null=False, |
133 |
180 |
on_delete=models.CASCADE, |
134 |
181 |
) |
135 |
182 |
|
136 |
183 |
|
+ |
184 |
""" It is imperative that everyone can come together with other people. |
+ |
185 |
A Group record is the way to accomplish this. """ |
+ |
186 |
name = models.CharField( |
+ |
187 |
max_length=64, |
+ |
188 |
primary_key=True, # So be unique I'd say |
+ |
189 |
blank=False, |
+ |
190 |
help_text=_("The name of your group."), |
+ |
191 |
) |
+ |
192 |
color = models.CharField( |
+ |
193 |
max_length=6, |
+ |
194 |
help_text=_("The hexadecimal code of the color for this group."), |
+ |
195 |
default = constants.COLORS["UHasselt default"], |
+ |
196 |
blank=False, |
+ |
197 |
validators=[validate_hex_color], |
+ |
198 |
) |
+ |
199 |
members = models.ManyToManyField( |
+ |
200 |
"Account", |
+ |
201 |
help_text=_("The members of this group."), |
+ |
202 |
) |
+ |
203 |
invite_only = models.BooleanField( |
+ |
204 |
default=True, |
+ |
205 |
help_text=_("Determines if everyone can join this group, or if " |
+ |
206 |
"only members can invite others."), |
+ |
207 |
) |
+ |
208 |
private = models.BooleanField( |
+ |
209 |
default=True, |
+ |
210 |
help_text=_("Determines if this group is visible to non-members."), |
+ |
211 |
) |
+ |
212 |
|
+ |
213 |
def __str__(self): |
+ |
214 |
return self.name |
+ |
215 |
|
+ |
216 |
|
137 |
217 |
class AccountCollection(models.Model): |
138 |
218 |
""" Every account can make a collection in which (s)he can list accounts |
139 |
219 |
at his/her wish. This can be a collection of Friends, study collegues, |
140 |
220 |
project partners, and so on. |
141 |
221 |
Accounts that are in a certain collection are not notified of this. |
142 |
222 |
However, there is one exception: |
143 |
223 |
If both accounts have a collection named "Friends" (or the localized |
144 |
224 |
equivalent), and both feature each other in that collection, then |
145 |
225 |
this is shared between the two accounts. """ |
146 |
226 |
account = models.ForeignKey( |
147 |
227 |
"Account", |
148 |
228 |
null=False, |
149 |
229 |
editable=False, |
150 |
230 |
on_delete=models.CASCADE, |
151 |
231 |
help_text=_("The account that created this collection."), |
152 |
232 |
) |
153 |
233 |
name = models.CharField( |
154 |
234 |
max_length=32, |
155 |
235 |
blank=False, |
156 |
236 |
help_text=_("The name of this collection."), |
157 |
237 |
) |
158 |
238 |
accounts = models.ManyToManyField( |
159 |
239 |
"Account", |
160 |
240 |
help_text=_("All accounts that are part of this collection."), |
161 |
241 |
) |
162 |
242 |
visible_to_public = models.BooleanField( |
163 |
243 |
default=False, |
164 |
244 |
help_text=_("Make this collection visible to everybody."), |
165 |
245 |
) |
166 |
246 |
visible_to_collection = models.BooleanField( |
167 |
247 |
default=True, |
168 |
248 |
help_text=_("Make this collection visible to the accounts in this collection. Other collections are not affected by this."), |
169 |
249 |
) |
170 |
250 |
|
171 |
251 |
def __str__(self): |
172 |
252 |
return str(self.account) + " | " + self.name |
173 |
253 |
|
174 |
254 |
class Vote(models.Model): |
175 |
255 |
""" Accounts can vote on posts (using ▲, which is funny because UHasselt). |
176 |
256 |
These votes are registered in this table. """ |
177 |
257 |
voter = models.ForeignKey( |
178 |
258 |
"Account", |
179 |
259 |
null=False, |
180 |
260 |
editable=False, |
181 |
261 |
on_delete=models.CASCADE, |
182 |
262 |
) |
183 |
263 |
post = models.ForeignKey( |
184 |
264 |
"Post", |
185 |
265 |
null=False, # Duh. |
186 |
266 |
editable=False, # Transferring votes doesn't make sense |
187 |
267 |
on_delete=models.CASCADE, |
188 |
268 |
) |
189 |
269 |
|
+ |
270 |
class SharedFile(models.Model): |
+ |
271 |
""" Groups and people can share files with each other, through a chat system. |
+ |
272 |
These files are represented here. """ |
+ |
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 |
file = models.FileField( |
+ |
282 |
upload_to="agora/chat/%Y/%m/%d/", |
+ |
283 |
null=False, |
+ |
284 |
editable=False, |
+ |
285 |
help_text=_("The file you want to share."), |
+ |
286 |
) |
+ |
287 |
uploader = models.ForeignKey( |
+ |
288 |
"Account", |
+ |
289 |
on_delete=models.CASCADE, |
+ |
290 |
null=False, |
+ |
291 |
editable=False, |
+ |
292 |
help_text=_("The account that uploaded this file."), |
+ |
293 |
) |
+ |
294 |
# TODO __str__ |
+ |
295 |
|
+ |
296 |
class Message(models.Model): |
+ |
297 |
""" Everyone can communicate with someone else using private messages. |
+ |
298 |
These messages are recorded here. """ |
+ |
299 |
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 |
text = models.TextField( |
+ |
308 |
blank=False, |
+ |
309 |
) |
+ |
310 |
sender = models.ForeignKey( |
+ |
311 |
"Account", |
+ |
312 |
on_delete=models.CASCADE, |
+ |
313 |
null=False, |
+ |
314 |
editable=False, |
+ |
315 |
help_text=_("The account that sent this message."), |
+ |
316 |
) |
+ |
317 |
# TODO __str__ |
+ |
318 |
|
+ |
319 |
|
+ |
320 |
class Chat(models.Model): |
+ |
321 |
""" Chats can happen between a group, or between two people in private. |
+ |
322 |
These messages are connected to a particular chat. """ |
+ |
323 |
class Meta: |
+ |
324 |
abstract=True |
+ |
325 |
|
+ |
326 |
class GroupChat(Chat): |
+ |
327 |
pass |
+ |
328 |
class PrivateChat(Chat): |
+ |
329 |
pass |
+ |
330 |
|
+ |
331 |
class GroupInvite(models.Model): |
+ |
332 |
pass |
+ |
333 |
agora/urls.py ¶
13 additions and 0 deletions.
View changes Hide changes
+ |
1 |
from . import views |
+ |
2 |
from django.utils.translation import ugettext_lazy as _ |
+ |
3 |
|
+ |
4 |
urlpatterns = [] + i18n_patterns( |
+ |
5 |
path(_('main'), views.main, name='agora-main'), |
+ |
6 |
path(_('group/<str:group>'), views.group, name='agora-group'), |
+ |
7 |
path(_('page/<str:page>'), views.page, name='agora-page'), |
+ |
8 |
path(_('settings/account'), views.account_settings, name='agora-account-settings'), |
+ |
9 |
path(_('settings/page/<str:page>'), views.page_settings, name='agora-page-settings'), |
+ |
10 |
path(_('settings/group/<str:group>'), views.group_settings, name='agora-group-settings'), |
+ |
11 |
#path(_('settings'), views.settings, name='agora-settings'), # Uncomment if all the other pages are done. This page links to all the other settings pages available to this account. |
+ |
12 |
) |
+ |
13 |
courses/models.py ¶
10 additions and 0 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 |
class HomeworkTask(models.Model): |
+ |
348 |
pass |
+ |
349 |
class Announcement(models.Model): |
+ |
350 |
pass |
+ |
351 |
class Upload(models.Model): |
+ |
352 |
pass |
+ |
353 |
class StudyGroup(models.Model): |
+ |
354 |
# Should foreignkey with Agora groups and stuff |
+ |
355 |
pass |
+ |
356 |
joeni/templates/joeni/index.html ¶
1 addition and 2 deletions.
View changes Hide changes
1 |
1 |
{% load i18n %} |
2 |
2 |
{% get_current_language as LANGUAGE_CODE %} |
3 |
3 |
{% get_language_info for LANGUAGE_CODE as lang %} |
4 |
4 |
{% load static %} |
5 |
5 |
|
6 |
6 |
<!DOCTYPE html> |
7 |
7 |
<html lang="{{ lang.code }}"> |
8 |
8 |
<head> |
9 |
9 |
<title>{% block title %}{% trans "Joeni | Test" %}{% endblock title %}</title> |
10 |
10 |
|
11 |
11 |
{% block stylesheets %} |
12 |
12 |
<link href="{% static 'stylesheets/main.css' %}" rel="stylesheet" media="screen,projection" /> |
13 |
13 |
{% endblock stylesheets %} |
14 |
14 |
|
15 |
15 |
{% block metaflags %} |
16 |
16 |
{# This is standard for all web pages and doesn't require changing. #} |
17 |
17 |
<meta charset="utf-8" /> |
18 |
18 |
<meta name="author" content="Maarten Vangeneugden"> |
19 |
19 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!--Because this page is suited for mobile devices.--> |
20 |
20 |
{% endblock metaflags %} |
21 |
21 |
|
22 |
22 |
<meta name="description" content="{% block description %}{% trans "The web informatics system for Hasselt University." %}{% endblock description %}" /> |
23 |
23 |
</head> |
24 |
24 |
<body> |
25 |
25 |
{% block header %} |
26 |
26 |
<header> |
27 |
- | </header> |
28 |
- | {% endblock header %} |
+ |
27 |
{% endblock header %} |
29 |
28 |
|
30 |
29 |
{% block main %} |
31 |
30 |
<main> |
32 |
31 |
</main> |
33 |
32 |
{% endblock main %} |
34 |
33 |
{% block footer %} |
35 |
34 |
{% include "joeni/footer.html" %} |
36 |
35 |
{% endblock footer %} |
37 |
36 |
</body> |
38 |
37 |
</html> |
39 |
38 |