joeni

Add new tables for the database

Author
Maarten 'Vngngdn' Vangeneugden
Date
Nov. 15, 2017, 6:21 p.m.
Hash
35d48d7399ca4e87c9820a7d256c50b2ede8d4e4
Parent
c3f5bcfa37c056eeee3908d148ff037aec95e1fc
Modified files
courses/models.py
joeni/models.py

courses/models.py

298 additions and 0 deletions.

View changes Hide changes
1
1
2
2
# Create your models here.
3
3
+
4
class Course(models.Model):
+
5
    """ Represents a course that is taught at the university. """
+
6
    number = models.PositiveSmallIntegerField(
+
7
        primary_key=True,
+
8
        blank=False,
+
9
        help_text=_("The number associated with this course. A leading "0" will be added if the number is smaller than 1000."),
+
10
        )
+
11
    name = models.CharField(
+
12
        max_length=64,
+
13
        blank=False,
+
14
        help_text=_("The name of this course, in the language that it is taught. Translations are for the appropriate template."),
+
15
        )
+
16
    contact_person = models.ForeignKey(
+
17
        "joeni.user",
+
18
        on_delete=models.PROTECT,  # A course must have a contact person
+
19
        limit_choices_to={'is_staff': True},
+
20
        null=False,
+
21
        help_text=_("The person to contact regarding this course."),
+
22
        )
+
23
    coordinator = models.ForeignKey(
+
24
        "joeni.user",
+
25
        on_delete=models.PROTECT,  # A course must have a coordinator
+
26
        limit_choices_to={'is_staff': True},
+
27
        null=False,
+
28
        help_text=_("The person whom's the coordinator of this course."),
+
29
        )
+
30
    educating_team = models.ManyToManyField(
+
31
        "joeni.user",
+
32
        # No on_delete, since M->M cannot be required at database level
+
33
        limit_choices_to={'is_staff': True},
+
34
        null=False,
+
35
        help_text=_("The team members of this course."),
+
36
        )
+
37
    language = models.CharField(
+
38
        max_length=64,
+
39
        choices = (
+
40
            ('NL', _("Dutch")),
+
41
            ('EN', _("English")),
+
42
            ('FR', _("French")),
+
43
            )
+
44
        null=False,
+
45
        help_text=_("The language in which this course is given."),
+
46
        )
+
47
    requirements = models.ManyToManyField()
+
48
+
49
    def __str__(self):
+
50
        number = str(self.number)
+
51
        for i in [10,100,1000]:
+
52
        if self.number < i:
+
53
            number = "0" + number
+
54
        return "(" + number + ") " + self.name
+
55
+
56
+
57
class Prerequisites(models.Model):
+
58
    """ Represents a collection of prerequisites a student must have obtained
+
59
    before being allowed to partake in this course.
+
60
    It's possible that, if a student has obtained credits in a certain set of
+
61
    courses, a certain part of the prerequisites do not have to be obtained.
+
62
    Because of this, make a different record for each different set. In other
+
63
    words: If one set of prerequisites is obtained, and another one isn't, BUT
+
64
    they point to the same course, the student is allowed to partake. """
+
65
    course = models.ForeignKey(
+
66
        "Course",
+
67
        on_delete=models.CASCADE,
+
68
        null=False,
+
69
        help_text=_("The course that these prerequisites are for."),
+
70
        )
+
71
    name = models.ForeignKey(
+
72
        blank=True,
+
73
        help_text=_("To specify a name for this set, if necessary."),
+
74
        )
+
75
    sequentialities = models.ManyToManyField(
+
76
        "Course",
+
77
        help_text=_("All courses for which a credit must've been received in order to follow the course."),
+
78
        )
+
79
    in_curriculum = models.ManyToManyField(
+
80
        "Course",
+
81
        help_text=_("All courses that have to be in the curriculum to follow this. If a credit was achieved, that course can be omitted."),
+
82
        )
