joeni

Finish creating database tables

Author
Maarten 'Vngngdn' Vangeneugden
Date
Nov. 18, 2017, 9: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