+
83
    required_study = models.ForeignKey(
+
84
        "Study",
+
85
        on_delete=models.CASCADE,
+
86
        null=True,
+
87
        help_text=_("If one must have a certain amount of obtained ECTS points for a particular course, state that course here."),
+
88
        )
+
89
    ECTS_for_required_study = models.PositiveSmallIntegerField(
+
90
        null=True,
+
91
        help_text=_("The amount of obtained ECTS points for the required course, if any."),
+
92
        )
+
93
+
94
    def __str__(self):
+
95
        if self.name == "":
+
96
            return _("Prerequisites for %(course)s") % {'course': str(self.course)}
+
97
        else:
+
98
            return self.name + " | " + str(self.course)
+
99
+
100
+
101
class ProgrammeInformation(models.Model):
+
102
    """ It's possible that a course is taught in multiple degree programmes; For
+
103
    example: Calculus can easily be taught to physics and mathematics students
+
104
    alike. In this table, these relations are set up, and the related properties
+
105
    are defined as well. """
+
106
    study = models.ForeignKey(
+
107
        "Study",
+
108
        on_delete=models.CASCADE,
+
109
        null=False,
+
110
        help_text=_("The study in which the course is taught."),
+
111
        )
+
112
    course = models.ForeignKey(
+
113
        "Course",
+
114
        on_delete=models.CASCADE,
+
115
        null=False,
+
116
        help_text=_("The course that this information is for."),
+
117
        )
+
118
    programme_type = models.CharField(
+
119
        max_length=1,
+
120
        blank=False,
+
121
        choices = (
+
122
            ('C', _("Compulsory")),
+
123
            ('O', _("Optional")),
+
124
            ),
+
125
        help_text=_("Type of this course for this study."),
+
126
        )
+
127
    study_hours = models.PositiveSmallIntegerField(
+
128
        blank=False,
+
129
        help_text=_("The required amount of hours to study this course."),
+
130
        )
+
131
    ECTS = models.PositiveSmallIntegerField(
+
132
        blank=False,
+
133
        help_text=_("The amount of ECTS points attached to this course."),
+
134
        )
+
135
    semester = models.PositiveSmallIntegerField(
+
136
        blank=False,
+
137
        choices = (
+
138
            (1, _("First semester")),
+
139
            (2, _("Second semester")),
+
140
            (3, _("Full year course")),
+
141
            (4, _("Taught in first quarter")),
+
142
            (5, _("Taught in second quarter")),
+
143
            (6, _("Taught in third quarter")),
+
144
            (7, _("Taught in fourth quarter")),
+
145
            )
+
146
        help_text=_("The period in which this course is being taught in this study."),
+
147
        )
+
148
    year = models.PositiveSmallIntegerField(
+
149
        blank=False,
+
150
        help_text=_("The year in which this course is taught for this study."),
+
151
        )
+
152
    second_chance = models.BooleanField(
+
153
        default=True,
+
154
        help_text=_("Defines if a second chance exam is planned for this course."),
+
155
        )
+
156
    tolerable = models.BooleanField(
+
157
        default=True,
+
158
        help_text=_("Defines if a failed result can be tolerated."),
+
159
        )
+
160
    scoring = models.CharField(
+
161
        max_length=2,
+
162
        choices = (
+
163
            ('N', _("Numerical")),
+
164
            ('FP', _("Fail/Pass")),
+
165
            ),
+
166
        default='N',
+
167
        blank=False,
+
168
        help_text=_("How the obtained score for this course is given."),
+
169
        )
+
170
+
171
    def __str__(self):
+
172
        return str(self.study) + " - " + str(self.course)
+
173
+
174
class Study(models.Model):
+
175
    """ Defines a certain study that can be followed at the university.
+
176
    This also includes abridged study programmes, like transition programmes.
+
177
    Other information, such as descriptions, are kept in the template file
+
178
    of this study, which can be manually edited. Joeni searches for a file
+
179
    with the exact name as the study + ".html". So if the study is called
+
180
    "Bachelor of Informatics", it will search for "Bachelor of Informatics.html".
+
181
    """
+
182
    # Degree types
+
183
    BSc = _("Bachelor of Science")
+
184
    Msc = _("Master of Science")
+
185
    LLB = _("Bachelor of Laws")
+
186
    LLM = _("Master of Laws")
+
187
    ir  = _("Engineer")
+
188
    ing = _("Technological Engineer")
+
189
    # Faculties
+
190
    FoMaLS = _("Faculty of Medicine and Life Sciences")
+
191
    Fos    = _("Faculty of Sciences")
+
192
    FoTS   = _("Faculty of Transportation Sciences")
+
193
    FoAaA  = _("Faculty of Architecture and Arts")
+
194
    FoBE   = _("Faculty of Business Economics")
+
195
    FoET   = _("Faculty of Engineering Technology")
+
196
    FoL    = _("Faculty of Law")
+
197
+
198
    name = models.CharField(
+
199
        max_length=128,
+
200
        blank=False,
+
201
        unique=True,
+
202
        help_text=_("The full name of this study, in the language it's taught in."),
+
203
        )
+
204
    degree_type = models.CharField(
+
205
        max_length=64,
+
206
        choices = (
+
207
            ('BSc', Bsc),
+
208
            ('MSc', Msc),
+
209
            ('LL.B', LLB),
+
210
            ('LL.M', LLM),
+
211
            ('ir.', ir ),
+
212
            ('ing.',ing),
+
213
            ),
+
214
        blank=False,
+
215
        help_text=_("The type of degree one obtains upon passing this study."),
+
216
        )
+
217
    language = models.CharField(
+
218
        max_length=64,
+
219
        choices = (
+
220
            ('NL', _("Dutch")),
+
221
            ('EN', _("English")),
+
222
            ('FR', _("French")),
+
223
            )
+
224
        null=False,
+
225
        help_text=_("The language in which this study is given."),
+
226
        )
+
227
    # Information about exam committee
+
228
    chairman = models.ForeignKey(
+
229
        "Joeni.users",
+
230
        on_delete=models.PROTECT,
+
231
        null=False,
+
232
        limit_choices_to={'is_staff': True},
+
233
        help_text=_("The chairman of this study."),
+
234
        )
+
235
    vice_chairman = models.ForeignKey(
+
236
        "Joeni.users",
+
237
        on_delete=models.PROTECT,
+
238
        null=False,
+
239
        help_text=_("The vice-chairman of this study."),
+
240
        limit_choices_to={'is_staff': True},
+
241
        )
+
242
    secretary = models.ForeignKey(
+
243
        "Joeni.users",
+
244
        on_delete=models.PROTECT,
+
245
        null=False,
+
246
        help_text=_("The secretary of this study."),
+
247
        limit_choices_to={'is_staff': True},
+
248
        )
+
249
    ombuds = models.ForeignKey(
+
250
        "Joeni.users",
+
251
        on_delete=models.PROTECT,
+
252
        null=False,
+
253
        help_text=_("The ombuds person of this study."),
+
254
        limit_choices_to={'is_staff': True},
+
255
        )
+
256
    vice_ombuds = models.ForeignKey(
+
257
        "Joeni.users",
+
258
        on_delete=models.PROTECT,
+
259
        null=False,
+
260
        help_text=_("The (replacing) ombuds person of this study."),
+
261
        limit_choices_to={'is_staff': True},
+
262
        )
+
263
    additional_members = models.ManyToManyField(
+
264
        "Joeni.users",
+
265
        help_text=_("All the other members of the exam committee."),
+
266
        limit_choices_to={'is_staff': True},
+
267
        )
+
268
    faculty = models.CharField(
+
269
        max_length=6,
+
270
        choices = (
+
271
            ('FoS', FoS),
+
272
            ('FoTS', FoTS),
+
273
            ('FoAaA', FoAaA),
+
274
            ('FoBE', FoBE),
+
275
            ('FoMaLS', FoMaLS),
+
276
            ('FoET', FoET),
+
277
            ('FoL', FoL),
+
278
            ),
+
279
        blank=False,
+
280
        help_text=_("The faculty where this study belongs to."),
+
281
        )
+
282
+
283
    def study_points(self):
+
284
        """ Returns the amount of study points for this year.
+
285
        This value is inferred based on the study programme information
+
286
        records that lists this study as their foreign key. """
+
287
        total_ECTS = 0
+
288
        for course in ProgrammeInformation.objects.filter(study=self):
+
289
            total_ECTS += course.ECTS
+
290
        return total_ECTS
+
291
    def years(self):
+
292
        """ Returns the amount of years this study takes.
+
293
        This value is inferred based on the study programme information
+
294
        records that lists this study as their foreign key. """
+
295
        highest_year = 0
+
296
        for course in ProgrammeInformation.objects.filter(study=self):
+
297
            highest_year = max(highest_year, course.year)
+
298
        return highest_year
+
299
    def __str__(self):
+
300
        return self.name
+
301

joeni/models.py

10 additions and 0 deletions.

View changes Hide changes
1
1
from django.core.exceptions import ValidationError  # For validating IBAN input
2
2
from django.utils.translation import ugettext_lazy as _
3
3
from django.db import models
4
4
import datetime
5
5
import os
6
6
7
7
class room(models.Model):
8
8
    """ Represents a room in the university.
9
9
    Rooms can have a number of properties, which are stored in the database.
10
10
    """
11
11
    name = models.TextField()
12
12
    seats = models.IntegerField()
13
13
    wheelchair_accessible = models.BooleanField(default=True,blank=False)
14
14
    exams_equipped = models.BooleanField(blank=False)
15
15
16
16
class User(models.Model):
17
17
    """ Replacement for the standard Django User model. """
18
18
    number = models.PositiveIntegerField(
19
19
        primary_key=True,
20
20
        help_text=_("The number assigned to this user."),
21
21
        )
22
22
    created = models.DateField(auto_now_add=True)
23
23
    passphrase = models.CharField(
24
24
        max_length=512,
25
25
        blank=False,
26
26
        help_text=_("The passphrase used for this account. This field must only contain hashed information."),
27
27
        )
28
28
    first_name = models.CharField(max_length=64, blank=False)
29
29
    last_name = models.CharField(max_length=64, blank=False)
30
30
    DOB = models.DateField(
+
31
        max_length=64,
+
32
        blank=True,
+
33
        help_text=_("The academic title of this user, if applicable."),
+
34
        )
+
35
    DOB = models.DateField(
31
36
        blank=False,
32
37
        editable=False,
33
38
        help_text=_("The date of birth of this user."),
34
39
        )
35
40
    POB = models.CharField(
36
41
        max_length=64,
37
42
        blank=False,
38
43
        editable=False,
39
44
        help_text=_("The place of birth of this user."),
40
45
        )
41
46
    nationality = models.CharField(
42
47
        max_length=64,
43
48
        blank=False,
44
49
        help_text=_("The current nationality of this user."),
45
50
        )
46
51
    national_registry_number = models.BigIntegerField(
47
52
        unique=True,
48
53
        editable=False,
49
54
        help_text=_("The assigned national registry number of this user."),
50
55
        )
51
56
    civil_status = models.CharField(
52
57
        choices = (
53
58
            ("Single", _("Single")),
54
59
            ("Married", _("Married")),
55
60
            ("Divorced", _("Divorced")),
56
61
            ("Widowed", _("Widowed")),
57
62
            ("Partnership", _("Partnership")),
58
63
            ),
59
64
        blank=False,
60
65
        # There may be more; consult http://www.aantrekkingskracht.com/trefwoord/burgerlijke-staat
61
66
        # for more information.
62
67
        help_text=_("The civil/marital status of the user."),
63
68
        )
64
69
65
70
    # Home address
+
71
        default=False,
+
72
        help_text=_("Determines if this user is part of the university's staff."),
+
73
        )
+
74
+
75
    # Home address
66
76
    home_street = models.CharField(max_length=64, blank=False)
67
77
    home_number = models.PositiveSmallIntegerField(blank=False)
68
78
    home_bus = models.PositiveSmallIntegerField()
69
79
    home_postal_code = models.PositiveSmallIntegerField(blank=False)
70
80
    home_country = models.CharField(max_length=64, blank=False)
71
81
    home_telephone = models.CharField(
72
82
        max_length=64,
73
83
        help_text=_("The telephone number for the house address. Prefix 0 can be presented with the national call code in the system."),
74
84
        )
75
85
    # Study address
76
86
    study_street = models.CharField(max_length=64, blank=False)
77
87
    study_number = models.PositiveSmallIntegerField(blank=False)
78
88
    study_bus = models.PositiveSmallIntegerField()
79
89
    study_postal_code = models.PositiveSmallIntegerField(blank=False)
80
90
    study_country = models.CharField(max_length=64, blank=False)
81
91
    study_telephone = models.CharField(
82
92
        max_length=64,
83
93
        help_text=_("The telephone number for the study address. Prefix 0 can be presented with the national call code in the system."),
84
94
        )
85
95
    study_cellphone = models.CharField(
86
96
        max_length=64,
87
97
        help_text=_("The cellphone number of the person. Prefix 0 can be presented with then national call code in the system."),
88
98
        )
89
99
    # Titularis address
90
100
    # XXX: These fields are only required if this differs from the user itself.
91
101
    titularis_street = models.CharField(max_length=64)
92
102
    titularis_number = models.PositiveSmallIntegerField()
93
103
    titularis_bus = models.PositiveSmallIntegerField()
94
104
    titularis_postal_code = models.PositiveSmallIntegerField()
95
105
    titularis_country = models.CharField(max_length=64)
96
106
    titularis_telephone = models.CharField(
97
107
        max_length=64,
98
108
        help_text=_("The telephone number of the titularis. Prefix 0 can be presented with the national call code in the system."),
99
109
        )
100
110
101
111
    # Financial details
102
112
    bank_account_number = models.CharField(
103
113
        max_length=34,  # Max length of all IBAN account numbers
104
114
        validators=[validate_IBAN],
105
115
        help_text=_("The IBAN of this user. No spaces!"),
106
116
        )
107
117
    BIC = models.CharField(
108
118
        max_length=11,
109
119
        validators=[validate_BIC],
110
120
        help_text=_("The BIC of this user's bank."),
111
121
        )
112
122
113
123
def validate_IBAN(value):
114
124
    """ Validates if the given value qualifies as a valid IBAN number.
115
125
    This validator checks if the structure is valid, and calculates the control
116
126
    number if the structure is correct. If the control number fails, or the
117
127
    structure is invalid, a ValidationError will be raised. In that case,
118
128
    the Error will specify whether the structure is incorrect, or the control
119
129
    number is not valid.
120
130
    """
121
131
    # FIXME: This function is not complete. When there's time, implement
122
132
    # as specified at https://nl.wikipedia.org/wiki/International_Bank_Account_Number#Structuur
123
133
    if False:
124
134
        raise ValidationError(
125
135
            _('%(value)s is not a valid IBAN number.'),
126
136
            params={'value': value},)
127
137
def validate_BIC(value):
128
138
    """ Same functionality as validate_IBAN, but for BIC-codes. """
129
139
    # FIXME: This function is not complete. When there's time, implement
130
140
    # as specified at https://nl.wikipedia.org/wiki/Business_Identifier_Code
131
141
    pass
132
142
133
143
134
144
""" NOTE: What about all the other features that should be in the administration?
135
145
While there are a lot of things to cover, as of now, I have no way to know which
136
146
ones are still valid, which are deprecated, and so on...
137
147
Additionally, every feature may have a different set of requirements, data,
138
148
and it's very likely making an abstract class won't do any good. Thus I have
139
149
decided to postpone making additional tables and forms for these features until
140
150
I have clearance about certain aspects. """
141
151
142
152
def post_title_directory(instance, filename):
143
153
    """ Files will be uploaded to MEDIA_ROOT/blog/<year of publishing>/<blog
144
154
    title>
145
155
    The blog title is determined by the text before the first period (".") in
146
156
    the filename. So if the file has the name "Trains are bæ.en.md", the file
147
157
    will be stored in "blog/<this year>/Trains are bæ". Name your files
148
158
    properly!
149
159
    It should also be noted that all files are stored in the same folder if they
150
160
    belong to the same blogpost, regardless of language. The titles that are
151
161
    displayed to the user however, should be the titles of the files themselves,
152
162
    which should be in the native language. So if a blog post is titled
153
163
    "Universities of Belgium", its Dutch counterpart should be titled
154
164
    "Universiteiten van België", so the correct title can be derived from the
155
165
    filename.
156
166
157
167
    Recommended way to name the uploaded file: "<name of blog post in language
158
168
    it's written>.md". This removes the maximum amount of redundancy (e.g. the
159
169
    language of the file can be derived from the title, no ".fr.md" or something
160
170
    like that necessary), and can directly be used for the end user (the title
161
171
    is what should be displayed).
162
172
    """
163
173
    english_file_name = os.path.basename(instance.english_file.name) # TODO: Test if this returns the file name!
164
174
    english_title = english_file_name.rpartition(".")[0] 
165
175
    year = datetime.date.today().year
166
176
167
177
    return "blog/{0}/{1}/{2}".format(year, english_title, filename)
168
178
169
179
class Post(models.Model):
170
180
    """ Represents a blog post. The title of the blog post is determnined by the name
171
181
    of the files.
172
182
    A blog post can be in 5 different languages: German, Spanish, English, French,
173
183
    and Dutch. For all these languages, a seperate field exists. Thus, a
174
184
    translated blog post has a seperate file for each translation, and is
175
185
    seperated from Django's internationalization/localization system.
176
186
    Only the English field is mandatory. The others may contain a value if a
177
187
    translated version exists, which will be displayed accordingly.
178
188
    """
179
189
    published = models.DateTimeField(auto_now_add=True)
180
190
    english_file = models.FileField(upload_to=post_title_directory, unique=True, blank=False)
181
191
    dutch_file = models.FileField(upload_to=post_title_directory, blank=True)
182
192
    french_file = models.FileField(upload_to=post_title_directory, blank=True)
183
193
    german_file = models.FileField(upload_to=post_title_directory, blank=True)
184
194
    spanish_file = models.FileField(upload_to=post_title_directory, blank=True)
185
195
    # Only the English file can be unique, because apparantly, there can't be
186
196
    # two blank fields in a unique column. Okay then.
187
197
188
198
    def __str__(self):
189
199
        return os.path.basename(self.english_file.name).rpartition(".")[0]
190
200
191
201
#class Comment(models.model):
192
202
    """ Represents a comment on a blog post.
193
203
194
204
    Comments are not linked to an account or anything, I'm trusting the
195
205
    commenter that he is honest with his credentials. That being said:
196
206
    XXX: Remember to put up a notification that comments are not checked for
197
207
    identity, and, unless verified by a trustworthy source, cannot be seen as
198
208
    being an actual statement from the commenter.
199
209
    Comments are linked to a blogpost, and are not filtered by language. (So a
200
210
    comment made by someone reading the article in Dutch, that's written in
201
211
    Dutch, will show up (unedited) for somebody whom's reading the Spanish
202
212
    version.
203
213
    XXX: Remember to notify (tiny footnote or something) that comments showing
204
214
    up in a foreign language is by design, and not a bug.
205
215
    """
206
216
#    date = models.DateTimeField(auto_now_add=True)
207
217
    #name = models.TextField()
208
218
    #mail = models.EmailField()
209
219
    #post = models.ForeignKey(Post) # TODO: Finish this class and the shit...
210
220