joeni

Made database migrations and other slight changes

Author
Maarten 'Vngngdn' Vangeneugden
Date
Nov. 18, 2017, 11:20 p.m.
Hash
f1175e5c018fb47d6b6dd76045df6de897b8ed39
Parent
fd0e170b5e330e5cdcdc6a4ceb04f88c6231ff40
Modified files
administration/admin.py
administration/migrations/0001_initial.py
administration/migrations/0002_auto_20171118_2203.py
administration/migrations/0003_auto_20171118_2204.py
administration/models.py
agora/admin.py
agora/migrations/0001_initial.py
agora/migrations/0002_auto_20171118_2203.py
agora/models.py
courses/admin.py
courses/migrations/0001_initial.py
courses/models.py
joeni/admin.py
joeni/apps.py
joeni/models.py
joeni/settings.py

administration/admin.py

11 additions and 1 deletion.

View changes Hide changes
1
1
+
2
from django.contrib.auth.admin import UserAdmin
+
3
+
4
admin.site.register(User, UserAdmin)
+
5
+
6
admin.site.register(Curriculum)
+
7
admin.site.register(CourseResult)
+
8
admin.site.register(PreRegistration)
+
9
admin.site.register(Room)
+
10
admin.site.register(RoomReservation)
+
11
admin.site.register(Degree)
+
12
2
13
# Register your models here.
3
-

administration/migrations/0001_initial.py

158 additions and 0 deletions.

View changes Hide changes
+
1
+
2
import administration.models
+
3
from django.conf import settings
+
4
import django.contrib.auth.models
+
5
import django.contrib.auth.validators
+
6
import django.core.validators
+
7
from django.db import migrations, models
+
8
import django.db.models.deletion
+
9
import django.utils.timezone
+
10
import uuid
+
11
+
12
+
13
class Migration(migrations.Migration):
+
14
+
15
    initial = True
+
16
+
17
    dependencies = [
+
18
    ]
+
19
+
20
    operations = [
+
21
        migrations.CreateModel(
+
22
            name='User',
+
23
            fields=[
+
24
                ('password', models.CharField(max_length=128, verbose_name='password')),
+
25
                ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
+
26
                ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
+
27
                ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
+
28
                ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
+
29
                ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
+
30
                ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
+
31
                ('number', models.PositiveIntegerField(help_text='The number assigned to this user.', primary_key=True, serialize=False)),
+
32
                ('created', models.DateField(auto_now_add=True)),
+
33
                ('passphrase', models.CharField(help_text='The passphrase used for this account. This field must only contain hashed information.', max_length=512)),
+
34
                ('first_name', models.CharField(max_length=64)),
+
35
                ('last_name', models.CharField(max_length=64)),
+
36
                ('title', models.CharField(blank=True, help_text='The academic title of this user, if applicable.', max_length=64)),
+
37
                ('DOB', models.DateField(editable=False, help_text='The date of birth of this user.')),
+
38
                ('POB', models.CharField(editable=False, help_text='The place of birth of this user.', max_length=64)),
+
39
                ('nationality', models.CharField(help_text='The current nationality of this user.', max_length=64)),
+
40
                ('national_registry_number', models.BigIntegerField(editable=False, help_text='The assigned national registry number of this user.', unique=True)),
+
41
                ('civil_status', models.CharField(choices=[('Single', 'Single'), ('Married', 'Married'), ('Divorced', 'Divorced'), ('Widowed', 'Widowed'), ('Partnership', 'Partnership')], help_text='The civil/marital status of the user.', max_length=32)),
+
42
                ('is_staff', models.BooleanField(default=False, help_text="Determines if this user is part of the university's staff.")),
+
43
                ('is_student', models.BooleanField(default=True, help_text='Indicates if this user is a student at the university.')),
+
44
                ('home_street', models.CharField(max_length=64)),
+
45
                ('home_number', models.PositiveSmallIntegerField()),
+
46
                ('home_bus', models.PositiveSmallIntegerField()),
+
47
                ('home_postal_code', models.PositiveSmallIntegerField()),
+
48
                ('home_country', models.CharField(max_length=64)),
+
49
                ('home_telephone', models.CharField(help_text='The telephone number for the house address. Prefix 0 can be presented with the national call code in the system.', max_length=64)),
+
50
                ('study_street', models.CharField(max_length=64)),
+
51
                ('study_number', models.PositiveSmallIntegerField()),
+
52
                ('study_bus', models.PositiveSmallIntegerField()),
+
53
                ('study_postal_code', models.PositiveSmallIntegerField()),
+
54
                ('study_country', models.CharField(max_length=64)),
+
55
                ('study_telephone', models.CharField(help_text='The telephone number for the study address. Prefix 0 can be presented with the national call code in the system.', max_length=64)),
+
56
                ('study_cellphone', models.CharField(help_text='The cellphone number of the person. Prefix 0 can be presented with then national call code in the system.', max_length=64)),
+
57
                ('titularis_street', models.CharField(max_length=64)),
+
58
                ('titularis_number', models.PositiveSmallIntegerField()),
+
59
                ('titularis_bus', models.PositiveSmallIntegerField()),
+
60
                ('titularis_postal_code', models.PositiveSmallIntegerField()),
+
61
                ('titularis_country', models.CharField(max_length=64)),
+
62
                ('titularis_telephone', models.CharField(help_text='The telephone number of the titularis. Prefix 0 can be presented with the national call code in the system.', max_length=64)),
+
63
                ('bank_account_number', models.CharField(help_text='The IBAN of this user. No spaces!', max_length=34, validators=[administration.models.validate_IBAN])),
+
64
                ('BIC', models.CharField(help_text="The BIC of this user's bank.", max_length=11, validators=[administration.models.validate_BIC])),
+
65
            ],
+
66
            options={
+
67
                'verbose_name': 'user',
+
68
                'verbose_name_plural': 'users',
+
69
                'abstract': False,
+
70
            },
+
71
            managers=[
+
72
                ('objects', django.contrib.auth.models.UserManager()),
+
73
            ],
+
74
        ),
+
75
        migrations.CreateModel(
+
76
            name='CourseResult',
+
77
            fields=[
+
78
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
79
                ('released', models.DateField(auto_now=True, help_text='The date that this result was last updated.')),
+
80
                ('first_score', models.PositiveSmallIntegerField(null=True, validators=[django.core.validators.MaxValueValidator(20, "The score mustn't be higher than 20.")])),
+
81
                ('second_score', models.PositiveSmallIntegerField(null=True, validators=[django.core.validators.MaxValueValidator(20, "The score mustn't be higher than 20.")])),
+
82
                ('result', models.CharField(choices=[('CRED', 'Credit acquired'), ('FAIL', 'Credit not acquired'), ('TLRD', 'Tolerated'), ('ITLD', 'Tolerance used')], help_text='The final result this record constitutes.', max_length=10)),
+
83
            ],
+
84
        ),
+
85
        migrations.CreateModel(
+
86
            name='Curriculum',
+
87
            fields=[
+
88
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
89
                ('year', models.DateField(auto_now_add=True, db_index=True, help_text='The academic year for which this curriculum is.')),
+
90
                ('last_modified', models.DateTimeField(auto_now=True, help_text='The last timestamp that this was updated.')),
+
91
                ('approved', models.NullBooleanField(default=None, help_text='Indicates if this curriculum has been approved. If true, that means the responsible committee has reviewed and approved the student for this curriculum. False otherwise. If review is still pending, the value is NULL. Modifying the curriculum implies this setting is set to NULL again.')),
+
92
                ('note', models.TextField(blank=True, help_text='Additional notes regarding this curriculum. This has multiple uses. For the student, it is used to clarify any questions, or to motivate why (s)he wants to take a course for which the requirements were not met. The reviewing committee can use this field to argument their decision, especially for when the curriculum is denied.')),
+
93
            ],
+
94
        ),
+
95
        migrations.CreateModel(
+
96
            name='Degree',
+
97
            fields=[
+
98
                ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+
99
                ('first_name', models.CharField(max_length=64)),
+
100
                ('last_name', models.CharField(max_length=64)),
+
101
                ('additional_names', models.CharField(max_length=64)),
+
102
                ('DOB', models.DateField(editable=False)),
+
103
                ('POB', models.CharField(editable=False, max_length=64)),
+
104
                ('study', models.CharField(editable=False, max_length=64)),
+
105
                ('achieved', models.DateField(editable=False)),
+
106
            ],
+
107
        ),
+
108
        migrations.CreateModel(
+
109
            name='PreRegistration',
+
110
            fields=[
+
111
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
112
                ('created', models.DateField(auto_now_add=True)),
+
113
                ('first_name', models.CharField(help_text='Your first name.', max_length=64)),
+
114
                ('last_name', models.CharField(help_text='Your last name.', max_length=64)),
+
115
                ('additional_names', models.CharField(blank=True, help_text='Any additional names.', max_length=64)),
+
116
                ('title', models.CharField(blank=True, help_text='Any additional titles, prefixes, ...', max_length=64)),
+
117
                ('DOB', models.DateField(editable=False, help_text='Your date of birth.')),
+
118
                ('POB', models.CharField(editable=False, help_text='The place you were born.', max_length=64)),
+
119
                ('nationality', models.CharField(help_text='Your current nationality.', max_length=64)),
+
120
                ('national_registry_number', models.BigIntegerField(help_text='If you have one, your national registry number.', null=True)),
+
121
                ('civil_status', models.CharField(choices=[('Single', 'Single'), ('Married', 'Married'), ('Divorced', 'Divorced'), ('Widowed', 'Widowed'), ('Partnership', 'Partnership')], help_text='Your civil/marital status.', max_length=32)),
+
122
                ('email', models.EmailField(help_text='The e-mail address we will use to communicate until your actual registration.', max_length=254, unique=True)),
+
123
                ('study_type', models.CharField(choices=[('Diplom contract', 'Diplom contract'), ('Exam contract', 'Exam contract'), ('Credit contract', 'Credit contract')], help_text='The type of study contract you wish to follow.', max_length=32)),
+
124
                ('document', models.FileField(help_text='Any legal documents regarding your enrollment.', upload_to='pre-enrollment/%Y')),
+
125
            ],
+
126
        ),
+
127
        migrations.CreateModel(
+
128
            name='Room',
+
129
            fields=[
+
130
                ('name', models.CharField(help_text='The name of this room. If more appropriate, this can be the colloquial name.', max_length=20, primary_key=True, serialize=False)),
+
131
                ('seats', models.PositiveSmallIntegerField(help_text='The amount of available seats in this room. This can be handy for exams for example.')),
+
132
                ('wheelchair_accessible', models.BooleanField(default=True)),
+
133
                ('exams_equipped', models.BooleanField(default=True, help_text='Indicates if exams can reasonably be held in this room.')),
+
134
                ('computers_available', models.PositiveSmallIntegerField(default=False, help_text='Indicates how many computers are available in this room.')),
+
135
                ('projector_available', models.BooleanField(default=False, help_text='Indicates if a projector is available at this room.')),
+
136
                ('blackboards_available', models.PositiveSmallIntegerField(help_text='The amount of blackboards available in this room.')),
+
137
                ('whiteboards_available', models.PositiveSmallIntegerField(help_text='The amount of whiteboards available in this room.')),
+
138
                ('category', models.CharField(choices=[('LABORATORY', 'Laboratory'), ('CLASS_ROOM', 'Class room'), ('AUDITORIUM', 'Auditorium'), ('PC_ROOM', 'PC room'), ('PUBLIC_ROOM', 'Public room'), ('OFFICE', 'Office'), ('PRIVATE_ROOM', 'Private room'), ('WORKSHOP', 'Workshop'), ('OTHER', 'Other')], help_text='The category that best suits the character of this room.', max_length=16)),
+
139
                ('reservable', models.BooleanField(default=True, help_text='Indicates if this room can be reserved for something.')),
+
140
                ('note', models.TextField(blank=True, help_text="If some additional info is required for this room, like a characteristic property (e.g. 'Usually occupied by 2BACH informatics'), state it here.")),
+
141
            ],
+
142
        ),
+
143
        migrations.CreateModel(
+
144
            name='RoomReservation',
+
145
            fields=[
+
146
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
147
                ('timestamp', models.DateTimeField(auto_now_add=True)),
+
148
                ('start_time', models.DateTimeField(help_text='The time that this reservation starts.')),
+
149
                ('end_time', models.DateTimeField(help_text='The time that this reservation ends.')),
+
150
                ('seats', models.PositiveSmallIntegerField(help_text='Indicates how many seats are required. If this is left null, it is assumed the entire room has to be reserved.', null=True)),
+
151
                ('reason', models.CharField(blank=True, help_text='The reason for this reservation, if useful.', max_length=64)),
+
152
                ('note', models.TextField(blank=True, help_text='If some additional info is required for this reservation, state it here.')),
+
153
                ('reservator', models.ForeignKey(editable=False, help_text='The person that made the reservation (and thus responsible).', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+
154
                ('room', models.ForeignKey(editable=False, help_text='The room that is being reserved at this point.', limit_choices_to={'reservable': True}, on_delete=django.db.models.deletion.CASCADE, to='administration.Room')),
+
155
            ],
+
156
        ),
+
157
    ]
+
158

administration/migrations/0002_auto_20171118_2203.py

59 additions and 0 deletions.

View changes Hide changes
+
1
+
2
from django.conf import settings
+
3
from django.db import migrations, models
+
4
import django.db.models.deletion
+
5
+
6
+
7
class Migration(migrations.Migration):
+
8
+
9
    initial = True
+
10
+
11
    dependencies = [
+
12
        ('courses', '0001_initial'),
+
13
        ('administration', '0001_initial'),
+
14
        ('auth', '0009_alter_user_last_name_max_length'),
+
15
    ]
+
16
+
17
    operations = [
+
18
        migrations.AddField(
+
19
            model_name='preregistration',
+
20
            name='study',
+
21
            field=models.ForeignKey(help_text='The study you wish to follow. Be sure to provide all legaldocuments that are required for this study with this application, or bring them with you to the final registration.', on_delete=django.db.models.deletion.PROTECT, to='courses.Study'),
+
22
        ),
+
23
        migrations.AddField(
+
24
            model_name='degree',
+
25
            name='user',
+
26
            field=models.ForeignKey(help_text='The person that achieved this degree, if (s)he still has an account at this university. If the account is deleted at a later date, this field will be set to NULL, but the other fields will be retained.', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
+
27
        ),
+
28
        migrations.AddField(
+
29
            model_name='curriculum',
+
30
            name='courses',
+
31
            field=models.ManyToManyField(help_text='All the courses included in this curriculum.', to='courses.Course'),
+
32
        ),
+
33
        migrations.AddField(
+
34
            model_name='curriculum',
+
35
            name='student',
+
36
            field=models.ForeignKey(editable=False, limit_choices_to={'is_student': True}, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, unique_for_year='year'),
+
37
        ),
+
38
        migrations.AddField(
+
39
            model_name='courseresult',
+
40
            name='course_programme',
+
41
            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='courses.ProgrammeInformation'),
+
42
        ),
+
43
        migrations.AddField(
+
44
            model_name='courseresult',
+
45
            name='student',
+
46
            field=models.ForeignKey(limit_choices_to={'is_student': True}, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
+
47
        ),
+
48
        migrations.AddField(
+
49
            model_name='user',
+
50
            name='groups',
+
51
            field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'),
+
52
        ),
+
53
        migrations.AddField(
+
54
            model_name='user',
+
55
            name='user_permissions',
+
56
            field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
+
57
        ),
+
58
    ]
+
59

administration/migrations/0003_auto_20171118_2204.py

18 additions and 0 deletions.

View changes Hide changes
+
1
+
2
from django.db import migrations, models
+
3
+
4
+
5
class Migration(migrations.Migration):
+
6
+
7
    dependencies = [
+
8
        ('administration', '0002_auto_20171118_2203'),
+
9
    ]
+
10
+
11
    operations = [
+
12
        migrations.AlterField(
+
13
            model_name='user',
+
14
            name='number',
+
15
            field=models.AutoField(help_text='The number assigned to this user.', primary_key=True, serialize=False),
+
16
        ),
+
17
    ]
+
18

administration/models.py

150 additions and 12 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
import datetime
+
6
import os
+
7
import uuid
+
8
5
9
class PersonalDetails(models.Model):
6
-
    user = models.OneToOneField(
7
-
        'joeni.User',
8
-
        on_delete=models.CASCADE,
9
-
        )
+
10
    """ Validates if the given value qualifies as a valid IBAN number.
+
11
    This validator checks if the structure is valid, and calculates the control
+
12
    number if the structure is correct. If the control number fails, or the
+
13
    structure is invalid, a ValidationError will be raised. In that case,
+
14
    the Error will specify whether the structure is incorrect, or the control
+
15
    number is not valid.
+
16
    """
+
17
    # FIXME: This function is not complete. When there's time, implement
+
18
    # as specified at https://nl.wikipedia.org/wiki/International_Bank_Account_Number#Structuur
+
19
    if False:
+
20
        raise ValidationError(
+
21
            _('%(value)s is not a valid IBAN number.'),
+
22
            params={'value': value},)
+
23
def validate_BIC(value):
+
24
    """ Same functionality as validate_IBAN, but for BIC-codes. """
+
25
    # FIXME: This function is not complete. When there's time, implement
+
26
    # as specified at https://nl.wikipedia.org/wiki/Business_Identifier_Code
+
27
    pass
+
28
+
29
class User(AbstractUser):
+
30
    """ Replacement for the standard Django User model. """
+
31
    number = models.AutoField(
+
32
        primary_key=True,
+
33
        help_text=_("The number assigned to this user."),
+
34
        )
+
35
    created = models.DateField(auto_now_add=True)
+
36
    passphrase = models.CharField(
+
37
        max_length=512,
+
38
        blank=False,
+
39
        help_text=_("The passphrase used for this account. This field must only contain hashed information."),
+
40
        )
+
41
    first_name = models.CharField(max_length=64, blank=False)
+
42
    last_name = models.CharField(max_length=64, blank=False)
+
43
    title = models.CharField(
+
44
        max_length=64,
+
45
        blank=True,
+
46
        help_text=_("The academic title of this user, if applicable."),
+
47
        )
+
48
    DOB = models.DateField(
+
49
        blank=False,
+
50
        editable=False,
+
51
        help_text=_("The date of birth of this user."),
+
52
        )
+
53
    POB = models.CharField(
+
54
        max_length=64,
+
55
        blank=False,
+
56
        editable=False,
+
57
        help_text=_("The place of birth of this user."),
+
58
        )
+
59
    nationality = models.CharField(
+
60
        max_length=64,
+
61
        blank=False,
+
62
        help_text=_("The current nationality of this user."),
+
63
        )
+
64
    # XXX: What if this starts with zeros?
+
65
    national_registry_number = models.BigIntegerField(
+
66
        unique=True,
+
67
        editable=False,
+
68
        help_text=_("The assigned national registry number of this user."),
+
69
        )
+
70
    civil_status = models.CharField(
+
71
        max_length=32,
+
72
        choices = (
+
73
            ("Single", _("Single")),
+
74
            ("Married", _("Married")),
+
75
            ("Divorced", _("Divorced")),
+
76
            ("Widowed", _("Widowed")),
+
77
            ("Partnership", _("Partnership")),
+
78
            ),
+
79
        blank=False,
+
80
        # There may be more; consult http://www.aantrekkingskracht.com/trefwoord/burgerlijke-staat
+
81
        # for more information.
+
82
        help_text=_("The civil/marital status of the user."),
+
83
        )
10
84
11
85
class Curriculum(models.Model):
+
86
        default=False,
+
87
        help_text=_("Determines if this user is part of the university's staff."),
+
88
        )
+
89
    is_student = models.BooleanField(
+
90
        default=True,
+
91
        help_text=_("Indicates if this user is a student at the university."),
+
92
        )
+
93
+
94
    # Home address
+
95
    home_street = models.CharField(max_length=64, blank=False)
+
96
    home_number = models.PositiveSmallIntegerField(blank=False)
+
97
    home_bus = models.PositiveSmallIntegerField()
+
98
    home_postal_code = models.PositiveSmallIntegerField(blank=False)
+
99
    home_country = models.CharField(max_length=64, blank=False)
+
100
    home_telephone = models.CharField(
+
101
        max_length=64,
+
102
        help_text=_("The telephone number for the house address. Prefix 0 can be presented with the national call code in the system."),
+
103
        )
+
104
    # Study address
+
105
    study_street = models.CharField(max_length=64, blank=False)
+
106
    study_number = models.PositiveSmallIntegerField(blank=False)
+
107
    study_bus = models.PositiveSmallIntegerField()
+
108
    study_postal_code = models.PositiveSmallIntegerField(blank=False)
+
109
    study_country = models.CharField(max_length=64, blank=False)
+
110
    study_telephone = models.CharField(
+
111
        max_length=64,
+
112
        help_text=_("The telephone number for the study address. Prefix 0 can be presented with the national call code in the system."),
+
113
        )
+
114
    study_cellphone = models.CharField(
+
115
        max_length=64,
+
116
        help_text=_("The cellphone number of the person. Prefix 0 can be presented with then national call code in the system."),
+
117
        )
+
118
    # Titularis address
+
119
    # XXX: These fields are only required if this differs from the user itself.
+
120
    titularis_street = models.CharField(max_length=64)
+
121
    titularis_number = models.PositiveSmallIntegerField()
+
122
    titularis_bus = models.PositiveSmallIntegerField()
+
123
    titularis_postal_code = models.PositiveSmallIntegerField()
+
124
    titularis_country = models.CharField(max_length=64)
+
125
    titularis_telephone = models.CharField(
+
126
        max_length=64,
+
127
        help_text=_("The telephone number of the titularis. Prefix 0 can be presented with the national call code in the system."),
+
128
        )
+
129
+
130
    # Financial details
+
131
    bank_account_number = models.CharField(
+
132
        max_length=34,  # Max length of all IBAN account numbers
+
133
        validators=[validate_IBAN],
+
134
        help_text=_("The IBAN of this user. No spaces!"),
+
135
        )
+
136
    BIC = models.CharField(
+
137
        max_length=11,
+
138
        validators=[validate_BIC],
+
139
        help_text=_("The BIC of this user's bank."),
+
140
        )
+
141
+
142
""" NOTE: What about all the other features that should be in the administration?
+
143
While there are a lot of things to cover, as of now, I have no way to know which
+
144
ones are still valid, which are deprecated, and so on...
+
145
Additionally, every feature may have a different set of requirements, data,
+
146
and it's very likely making an abstract class won't do any good. Thus I have
+
147
decided to postpone making additional tables and forms for these features until
+
148
I have clearance about certain aspects. """
+
149
+
150
class Curriculum(models.Model):
12
151
    """ The curriculum of a particular student.
13
152
    Every academic year, a student has to hand in a curriculum (s)he wishes to
14
153
    follow. This is then reviewed by a committee. A curriculum exists of all the
15
154
    courses one wants to partake in in a certain year. """
16
155
    student = models.ForeignKey(
17
156
        "joeni.User",
18
-
        on_delete=models.CASCADE,
+
157
        on_delete=models.CASCADE,
19
158
        limit_choices_to={'is_student': True},
20
159
        null=False,
21
160
        editable=False,
22
161
        unique_for_year="year",  # Only 1 curriculum per year
23
162
        )
24
163
    year = models.DateField(
25
164
        auto_now_add=True,
26
165
        db_index=True,
27
166
        help_text=_("The academic year for which this curriculum is."),
28
167
        )
29
168
    last_modified = models.DateTimeField(
30
169
        auto_now=True,
31
170
        help_text=_("The last timestamp that this was updated."),
32
171
        )
33
172
    courses = models.ManyToManyField(
34
173
        "courses.Course",
35
174
        null=False,
36
175
        help_text=_("All the courses included in this curriculum."),
37
176
        )
38
177
    approved = models.NullBooleanField(
39
178
        default=None,
40
179
        help_text=_("Indicates if this curriculum has been approved. If true, "
41
180
                    "that means the responsible committee has reviewed and "
42
181
                    "approved the student for this curriculum. False otherwise. "
43
182
                    "If review is still pending, the value is NULL. Modifying "
44
183
                    "the curriculum implies this setting is set to NULL again."),
45
184
        )
46
185
    note = models.TextField(
47
186
        blank=True,
48
187
        help_text=_("Additional notes regarding this curriculum. This has "
49
188
                    "multiple uses. For the student, it is used to clarify "
50
189
                    "any questions, or to motivate why (s)he wants to take a "
51
190
                    "course for which the requirements were not met. "
52
191
                    "The reviewing committee can use this field to argument "
53
192
                    "their decision, especially for when the curriculum is "
54
193
                    "denied."),
55
194
        )
56
195
57
196
    def curriculum_type(self):
58
197
        """ Returns the type of this curriculum. At the moment, this is
59
198
        either a standard programme, or an individualized programme. """
60
199
        # Currently: A standard programme means: All courses are from the
61
200
        # same study, ánd from the same year. Additionally, all courses
62
201
        # from that year must've been taken.
63
202
        # FIXME: Need a way to determine what is the standard programme.
64
203
        # If not possible, make this a charfield with options or something
65
204
        pass
66
205
67
206
    def __str__(self):
68
207
        year = self.year.year
69
208
        if self.year.month < 7:
70
209
            return str(self.student) +" | "+ str(year-1) +"-"+ str(year)
71
210
        else:
72
211
            return str(self.student) +" | "+ str(year) +"-"+ str(year+1)
73
212
74
213
75
214
class CourseResult(models.Model):
76
215
    """ A student has to obtain a certain course result. These are stored here,
77
216
    together with all the appropriate information. """
78
217
    # TODO: Validate that a course programme for a student can only be made once per year for each course, if possible.
79
218
    CRED = _("Credit acquired")
80
219
    FAIL = _("Credit not acquired")
81
220
    TLRD = _("Tolerated")
82
221
    ITLD = _("Tolerance used")
83
222
    # Possible to add more in the future
84
223
85
224
    student = models.ForeignKey(
86
225
        "joeni.User",
87
-
        on_delete=models.CASCADE,
+
226
        on_delete=models.CASCADE,
88
227
        limit_choices_to={'is_student': True},
89
228
        null=False,
90
229
        )
91
230
    course_programme = models.ForeignKey(
92
231
        "courses.ProgrammeInformation",
93
232
        on_delete=models.PROTECT,
94
233
        null=False,
95
234
        )
96
235
    released = models.DateField(
97
236
        auto_now=True,
98
237
        help_text=_("The date that this result was last updated."),
99
238
        )
100
239
    first_score = models.PositiveSmallIntegerField(
101
240
        null=True,  # It's possible a score does not exist.
102
241
        validators=[MaxValueValidator(
103
242
            20,
104
243
            _("%(score)s mustn't be higher than 20."),
105
-
                params={'score': score},
106
-
            )],
+
244
            )],
107
245
        )
108
246
    second_score = models.PositiveSmallIntegerField(
109
247
        null=True,
110
248
        validators=[MaxValueValidator(
111
249
            20,
112
250
            _("%(score)s mustn't be higher than 20."),
113
-
                params={'score': score},
114
-
            )],
+
251
            )],
115
252
        )
116
253
    result = models.CharField(
117
254
        max_length=10,
118
255
        choices = (
119
256
            ("CRED", CRED),
120
257
            ("FAIL", FAIL),
121
258
            ("TLRD", TLRD),
122
259
            ("ITLD", ITLD),
123
260
            ),
124
261
        blank=False,
125
262
        help_text=_("The final result this record constitutes."),
126
263
        )
127
264
128
265
    def __str__(self):
129
266
        stdnum = str(self.student.number)
130
267
        result = self.result
131
268
        if result == "CRED":
132
269
            if self.first_score < 10:
133
270
                result = "C" + self.first_score + "1"
134
271
            else:
135
272
                result = "C" + self.second_score + "2"
136
273
        course = str(self.course_programme.course)
137
274
        return stdnum +" ("+ result +") | "+ course
138
275
139
276
class PreRegistration(models.Model):
140
277
    """ At the beginning of the new academic year, students can register
141
278
    themselves at the university. Online, they can do a preregistration already.
142
279
    These records are stored here and can later be retrieved for the actual
143
280
    registration process.
144
281
    Note: The current system in use at Hasselt University provides a password system.
145
282
    That will be eliminated here. Just make sure that the entered details are correct.
146
283
    Should there be an error, and the same email address is used to update something,
147
284
    a mail will be sent to that address to verify this was a genuine update."""
148
285
    created = models.DateField(auto_now_add=True)
149
286
    first_name = models.CharField(max_length=64, blank=False, help_text=_("Your first name."))
150
287
    last_name = models.CharField(max_length=64, blank=False, help_text=_("Your last name."))
151
288
    additional_names = models.CharField(max_length=64, blank=True, help_text=_("Any additional names."))
152
289
    title = models.CharField(
153
290
        max_length=64,
154
291
        blank=True,
155
292
        help_text=_("Any additional titles, prefixes, ..."),
156
293
        )
157
294
    DOB = models.DateField(
158
295
        blank=False,
159
296
        editable=False,
160
297
        help_text=_("Your date of birth."),
161
298
        )
162
299
    POB = models.CharField(
163
300
        max_length=64,
164
301
        blank=False,
165
302
        editable=False,
166
303
        help_text=_("The place you were born."),
167
304
        )
168
305
    nationality = models.CharField(
169
306
        max_length=64,
170
307
        blank=False,
171
308
        help_text=_("Your current nationality."),
172
309
        )
173
310
    national_registry_number = models.BigIntegerField(
174
311
        null=True,
175
312
        help_text=_("If you have one, your national registry number."),
176
313
        )
177
314
    civil_status = models.CharField(
178
315
        choices = (
+
316
        choices = (
179
317
            ("Single", _("Single")),
180
318
            ("Married", _("Married")),
181
319
            ("Divorced", _("Divorced")),
182
320
            ("Widowed", _("Widowed")),
183
321
            ("Partnership", _("Partnership")),
184
322
            ),
185
323
        blank=False,
186
324
        # There may be more; consult http://www.aantrekkingskracht.com/trefwoord/burgerlijke-staat
187
325
        # for more information.
188
326
        help_text=_("Your civil/marital status."),
189
327
        )
190
328
    email = models.EmailField(
191
329
        blank=False,
192
330
        unique=True,
193
331
        help_text=_("The e-mail address we will use to communicate until your actual registration."),
194
332
        )
195
333
    study = models.ForeignKey(
196
334
        "courses.Study",
197
335
        on_delete=models.PROTECT,
198
336
        null=False,
199
337
        help_text=_("The study you wish to follow. Be sure to provide all legal"
200
338
                    "documents that are required for this study with this "
201
339
                    "application, or bring them with you to the final registration."),
202
340
        )
203
341
    study_type = models.CharField(
204
342
        max_length=32,
205
343
        choices = (
206
344
            ("Diplom contract", _("Diplom contract")),
207
345
            ("Exam contract", _("Exam contract")),
208
346
            ("Credit contract", _("Credit contract")),
209
347
            ),
210
348
        blank=False,
211
349
        help_text=_("The type of study contract you wish to follow."),
212
350
        )
213
351
    document = models.FileField(
214
352
        upload_to="pre-enrollment/%Y",
215
353
        help_text=_("Any legal documents regarding your enrollment."),
216
354
        )
217
355
    # XXX: If the database in production is PostgreSQL, comment document, and
218
356
    # uncomment the next column.
219
357
    """documents = models.ArrayField(
220
358
        models.FileField(upload_to="pre-enrollment/%Y"),
221
359
        help_text=_("Any legal documents regarding your enrollment."),
222
360
        )"""
223
361
224
362
    def __str__(self):
225
363
        name = self.last_name +" "+ self.first_name
226
364
        dob = self.DOB.strftime("%d/%m/%Y")
227
365
        return name +" | "+ dob
228
366
229
367
230
368
# Planning and organization related tables
231
369
class Room(models.Model):
232
370
    """ Represents a room in the university.
233
371
    Rooms can have a number of properties, which are stored in the database.
234
372
    """
235
373
    # Types of rooms
236
374
    LABORATORY = _("Laboratory")  # Chemistry/Physics equipped rooms
237
375
    CLASS_ROOM = _("Class room")  # Simple class rooms
238
376
    AUDITORIUM = _("Auditorium")  # Large rooms with ample seating and equipment for lectures
239
377
    PC_ROOM    = _("PC room"   )  # Rooms equipped for executing PC related tasks
240
378
    PUBLIC_ROOM= _("Public room") # Restaurants, restrooms, ... general public spaces
241
379
    OFFICE     = _("Office"    )  # Private offices for staff
242
380
    PRIVATE_ROOM = _("Private room")  # Rooms accessible for a limited public; cleaning cupboards, kitchens, ...
243
381
    WORKSHOP   = _("Workshop"  )  # Rooms with hardware equipment to build and work on materials
244
382
    OTHER      = _("Other"     )  # Rooms that do not fit in any other category
245
383
246
384
247
385
    name = models.CharField(
248
386
        max_length=20,
249
387
        primary_key=True,
250
388
        blank=False,
251
389
        help_text=_("The name of this room. If more appropriate, this can be the colloquial name."),
252
390
        )
253
391
    seats = models.PositiveSmallIntegerField(
254
392
        help_text=_("The amount of available seats in this room. This can be handy for exams for example."),
255
393
        )
256
394
    wheelchair_accessible = models.BooleanField(default=True)
257
395
    exams_equipped = models.BooleanField(
258
396
        default=True,
259
397
        help_text=_("Indicates if exams can reasonably be held in this room."),
260
398
        )
261
399
    computers_available = models.PositiveSmallIntegerField(
262
400
        default=False,
263
401
        help_text=_("Indicates how many computers are available in this room."),
264
402
        )
265
403
    projector_available = models.BooleanField(
266
404
        default=False,
267
405
        help_text=_("Indicates if a projector is available at this room."),
268
406
        )
269
407
    blackboards_available = models.PositiveSmallIntegerField(
270
408
        help_text=_("The amount of blackboards available in this room."),
271
409
        )
272
410
    whiteboards_available = models.PositiveSmallIntegerField(
273
411
        help_text=_("The amount of whiteboards available in this room."),
274
412
        )
275
413
    category = models.CharField(
276
414
        max_length=16,
277
415
        blank=False,
278
416
        choices = (
279
417
            ("LABORATORY", LABORATORY),
280
418
            ("CLASS_ROOM", CLASS_ROOM),
281
419
            ("AUDITORIUM", AUDITORIUM),
282
420
            ("PC_ROOM", PC_ROOM),
283
421
            ("PUBLIC_ROOM", PUBLIC_ROOM),
284
422
            ("OFFICE", OFFICE),
285
423
            ("PRIVATE_ROOM", PRIVATE_ROOM),
286
424
            ("WORKSHOP", WORKSHOP),
287
425
            ("OTHER", OTHER),
288
426
            ),
289
427
        help_text=_("The category that best suits the character of this room."),
290
428
        )
291
429
    reservable = models.BooleanField(
292
430
        default=True,
293
431
        help_text=_("Indicates if this room can be reserved for something."),
294
432
        )
295
433
    note = models.TextField(
296
434
        blank=True,
297
435
        help_text=_("If some additional info is required for this room, like a "
298
436
                    "characteristic property (e.g. 'Usually occupied by 2BACH "
299
437
                    "informatics'), state it here."),
300
438
        )
301
439
    # TODO: Add a campus/building field or not?
302
440
303
441
    def reservation_possible(self, begin, end, seats=None):
304
442
        """ Returns a boolean indicating if reservating during the given time
305
443
        is possible. If the begin overlaps with a reservation's end or vice versa,
306
444
        this is regarded as possible.
307
445
        Takes seats as optional argument. If not specified, it is assumed the entire
308
446
        room has to be reserved. """
309
447
        if self.reservable is False:
310
448
            return False
311
449
        if seats is not None and seats < 0: raise ValueError(_("seats ∈ ℕ"))
312
450
313
451
        reservations = RoomReservation.objects.filter(room=self)
314
452
        for reservation in reservations:
315
453
            if reservation.end <= begin or reservation.begin >= end:
316
454
                continue  # Can be trivially skipped, no overlap here
317
455
            elif seats is None or reservation.seats is None:
318
456
                return False  # The whole room cannot be reserved -> False
319
457
            elif seats + reservation.seats > self.seats:
320
458
                    return False  # Total amount of seats exceeds the available amount -> False
321
459
        return True  # No overlappings found -> True
322
460
323
461
    def __str__(self):
324
462
        return self.name
325
463
326
464
class RoomReservation(models.Model):
327
465
    """ Rooms are to be reserved from time to time. They can be reserved
328
466
    by externals, for something else, and whatnot. That is stored in this table.
329
467
    """
330
468
    room = models.ForeignKey(
331
469
        "Room",
332
470
        on_delete=models.CASCADE,
333
471
        null=False,
334
472
        editable=False,
335
473
        db_index=True,
336
474
        limit_choices_to={"reservable": True},
337
475
        help_text=_("The room that is being reserved at this point."),
338
476
        )
339
477
    reservator = models.ForeignKey(
340
478
        "joeni.User",
341
-
        on_delete=models.CASCADE,
+
479
        on_delete=models.CASCADE,
342
480
        null=False,
343
481
        editable=False,
344
482
        help_text=_("The person that made the reservation (and thus responsible)."),
345
483
        )
346
484
    timestamp = models.DateTimeField(auto_now_add=True)
347
485
    start_time = models.DateTimeField(
348
486
        null=False,
349
487
        help_text=_("The time that this reservation starts."),
350
488
        )
351
489
    end_time = models.DateTimeField(
352
490
        null=False,
353
491
        help_text=_("The time that this reservation ends."),
354
492
        )
355
493
    seats = models.PositiveSmallIntegerField(
356
494
        null=True,
357
495
        help_text=_("Indicates how many seats are required. If this is left null, "
358
496
                    "it is assumed the entire room has to be reserved."),
359
497
        )
360
498
    reason = models.CharField(
361
499
        max_length=64,
362
500
        blank=True,
363
501
        help_text=_("The reason for this reservation, if useful."),
364
502
        )
365
503
    note = models.TextField(
366
504
        blank=True,
367
505
        help_text=_("If some additional info is required for this reservation, "
368
506
                    "state it here."),
369
507
        )
370
508
371
509
    def __str__(self):
372
510
        start = self.start_time.strftime("%H:%M")
373
511
        end = self.end_time.strftime("%H:%M")
374
512
        return str(self.room) +" | "+ start +"-"+ end
375
513
376
514
class Degree(models.Model):
377
515
    """ Contains all degrees that were achieved at this university.
378
516
    There are no foreign keys in this field. This allows system
379
517
    administrators to safely remove accounts from alumni, without
380
518
    the risk of breaking referential integrity or accidentally removing
381
519
    degrees.
382
520
    While keeping some fields editable that look like they shouldn't be
383
521
    (e.g. first_name), this makes it possible for alumni to have a name change
384
522
    later in their life, and still being able to get a copy of their degree. """
385
523
    """ Reason for an ID field for every degree:
386
524
    This system allows for employers to verify that a certain applicant has indeed,
387
525
    achieved the degrees (s)he proclaims to have. Because of privacy concerns,
388
526
    a university cannot disclose information about alumni.
389
527
    That's where the degree ID comes in. This ID can be printed on all future
390
528
    degrees. The employer can then visit the university's website, and simply
391
529
    enter the ID. The website will then simply print what study is attached to
392
530
    this degree, but not disclose names or anything identifiable. This strikes
393
531
    thé perfect balance between (easy and digital) degree verification for employers, and maintaining
394
532
    alumni privacy to the highest extent possible. """
395
533
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
396
534
    first_name = models.CharField(
397
535
        max_length=64,
398
536
        blank=False,
399
537
        )
400
538
    last_name = models.CharField(
401
539
        max_length=64,
402
540
        blank=False,
403
541
        )
404
542
    additional_names = models.CharField(
405
543
        max_length=64,
406
544
        )
407
545
    DOB = models.DateField(editable=False, null=False)  # This can't be changed, of course
408
546
    POB = models.CharField(
409
547
        max_length=64,
410
548
        blank=False,
411
549
        editable=False,
412
550
        )
413
551
    # The study also has to be a charfield, because if a study is removed,
414
552
    # The information will be lost.
415
553
    study = models.CharField(
416
554
        max_length=64,
417
555
        blank=False,
418
556
        editable=False,
419
557
        )
420
558
    achieved = models.DateField(editable=False, null=False)
421
559
    user = models.ForeignKey(
422
560
        "joeni.User",
423
-
        on_delete=models.SET_NULL,
+
561
        on_delete=models.SET_NULL,
424
562
        null=True,
425
563
        help_text=_("The person that achieved this degree, if (s)he still has "
426
564
                    "an account at this university. If the account is deleted "
427
565
                    "at a later date, this field will be set to NULL, but the "
428
566
                    "other fields will be retained."),
429
567
        )
430
568
431
569
    def __str__(self):
432
570
        return self.first_name +" "+ self.last_name +" | "+ self.study
433
571

agora/admin.py

19 additions and 1 deletion.

View changes Hide changes
1
1
+
2
2
3
# Register your models here.
3
-
+
4
admin.site.register(Account)
+
5
admin.site.register(AccountSettings)
+
6
admin.site.register(Post)
+
7
admin.site.register(FilePost)
+
8
#admin.site.register(Page)  # Abstract
+
9
admin.site.register(AccountPage)
+
10
admin.site.register(GroupPage)
+
11
admin.site.register(CoursePage)
+
12
admin.site.register(Group)
+
13
admin.site.register(AccountCollection)
+
14
#admin.site.register(Vote)  # Not really necessary
+
15
admin.site.register(SharedFile)
+
16
admin.site.register(Message)
+
17
#admin.site.register(Chat)  # Abstract
+
18
admin.site.register(GroupChat)
+
19
admin.site.register(PrivateChat)
+
20
admin.site.register(GroupInvite)
+
21

agora/migrations/0001_initial.py

204 additions and 0 deletions.

View changes Hide changes
+
1
+
2
import agora.models
+
3
from django.conf import settings
+
4
from django.db import migrations, models
+
5
import django.db.models.deletion
+
6
+
7
+
8
class Migration(migrations.Migration):
+
9
+
10
    initial = True
+
11
+
12
    dependencies = [
+
13
        ('administration', '0001_initial'),
+
14
    ]
+
15
+
16
    operations = [
+
17
        migrations.CreateModel(
+
18
            name='Account',
+
19
            fields=[
+
20
                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
+
21
                ('alias', models.CharField(max_length=64, unique=True)),
+
22
            ],
+
23
        ),
+
24
        migrations.CreateModel(
+
25
            name='AccountCollection',
+
26
            fields=[
+
27
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
28
                ('name', models.CharField(help_text='The name of this collection.', max_length=32)),
+
29
                ('visible_to_public', models.BooleanField(default=False, help_text='Make this collection visible to everybody.')),
+
30
                ('visible_to_collection', models.BooleanField(default=True, help_text='Make this collection visible to the accounts in this collection. Other collections are not affected by this.')),
+
31
                ('account', models.ForeignKey(editable=False, help_text='The account that created this collection.', on_delete=django.db.models.deletion.CASCADE, to='agora.Account')),
+
32
                ('accounts', models.ManyToManyField(help_text='All accounts that are part of this collection.', related_name='in_collection', to='agora.Account')),
+
33
            ],
+
34
        ),
+
35
        migrations.CreateModel(
+
36
            name='AccountSettings',
+
37
            fields=[
+
38
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
39
                ('color', models.CharField(default='E73B2B', help_text='The hexadecimal code of the color for this account.', max_length=6, validators=[agora.models.validate_hex_color])),
+
40
                ('account_page_banner', models.ImageField(help_text="The banner image to be shown on this account's homepage.", upload_to=agora.models.account_user_directory)),
+
41
                ('avatar', models.ImageField(help_text='The avatar image of this account.', upload_to=agora.models.account_user_directory)),
+
42
                ('account', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='agora.Account')),
+
43
            ],
+
44
        ),
+
45
        migrations.CreateModel(
+
46
            name='Group',
+
47
            fields=[
+
48
                ('name', models.CharField(help_text='The name of your group.', max_length=64, primary_key=True, serialize=False)),
+
49
                ('color', models.CharField(default='E73B2B', help_text='The hexadecimal code of the color for this group.', max_length=6, validators=[agora.models.validate_hex_color])),
+
50
                ('invite_only', models.BooleanField(default=True, help_text='Determines if everyone can join this group, or if only members can invite others.')),
+
51
                ('private', models.BooleanField(default=True, help_text='Determines if this group is visible to non-members.')),
+
52
                ('members', models.ManyToManyField(help_text='The members of this group.', to='agora.Account')),
+
53
            ],
+
54
        ),
+
55
        migrations.CreateModel(
+
56
            name='GroupChat',
+
57
            fields=[
+
58
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
59
                ('group', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='agora.Group')),
+
60
            ],
+
61
            options={
+
62
                'abstract': False,
+
63
            },
+
64
        ),
+
65
        migrations.CreateModel(
+
66
            name='GroupInvite',
+
67
            fields=[
+
68
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
69
                ('accepted', models.NullBooleanField(default=None, help_text='Indicates if the invitation was accepted, rejected, or pending an answer. if somebody rejects the invitation, that group can no longer send an invitation to the invitee, unless (s)he removes the answer from her history. Also, a person can not reject an invitation, and accept it later. For that, a new invitation must be received.')),
+
70
                ('group', models.ForeignKey(editable=False, help_text='The group for which this invitation is.', on_delete=django.db.models.deletion.CASCADE, to='agora.Group')),
+
71
                ('invitee', models.ForeignKey(editable=False, help_text='The account which will receive the invitation.', on_delete=django.db.models.deletion.CASCADE, related_name='invitee', to='agora.Account')),
+
72
                ('inviter', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='agora.Account')),
+
73
            ],
+
74
        ),
+
75
        migrations.CreateModel(
+
76
            name='Message',
+
77
            fields=[
+
78
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
79
                ('timestamp', models.DateTimeField(auto_now_add=True)),
+
80
                ('text', models.TextField()),
+
81
                ('sender', models.ForeignKey(editable=False, help_text='The account that sent this message.', on_delete=django.db.models.deletion.CASCADE, to='agora.Account')),
+
82
            ],
+
83
        ),
+
84
        migrations.CreateModel(
+
85
            name='Page',
+
86
            fields=[
+
87
                ('name', models.CharField(help_text='The name of this page.', max_length=64, primary_key=True, serialize=False)),
+
88
                ('created', models.DateTimeField(auto_now_add=True)),
+
89
                ('hidden', models.BooleanField(default=False, help_text='Determines if this page can be found without a direct link.')),
+
90
                ('main_content', models.TextField(blank=True, help_text='If you want to put some text on this page, you can put it here. You can use Orgmode-syntax to get as much out of your page as possible. While doing so, be aware of the limitations imposed by the code of conduct.')),
+
91
                ('public_posting', models.BooleanField(default=True, help_text='Determines if everyone can post on this page, or only the people that are linked with it. Know that if a post is made and responding is allowed, everyone can respond to that post.')),
+
92
            ],
+
93
        ),
+
94
        migrations.CreateModel(
+
95
            name='Post',
+
96
            fields=[
+
97
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
98
                ('timestamp', models.DateTimeField(auto_now_add=True)),
+
99
                ('title', models.CharField(blank=True, help_text='The title for this post.', max_length=64)),
+
100
                ('text', models.TextField(blank=True, help_text='A text message for this post. May be left blank.')),
+
101
                ('allow_votes', models.BooleanField(default=True, help_text='Decide whether to allow voting or disable it for this post.')),
+
102
                ('allow_responses', models.BooleanField(default=True, help_text='Decide if other people can respond to this post or not. This does not influence what people allow on their posts.')),
+
103
            ],
+
104
        ),
+
105
        migrations.CreateModel(
+
106
            name='PrivateChat',
+
107
            fields=[
+
108
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
109
                ('account1', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='agora.Account')),
+
110
                ('account2', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='correspondent', to='agora.Account')),
+
111
                ('messages', models.ManyToManyField(help_text='All messages that were shared in this chat.', to='agora.Message')),
+
112
            ],
+
113
            options={
+
114
                'abstract': False,
+
115
            },
+
116
        ),
+
117
        migrations.CreateModel(
+
118
            name='SharedFile',
+
119
            fields=[
+
120
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
121
                ('timestamp', models.DateTimeField(auto_now_add=True)),
+
122
                ('file', models.FileField(editable=False, help_text='The file you want to share.', upload_to='agora/chat/%Y/%m/%d/')),
+
123
                ('uploader', models.ForeignKey(editable=False, help_text='The account that uploaded this file.', on_delete=django.db.models.deletion.CASCADE, to='agora.Account')),
+
124
            ],
+
125
        ),
+
126
        migrations.CreateModel(
+
127
            name='Vote',
+
128
            fields=[
+
129
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
130
            ],
+
131
        ),
+
132
        migrations.CreateModel(
+
133
            name='AccountPage',
+
134
            fields=[
+
135
                ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='agora.Page')),
+
136
            ],
+
137
            bases=('agora.page',),
+
138
        ),
+
139
        migrations.CreateModel(
+
140
            name='CoursePage',
+
141
            fields=[
+
142
                ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='agora.Page')),
+
143
            ],
+
144
            bases=('agora.page',),
+
145
        ),
+
146
        migrations.CreateModel(
+
147
            name='FilePost',
+
148
            fields=[
+
149
                ('post_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='agora.Post')),
+
150
                ('file', models.FileField(editable=False, help_text='The file you wish to share.', upload_to='agora/posts/%Y/%m/%d/')),
+
151
                ('file_type', models.CharField(choices=[('image', 'Image'), ('video', 'Video'), ('music', 'Sound'), ('text', 'Text'), ('other', 'Other')], help_text='How this file should be seen as.', max_length=16)),
+
152
            ],
+
153
            bases=('agora.post',),
+
154
        ),
+
155
        migrations.CreateModel(
+
156
            name='GroupPage',
+
157
            fields=[
+
158
                ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='agora.Page')),
+
159
                ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='agora.Group')),
+
160
            ],
+
161
            bases=('agora.page',),
+
162
        ),
+
163
        migrations.AddField(
+
164
            model_name='vote',
+
165
            name='post',
+
166
            field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='agora.Post'),
+
167
        ),
+
168
        migrations.AddField(
+
169
            model_name='vote',
+
170
            name='voter',
+
171
            field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='agora.Account'),
+
172
        ),
+
173
        migrations.AddField(
+
174
            model_name='privatechat',
+
175
            name='shared_files',
+
176
            field=models.ManyToManyField(help_text='The files that are shared in this chat.', to='agora.SharedFile'),
+
177
        ),
+
178
        migrations.AddField(
+
179
            model_name='post',
+
180
            name='author',
+
181
            field=models.ForeignKey(editable=False, help_text='The authoring account of this post.', on_delete=django.db.models.deletion.CASCADE, to='agora.Account'),
+
182
        ),
+
183
        migrations.AddField(
+
184
            model_name='post',
+
185
            name='placed_on',
+
186
            field=models.ForeignKey(editable=False, help_text='The page on which this post was placed.', on_delete=django.db.models.deletion.CASCADE, to='agora.Page'),
+
187
        ),
+
188
        migrations.AddField(
+
189
            model_name='post',
+
190
            name='response_to',
+
191
            field=models.ForeignKey(editable=False, help_text='The post to which this was a response, if applicable.', null=True, on_delete=django.db.models.deletion.CASCADE, to='agora.Post'),
+
192
        ),
+
193
        migrations.AddField(
+
194
            model_name='groupchat',
+
195
            name='messages',
+
196
            field=models.ManyToManyField(help_text='All messages that were shared in this chat.', to='agora.Message'),
+
197
        ),
+
198
        migrations.AddField(
+
199
            model_name='groupchat',
+
200
            name='shared_files',
+
201
            field=models.ManyToManyField(help_text='The files that are shared in this chat.', to='agora.SharedFile'),
+
202
        ),
+
203
    ]
+
204

agora/migrations/0002_auto_20171118_2203.py

27 additions and 0 deletions.

View changes Hide changes
+
1
+
2
from django.db import migrations, models
+
3
import django.db.models.deletion
+
4
+
5
+
6
class Migration(migrations.Migration):
+
7
+
8
    initial = True
+
9
+
10
    dependencies = [
+
11
        ('agora', '0001_initial'),
+
12
        ('courses', '0001_initial'),
+
13
    ]
+
14
+
15
    operations = [
+
16
        migrations.AddField(
+
17
            model_name='coursepage',
+
18
            name='course',
+
19
            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='courses.Course'),
+
20
        ),
+
21
        migrations.AddField(
+
22
            model_name='accountpage',
+
23
            name='account',
+
24
            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='agora.Account'),
+
25
        ),
+
26
    ]
+
27

agora/models.py

12 additions and 3 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
    pass  # TODO
+
6
+
7
class Account(models.Model):
5
8
    user = models.OneToOneField(
6
9
        'joeni.User',
7
-
        on_delete=models.CASCADE,
+
10
        on_delete=models.CASCADE,
8
11
        primary_key=True,
9
12
        )
10
13
    alias = models.CharField(max_length=64, unique=True)
11
14
12
15
    def __str__(self):
13
16
        return self.alias
14
17
15
18
def account_user_directory(instance, filename):
16
19
    return '{0}/account/settings/{1}'.format(instace.account.alias, filename)
17
20
18
21
class AccountSettings(models.Model):
19
22
    account = models.OneToOneField(
20
23
        'Account',
21
24
        on_delete=models.CASCADE,
22
25
        )
23
26
    # TODO: Build validator for primary_color to make sure what is given is a
24
27
    # valid hexadecimal RGB value.
25
28
    color = models.CharField(
26
29
        max_length=6,
27
30
        help_text=_("The hexadecimal code of the color for this account."),
28
31
        default = constants.COLORS["UHasselt default"],
29
32
        blank=False,
30
33
        validators=[validate_hex_color],
31
34
        )
32
35
    account_page_banner = models.ImageField(  # Requires the Pillow library!
33
36
        upload_to=account_user_directory,
34
37
        help_text=_("The banner image to be shown on this account's homepage."),
35
38
        )
36
39
    avatar = models.ImageField(
37
40
        upload_to=account_user_directory,
38
41
        help_text=_("The avatar image of this account."),
39
42
        )
40
43
41
44
    def __str__(self):
42
45
        return str(self.account)
43
46
44
47
class Post(models.Model):
45
48
    timestamp = models.DateTimeField(auto_now_add=True)
46
49
    title = models.CharField(
47
50
        max_length=64,
48
51
        blank=True,
49
52
        help_text=_("The title for this post."),
50
53
        )
51
54
    text = models.TextField(
52
55
        blank=True,
53
56
        help_text=_("A text message for this post. May be left blank."),
54
57
        )
55
58
    author = models.ForeignKey(
56
59
        "Account",
57
60
        on_delete=models.CASCADE,
58
61
        null=False,  # There must be an author
59
62
        editable=False,  # It makes no sense to change the author after creation
60
63
        help_text=_("The authoring account of this post."),
61
64
        )
62
65
    response_to = models.ForeignKey(
63
66
        "self",
64
67
        on_delete=models.CASCADE,
65
68
        null=True,  # If this is null, this post is not a response, but a beginning post
66
69
        editable=False,  # This cannot be changed after creation, wouldn't make sense
67
70
        help_text=_("The post to which this was a response, if applicable."),
68
71
        )
69
72
    placed_on = models.ForeignKey(
70
73
        "Page",
71
74
        on_delete=models.CASCADE,
72
75
        null=False,
73
76
        editable=False,
74
77
        help_text=_("The page on which this post was placed."),
75
78
        )
76
79
    # Voting fields
77
80
    allow_votes = models.BooleanField(
78
81
        default=True,
79
82
        help_text=_("Decide whether to allow voting or disable it for this post."),
80
83
        )
81
84
    allow_responses = models.BooleanField(
82
85
        default=True,
83
86
        help_text=_("Decide if other people can respond to this post or not. "
84
87
                    "This does not influence what people allow on their posts."),
85
88
        )
86
89
87
90
    def __str__(self):
88
91
        return str(self.timestamp) + " | " + str(self.author)
89
92
    # TODO Add a way to attach geographical data to a post, which could
90
93
    # then be used with OpenStreetMap or something
91
94
92
95
class FilePost(Post):
93
96
    """ A special type of Post, which has a file linked with it.
94
97
    The poster can specify how to treat this file. """
95
98
    image = _("Image")
96
99
    video = _("Video")
97
100
    music = _("Sound")
98
101
    text  = _("Text" )
99
102
    other = _("Other")
100
103
    file = models.FileField(
101
104
        upload_to="agora/posts/%Y/%m/%d/",
102
105
        null=False,
103
106
        editable=False,
104
107
        help_text=_("The file you wish to share."),
105
108
        )
106
109
    file_type = models.CharField(
107
110
        max_length=16,
108
111
        blank=False,
109
112
        choices = (
110
113
            ('image', image),
111
114
            ('video', video),
112
115
            ('music', music),
113
116
            ('text' , text ),
114
117
            ('other', other),
115
118
            ),
116
119
        help_text=_("How this file should be seen as."),
117
120
        )
118
121
119
122
class Page(models.Model):
120
123
    """ In the university, people can create pages for everything they want and
121
124
    then some. These pages are put in the database through this table. """
122
125
    name = models.CharField(
123
126
        max_length=64,
124
127
        primary_key=True,
125
128
        blank=False,
126
129
        help_text=_("The name of this page."),
127
130
        )
128
131
    created = models.DateTimeField(auto_now_add=True)
129
132
    hidden = models.BooleanField(
130
133
        default=False,
131
134
        help_text=_("Determines if this page can be found without a direct link."),
132
135
        )
133
136
    main_content = models.TextField(
134
137
        blank=True,
135
138
        help_text=_("If you want to put some text on this page, "
136
139
                    "you can put it here. You can use Orgmode-syntax to "
137
140
                    "get as much out of your page as possible. While doing so, "
138
141
                    "be aware of the limitations imposed by the code of conduct."),
139
142
        )
140
143
    public_posting = models.BooleanField(
141
144
        default=True,
142
145
        help_text=_("Determines if everyone can post on this page, or only the "
143
146
                    "people that are linked with it. Know that if a post is made "
144
147
                    "and responding is allowed, everyone can respond to that post."),
145
148
        )
146
149
147
150
    class Meta:
148
-
        abstract=True
149
-
+
151
        abstract=True"""
+
152
    """ I've disabled the abstraction, because otherwise I can't relate a Post to a Page. """
+
153
150
154
class AccountPage(Page):
151
155
    """ Every account has its own homepage. This is that page.
152
156
    This page can only be edited by the account holder, or staff members. """
153
157
    # TODO: Find a way to auto-create one of these every time a new account is created
154
158
    # TODO: Require that changes can only occur by either the account holder or staff
155
159
    account = models.OneToOneField(
156
160
        "Account",
157
161
        null=False,
158
162
        on_delete=models.CASCADE,
159
163
        )
160
164
161
165
class GroupPage(Page):
162
166
    """ A page where a group can present itself to the university.
163
167
    This page can only be edited by group members or staff members. """
164
168
    # TODO: Find a way to auto-create one of these every time a new group is created
165
169
    # TODO: Require that changes can only occur by either the group or staff
166
170
    group = models.ForeignKey(
167
171
        "Group",
168
172
        null=False,
169
173
        on_delete=models.CASCADE,
170
174
        )
171
175
172
176
class CoursePage(Page):
173
177
    """ A page that serves as a course's main entry point.
174
178
    This page can only be edited by the course's educating team or staff members. """
175
179
    # TODO: Find a way to auto-create one of these every time a new course is created
176
180
    # TODO: Require that changes can only occur by either the course team or staff
177
181
    course = models.OneToOneField(
178
182
        "courses.Course",
179
183
        null=False,
180
184
        on_delete=models.CASCADE,
181
185
        )
182
186
183
187
class Group(models.Model):
184
188
    """ It is imperative that everyone can come together with other people.
185
189
    A Group record is the way to accomplish this. """
186
190
    name = models.CharField(
187
191
        max_length=64,
188
192
        primary_key=True,  # So be unique I'd say
189
193
        blank=False,
190
194
        help_text=_("The name of your group."),
191
195
        )
192
196
    color = models.CharField(
193
197
        max_length=6,
194
198
        help_text=_("The hexadecimal code of the color for this group."),
195
199
        default = constants.COLORS["UHasselt default"],
196
200
        blank=False,
197
201
        validators=[validate_hex_color],
198
202
        )
199
203
    members = models.ManyToManyField(
200
204
        "Account",
201
205
        help_text=_("The members of this group."),
202
206
        )
203
207
    invite_only = models.BooleanField(
204
208
        default=True,
205
209
        help_text=_("Determines if everyone can join this group, or if "
206
210
                    "only members can invite others."),
207
211
        )
208
212
    private = models.BooleanField(
209
213
        default=True,
210
214
        help_text=_("Determines if this group is visible to non-members."),
211
215
        )
212
216
213
217
    def __str__(self):
214
218
        return self.name
215
219
216
220
217
221
class AccountCollection(models.Model):
218
222
    """ Every account can make a collection in which (s)he can list accounts
219
223
    at his/her wish. This can be a collection of Friends, study collegues,
220
224
    project partners, and so on.
221
225
    Accounts that are in a certain collection are not notified of this.
222
226
    However, there is one exception:
223
227
    If both accounts have a collection named "Friends" (or the localized
224
228
    equivalent), and both feature each other in that collection, then
225
229
    this is shared between the two accounts. """
226
230
    account = models.ForeignKey(
227
231
        "Account",
228
232
        null=False,
229
233
        editable=False,
230
234
        on_delete=models.CASCADE,
231
235
        help_text=_("The account that created this collection."),
232
236
        )
233
237
    name = models.CharField(
234
238
        max_length=32,
235
239
        blank=False,
236
240
        help_text=_("The name of this collection."),
237
241
        )
238
242
    accounts = models.ManyToManyField(
239
243
        "Account",
240
244
        help_text=_("All accounts that are part of this collection."),
241
245
        )
+
246
        )
242
247
    visible_to_public = models.BooleanField(
243
248
        default=False,
244
249
        help_text=_("Make this collection visible to everybody."),
245
250
        )
246
251
    visible_to_collection = models.BooleanField(
247
252
        default=True,
248
253
        help_text=_("Make this collection visible to the accounts in this collection. Other collections are not affected by this."),
249
254
        )
250
255
251
256
    def __str__(self):
252
257
        return str(self.account) + " | " + self.name
253
258
254
259
class Vote(models.Model):
255
260
    """ Accounts can vote on posts (using ▲, which is funny because UHasselt).
256
261
    These votes are registered in this table. """
257
262
    voter = models.ForeignKey(
258
263
        "Account",
259
264
        null=False,
260
265
        editable=False,
261
266
        on_delete=models.CASCADE,
262
267
        )
263
268
    post = models.ForeignKey(
264
269
        "Post",
265
270
        null=False,  # Duh.
266
271
        editable=False,  # Transferring votes doesn't make sense
267
272
        on_delete=models.CASCADE,
268
273
        )
269
274
270
275
class SharedFile(models.Model):
271
276
    """ Groups and people can share files with each other, through a chat system.
272
277
    These files are represented here. """
273
278
    timestamp = models.DateTimeField(auto_now_add=True)
274
279
    file = models.FileField(
275
280
        upload_to="agora/chat/%Y/%m/%d/",
276
281
        null=False,
277
282
        editable=False,
278
283
        help_text=_("The file you want to share."),
279
284
        )
280
285
    uploader = models.ForeignKey(
281
286
        "Account",
282
287
        on_delete=models.CASCADE,
283
288
        null=False,
284
289
        editable=False,
285
290
        help_text=_("The account that uploaded this file."),
286
291
        )
287
292
    # TODO __str__ ... or maybe not?
288
293
289
294
class Message(models.Model):
290
295
    """ Everyone can communicate with someone else using private messages.
291
296
    These messages are recorded here. """
292
297
    timestamp = models.DateTimeField(auto_now_add=True)
293
298
    text = models.TextField(
294
299
        blank=False,
295
300
        )
296
301
    sender = models.ForeignKey(
297
302
        "Account",
298
303
        on_delete=models.CASCADE,
299
304
        null=False,
300
305
        editable=False,
301
306
        help_text=_("The account that sent this message."),
302
307
        )
303
308
    # TODO __str__
304
309
305
310
306
311
class Chat(models.Model):
307
312
    """ Chats can happen between a group, or between two people in private.
308
313
    These messages are connected to a particular chat. """
309
314
    messages = models.ManyToManyField(
310
315
        "Message",
311
316
        help_text=_("All messages that were shared in this chat."),
312
317
        )
313
318
    shared_files = models.ManyToManyField(
314
319
        "SharedFile",
315
320
        help_text=_("The files that are shared in this chat."),
316
321
        )
317
322
    class Meta:
318
323
        abstract=True
319
324
320
325
class GroupChat(Chat):
321
326
    """ Represents a chat of a group. """
322
327
    group = models.ForeignKey(
323
328
        "Group",
324
329
        on_delete=models.CASCADE,
325
330
        null=False,
326
331
        editable=False,
327
332
        )
328
333
329
334
    def __str__(self):
330
335
        return str(self.group)
331
336
332
337
class PrivateChat(Chat):
333
338
    """ Represents a private chat between two accounts. """
334
339
    # FIXME: It's theoretically possible to start a chat by one person, and have
335
340
    # the other person start the same as well. Find a reliable way to block that.
336
341
    account1 = models.ForeignKey(
337
342
        "Account",
338
343
        on_delete=models.CASCADE,
339
344
        null=False,
340
345
        editable=False,
341
346
        )
342
347
    account2 = models.ForeignKey(
343
348
        "Account",
344
349
        on_delete=models.CASCADE,
345
350
        null=False,
346
351
        editable=False,
347
352
        )
+
353
        )
348
354
    def __str__(self):
+
355
    def __str__(self):
349
356
        return str(self.account1) +" - "+ str(self.account2)
350
357
351
358
class GroupInvite(models.Model):
352
359
    """ Groups can send invites to other people to join the group, and this is
353
360
    not limited to private groups. Group invitations are stored here, as well as their outcome. """
354
361
    inviter = models.ForeignKey(
355
362
        "Account",
356
363
        on_delete=models.CASCADE,
357
364
        null=False,
358
365
        editable=False,
359
366
        )
360
367
    invitee = models.ForeignKey(
361
368
        "Account",
362
369
        on_delete=models.CASCADE,
363
370
        null=False,
364
371
        editable=False,
365
372
        help_text=_("The account which will receive the invitation."),
366
373
        )
+
374
        )
367
375
    group = models.ForeignKey(
368
376
        "Group",
369
377
        on_delete=models.CASCADE,
370
378
        null=False,
371
379
        editable=False,
372
380
        db_index=True,
373
381
        help_text=_("The group for which this invitation is."),
374
382
        )
375
383
    accepted = models.NullBooleanField(
376
384
        default=None,
377
385
        help_text=_("Indicates if the invitation was accepted, rejected, or "
378
386
                    "pending an answer. if somebody rejects the invitation, "
379
387
                    "that group can no longer send an invitation to the invitee, "
380
388
                    "unless (s)he removes the answer from her history. Also, "
381
389
                    "a person can not reject an invitation, and accept it later. "
382
390
                    "For that, a new invitation must be received."),
383
391
        )
384
392
385
393
    def __str__(self):
386
394
        return str(self.invitee) +" | "+ str(self.group)
387
395
+
396

courses/admin.py

10 additions and 1 deletion.

View changes Hide changes
1
1
+
2
2
3
# Register your models here.
3
-
+
4
admin.site.register(Prerequisites)
+
5
admin.site.register(ProgrammeInformation)
+
6
admin.site.register(Study)
+
7
admin.site.register(StudyProgramme)
+
8
admin.site.register(Assignment)
+
9
admin.site.register(Announcement)
+
10
admin.site.register(Upload)
+
11
admin.site.register(StudyGroup)
+
12

courses/migrations/0001_initial.py

145 additions and 0 deletions.

View changes Hide changes
+
1
+
2
from django.conf import settings
+
3
from django.db import migrations, models
+
4
import django.db.models.deletion
+
5
+
6
+
7
class Migration(migrations.Migration):
+
8
+
9
    initial = True
+
10
+
11
    dependencies = [
+
12
        ('agora', '0001_initial'),
+
13
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+
14
    ]
+
15
+
16
    operations = [
+
17
        migrations.CreateModel(
+
18
            name='Announcement',
+
19
            fields=[
+
20
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
21
                ('title', models.CharField(help_text='A quick title for what this is about.', max_length=20)),
+
22
                ('text', models.TextField(help_text='The announcement itself. Orgmode syntax available.')),
+
23
                ('posted', models.DateTimeField(auto_now_add=True)),
+
24
            ],
+
25
        ),
+
26
        migrations.CreateModel(
+
27
            name='Assignment',
+
28
            fields=[
+
29
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
30
                ('information', models.TextField(help_text='Any additional information regarding the assignment. Orgmode syntax available.')),
+
31
                ('deadline', models.DateTimeField(help_text='The date and time this task is due.')),
+
32
                ('posted', models.DateField(auto_now_add=True)),
+
33
                ('digital_task', models.BooleanField(default=True, help_text='This determines whether this assignment requires handing in a digital file.')),
+
34
            ],
+
35
        ),
+
36
        migrations.CreateModel(
+
37
            name='Course',
+
38
            fields=[
+
39
                ('number', models.PositiveSmallIntegerField(help_text="The number associated with this course. A leading '0' will be added if the number is smaller than 1000.", primary_key=True, serialize=False)),
+
40
                ('name', models.CharField(help_text='The name of this course, in the language that it is taught. Translations are for the appropriate template.', max_length=64)),
+
41
                ('language', models.CharField(choices=[('NL', 'Dutch'), ('EN', 'English'), ('FR', 'French')], help_text='The language in which this course is given.', max_length=64)),
+
42
                ('contact_person', models.ForeignKey(help_text='The person to contact regarding this course.', limit_choices_to={'is_staff': True}, on_delete=django.db.models.deletion.PROTECT, related_name='contact_person', to=settings.AUTH_USER_MODEL)),
+
43
                ('coordinator', models.ForeignKey(help_text="The person whom's the coordinator of this course.", limit_choices_to={'is_staff': True}, on_delete=django.db.models.deletion.PROTECT, related_name='coordinator', to=settings.AUTH_USER_MODEL)),
+
44
                ('educating_team', models.ManyToManyField(help_text='The team members of this course.', limit_choices_to={'is_staff': True}, related_name='educating_team', to=settings.AUTH_USER_MODEL)),
+
45
            ],
+
46
        ),
+
47
        migrations.CreateModel(
+
48
            name='Prerequisites',
+
49
            fields=[
+
50
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
51
                ('name', models.CharField(blank=True, help_text='To specify a name for this set, if necessary.', max_length=64)),
+
52
                ('ECTS_for_required_study', models.PositiveSmallIntegerField(help_text='The amount of obtained ECTS points for the required course, if any.', null=True)),
+
53
                ('course', models.ForeignKey(help_text='The course that these prerequisites are for.', on_delete=django.db.models.deletion.CASCADE, related_name='prerequisite_course', to='courses.Course')),
+
54
                ('in_curriculum', models.ManyToManyField(help_text='All courses that have to be in the curriculum to follow this. If a credit was achieved, that course can be omitted.', related_name='in_curriculum', to='courses.Course')),
+
55
            ],
+
56
        ),
+
57
        migrations.CreateModel(
+
58
            name='ProgrammeInformation',
+
59
            fields=[
+
60
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
61
                ('programme_type', models.CharField(choices=[('C', 'Compulsory'), ('O', 'Optional')], help_text='Type of this course for this study.', max_length=1)),
+
62
                ('study_hours', models.PositiveSmallIntegerField(help_text='The required amount of hours to study this course.')),
+
63
                ('ECTS', models.PositiveSmallIntegerField(help_text='The amount of ECTS points attached to this course.')),
+
64
                ('semester', models.PositiveSmallIntegerField(choices=[(1, 'First semester'), (2, 'Second semester'), (3, 'Full year course'), (4, 'Taught in first quarter'), (5, 'Taught in second quarter'), (6, 'Taught in third quarter'), (7, 'Taught in fourth quarter')], help_text='The period in which this course is being taught in this study.')),
+
65
                ('year', models.PositiveSmallIntegerField(help_text='The year in which this course is taught for this study.')),
+
66
                ('second_chance', models.BooleanField(default=True, help_text='Defines if a second chance exam is planned for this course.')),
+
67
                ('tolerable', models.BooleanField(default=True, help_text='Defines if a failed result can be tolerated.')),
+
68
                ('scoring', models.CharField(choices=[('N', 'Numerical'), ('FP', 'Fail/Pass')], default='N', help_text='How the obtained score for this course is given.', max_length=2)),
+
69
                ('course', models.ForeignKey(help_text='The course that this information is for.', on_delete=django.db.models.deletion.CASCADE, to='courses.Course')),
+
70
            ],
+
71
        ),
+
72
        migrations.CreateModel(
+
73
            name='Study',
+
74
            fields=[
+
75
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
76
                ('name', models.CharField(help_text="The full name of this study, in the language it's taught in.", max_length=128, unique=True)),
+
77
                ('degree_type', models.CharField(choices=[('BSc', 'Bachelor of Science'), ('MSc', 'Master of Science'), ('LL.B', 'Bachelor of Laws'), ('LL.M', 'Master of Laws'), ('ir.', 'Engineer'), ('ing.', 'Technological Engineer')], help_text='The type of degree one obtains upon passing this study.', max_length=64)),
+
78
                ('language', models.CharField(choices=[('NL', 'Dutch'), ('EN', 'English'), ('FR', 'French')], help_text='The language in which this study is given.', max_length=64)),
+
79
                ('faculty', models.CharField(choices=[('FoS', 'Faculty of Sciences'), ('FoTS', 'Faculty of Transportation Sciences'), ('FoAaA', 'Faculty of Architecture and Arts'), ('FoBE', 'Faculty of Business Economics'), ('FoMaLS', 'Faculty of Medicine and Life Sciences'), ('FoET', 'Faculty of Engineering Technology'), ('FoL', 'Faculty of Law')], help_text='The faculty where this study belongs to.', max_length=6)),
+
80
                ('additional_members', models.ManyToManyField(help_text='All the other members of the exam committee.', limit_choices_to={'is_staff': True}, related_name='additional_members', to=settings.AUTH_USER_MODEL)),
+
81
                ('chairman', models.ForeignKey(help_text='The chairman of this study.', limit_choices_to={'is_staff': True}, on_delete=django.db.models.deletion.PROTECT, related_name='chairman', to=settings.AUTH_USER_MODEL)),
+
82
                ('ombuds', models.ForeignKey(help_text='The ombuds person of this study.', limit_choices_to={'is_staff': True}, on_delete=django.db.models.deletion.PROTECT, related_name='ombuds', to=settings.AUTH_USER_MODEL)),
+
83
                ('secretary', models.ForeignKey(help_text='The secretary of this study.', limit_choices_to={'is_staff': True}, on_delete=django.db.models.deletion.PROTECT, related_name='secretary', to=settings.AUTH_USER_MODEL)),
+
84
                ('vice_chairman', models.ForeignKey(help_text='The vice-chairman of this study.', limit_choices_to={'is_staff': True}, on_delete=django.db.models.deletion.PROTECT, related_name='vice_chairman', to=settings.AUTH_USER_MODEL)),
+
85
                ('vice_ombuds', models.ForeignKey(help_text='The (replacing) ombuds person of this study.', limit_choices_to={'is_staff': True}, on_delete=django.db.models.deletion.PROTECT, related_name='vice_ombuds', to=settings.AUTH_USER_MODEL)),
+
86
            ],
+
87
        ),
+
88
        migrations.CreateModel(
+
89
            name='StudyGroup',
+
90
            fields=[
+
91
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
92
                ('course', models.ForeignKey(editable=False, help_text='The course for which this group is.', on_delete=django.db.models.deletion.CASCADE, to='courses.Course')),
+
93
                ('group', models.ForeignKey(editable=False, help_text='The group that will be seen as the study group.', on_delete=django.db.models.deletion.PROTECT, to='agora.Group')),
+
94
            ],
+
95
        ),
+
96
        migrations.CreateModel(
+
97
            name='StudyProgramme',
+
98
            fields=[
+
99
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
100
                ('name', models.CharField(help_text='The name of this programme.', max_length=64)),
+
101
            ],
+
102
        ),
+
103
        migrations.CreateModel(
+
104
            name='Upload',
+
105
            fields=[
+
106
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
107
                ('upload_time', models.DateTimeField(auto_now_add=True)),
+
108
                ('comment', models.TextField(blank=True, help_text='If you wish to add an additional comment, state it here.')),
+
109
                ('file', models.FileField(editable=False, help_text='The file you want to upload for this assignment.', upload_to='assignments/uploads/%Y/%m/')),
+
110
                ('assignment', models.ForeignKey(editable=False, help_text='For which assignment this upload is.', limit_choices_to={'digital_task': True}, on_delete=django.db.models.deletion.CASCADE, to='courses.Assignment')),
+
111
                ('student', models.ForeignKey(editable=False, help_text='The student who handed this in.', limit_choices_to={'is_student': True}, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+
112
            ],
+
113
        ),
+
114
        migrations.AddField(
+
115
            model_name='programmeinformation',
+
116
            name='study',
+
117
            field=models.ForeignKey(help_text='The study in which the course is taught.', on_delete=django.db.models.deletion.CASCADE, to='courses.Study'),
+
118
        ),
+
119
        migrations.AddField(
+
120
            model_name='programmeinformation',
+
121
            name='study_programme',
+
122
            field=models.ForeignKey(help_text='The study programme that this course belongs to.', on_delete=django.db.models.deletion.CASCADE, to='courses.StudyProgramme'),
+
123
        ),
+
124
        migrations.AddField(
+
125
            model_name='prerequisites',
+
126
            name='required_study',
+
127
            field=models.ForeignKey(help_text='If one must have a certain amount of obtained ECTS points for a particular course, state that course here.', null=True, on_delete=django.db.models.deletion.CASCADE, to='courses.Study'),
+
128
        ),
+
129
        migrations.AddField(
+
130
            model_name='prerequisites',
+
131
            name='sequentialities',
+
132
            field=models.ManyToManyField(help_text="All courses for which a credit must've been received in order to follow the course.", related_name='sequentialities', to='courses.Course'),
+
133
        ),
+
134
        migrations.AddField(
+
135
            model_name='assignment',
+
136
            name='course',
+
137
            field=models.ForeignKey(editable=False, help_text='The course for which this task is assigned.', on_delete=django.db.models.deletion.CASCADE, to='courses.Course'),
+
138
        ),
+
139
        migrations.AddField(
+
140
            model_name='announcement',
+
141
            name='course',
+
142
            field=models.ForeignKey(editable=False, help_text='The course for which this announcement is made.', on_delete=django.db.models.deletion.CASCADE, to='courses.Course'),
+
143
        ),
+
144
    ]
+
145

courses/models.py

38 additions and 19 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
-
        on_delete=models.PROTECT,  # A course must have a contact person
+
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
23
    coordinator = models.ForeignKey(
23
24
        "joeni.user",
24
-
        on_delete=models.PROTECT,  # A course must have a coordinator
+
25
        on_delete=models.PROTECT,  # A course must have a coordinator
25
26
        limit_choices_to={'is_staff': True},
26
27
        null=False,
27
28
        help_text=_("The person whom's the coordinator of this course."),
28
29
        )
+
30
        )
29
31
    educating_team = models.ManyToManyField(
30
32
        "joeni.user",
31
-
        # No on_delete, since M->M cannot be required at database level
+
33
        # No on_delete, since M->M cannot be required at database level
32
34
        limit_choices_to={'is_staff': True},
33
35
        null=False,
34
36
        help_text=_("The team members of this course."),
35
37
        )
+
38
        )
36
39
    language = models.CharField(
37
40
        max_length=64,
38
41
        choices = (
39
42
            ('NL', _("Dutch")),
40
43
            ('EN', _("English")),
41
44
            ('FR', _("French")),
42
45
            ),
43
46
        null=False,
44
47
        help_text=_("The language in which this course is given."),
45
48
        )
46
49
    requirements = models.ManyToManyField()
47
-
48
50
    def __str__(self):
49
51
        number = str(self.number)
50
52
        for i in [10,100,1000]:
51
53
            if self.number < i:
52
54
                number = "0" + number
53
55
        return "(" + number + ") " + self.name
54
56
55
57
56
58
class Prerequisites(models.Model):
57
59
    """ Represents a collection of prerequisites a student must have obtained
58
60
    before being allowed to partake in this course.
59
61
    It's possible that, if a student has obtained credits in a certain set of
60
62
    courses, a certain part of the prerequisites do not have to be obtained.
61
63
    Because of this, make a different record for each different set. In other
62
64
    words: If one set of prerequisites is obtained, and another one isn't, BUT
63
65
    they point to the same course, the student is allowed to partake. """
64
66
    course = models.ForeignKey(
65
67
        "Course",
66
68
        on_delete=models.CASCADE,
67
69
        null=False,
68
70
        help_text=_("The course that these prerequisites are for."),
69
71
        )
+
72
        )
70
73
    name = models.ForeignKey(
71
-
        blank=True,
+
74
        max_length=64,
+
75
        blank=True,
72
76
        help_text=_("To specify a name for this set, if necessary."),
73
77
        )
74
78
    sequentialities = models.ManyToManyField(
75
79
        "Course",
76
80
        help_text=_("All courses for which a credit must've been received in order to follow the course."),
77
81
        )
+
82
        )
78
83
    in_curriculum = models.ManyToManyField(
79
84
        "Course",
80
85
        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
86
        )
+
87
        )
82
88
    required_study = models.ForeignKey(
83
89
        "Study",
84
90
        on_delete=models.CASCADE,
85
91
        null=True,
86
92
        help_text=_("If one must have a certain amount of obtained ECTS points for a particular course, state that course here."),
87
93
        )
88
94
    ECTS_for_required_study = models.PositiveSmallIntegerField(
89
95
        null=True,
90
96
        help_text=_("The amount of obtained ECTS points for the required course, if any."),
91
97
        )
92
98
93
99
    def __str__(self):
94
100
        if self.name == "":
95
101
            return _("Prerequisites for %(course)s") % {'course': str(self.course)}
96
102
        else:
97
103
            return self.name + " | " + str(self.course)
98
104
99
105
100
106
class ProgrammeInformation(models.Model):
101
107
    """ It's possible that a course is taught in multiple degree programmes; For
102
108
    example: Calculus can easily be taught to physics and mathematics students
103
109
    alike. In this table, these relations are set up, and the related properties
104
110
    are defined as well. """
105
111
    study = models.ForeignKey(
106
112
        "Study",
107
113
        on_delete=models.CASCADE,
108
114
        null=False,
109
115
        help_text=_("The study in which the course is taught."),
110
116
        )
111
117
    course = models.ForeignKey(
112
118
        "Course",
113
119
        on_delete=models.CASCADE,
114
120
        null=False,
115
121
        help_text=_("The course that this information is for."),
116
122
        )
117
123
    study_programme = models.ForeignKey(
118
124
        "StudyProgramme",
119
125
        on_delete=models.CASCADE,
120
126
        null=False,
121
127
        help_text=_("The study programme that this course belongs to."),
122
128
        )
123
129
    programme_type = models.CharField(
124
130
        max_length=1,
125
131
        blank=False,
126
132
        choices = (
127
133
            ('C', _("Compulsory")),
128
134
            ('O', _("Optional")),
129
135
            ),
130
136
        help_text=_("Type of this course for this study."),
131
137
        )
132
138
    study_hours = models.PositiveSmallIntegerField(
133
139
        blank=False,
134
140
        help_text=_("The required amount of hours to study this course."),
135
141
        )
136
142
    ECTS = models.PositiveSmallIntegerField(
137
143
        blank=False,
138
144
        help_text=_("The amount of ECTS points attached to this course."),
139
145
        )
140
146
    semester = models.PositiveSmallIntegerField(
141
147
        blank=False,
142
148
        choices = (
143
149
            (1, _("First semester")),
144
150
            (2, _("Second semester")),
145
151
            (3, _("Full year course")),
146
152
            (4, _("Taught in first quarter")),
147
153
            (5, _("Taught in second quarter")),
148
154
            (6, _("Taught in third quarter")),
149
155
            (7, _("Taught in fourth quarter")),
150
156
            ),
151
157
        help_text=_("The period in which this course is being taught in this study."),
152
158
        )
153
159
    year = models.PositiveSmallIntegerField(
154
160
        blank=False,
155
161
        help_text=_("The year in which this course is taught for this study."),
156
162
        )
157
163
    second_chance = models.BooleanField(
158
164
        default=True,
159
165
        help_text=_("Defines if a second chance exam is planned for this course."),
160
166
        )
161
167
    tolerable = models.BooleanField(
162
168
        default=True,
163
169
        help_text=_("Defines if a failed result can be tolerated."),
164
170
        )
165
171
    scoring = models.CharField(
166
172
        max_length=2,
167
173
        choices = (
168
174
            ('N', _("Numerical")),
169
175
            ('FP', _("Fail/Pass")),
170
176
            ),
171
177
        default='N',
172
178
        blank=False,
173
179
        help_text=_("How the obtained score for this course is given."),
174
180
        )
175
181
176
182
    def __str__(self):
177
183
        return str(self.study) + " - " + str(self.course)
178
184
179
185
class Study(models.Model):
180
186
    """ Defines a certain study that can be followed at the university.
181
187
    This also includes abridged study programmes, like transition programmes.
182
188
    Other information, such as descriptions, are kept in the template file
183
189
    of this study, which can be manually edited. Joeni searches for a file
184
190
    with the exact name as the study + ".html". So if the study is called
185
191
    "Bachelor of Informatics", it will search for "Bachelor of Informatics.html".
186
192
    """
187
193
    # Degree types
188
194
    BSc = _("Bachelor of Science")
189
195
    Msc = _("Master of Science")
190
-
    LLB = _("Bachelor of Laws")
+
196
    LLB = _("Bachelor of Laws")
191
197
    LLM = _("Master of Laws")
192
198
    ir  = _("Engineer")
193
199
    ing = _("Technological Engineer")
194
200
    # Faculties
195
201
    FoMaLS = _("Faculty of Medicine and Life Sciences")
196
202
    Fos    = _("Faculty of Sciences")
197
-
    FoTS   = _("Faculty of Transportation Sciences")
+
203
    FoTS   = _("Faculty of Transportation Sciences")
198
204
    FoAaA  = _("Faculty of Architecture and Arts")
199
205
    FoBE   = _("Faculty of Business Economics")
200
206
    FoET   = _("Faculty of Engineering Technology")
201
207
    FoL    = _("Faculty of Law")
202
208
203
209
    name = models.CharField(
204
210
        max_length=128,
205
211
        blank=False,
206
212
        unique=True,
207
213
        help_text=_("The full name of this study, in the language it's taught in."),
208
214
        )
209
215
    degree_type = models.CharField(
210
216
        max_length=64,
211
217
        choices = (
212
218
            ('BSc', Bsc),
213
-
            ('MSc', Msc),
214
-
            ('LL.B', LLB),
+
219
            ('MSc', MSc),
+
220
            ('LL.B', LLB),
215
221
            ('LL.M', LLM),
216
222
            ('ir.', ir ),
217
223
            ('ing.',ing),
218
224
            ),
219
225
        blank=False,
220
226
        help_text=_("The type of degree one obtains upon passing this study."),
221
227
        )
222
228
    language = models.CharField(
223
229
        max_length=64,
224
230
        choices = (
225
231
            ('NL', _("Dutch")),
226
232
            ('EN', _("English")),
227
233
            ('FR', _("French")),
228
234
            ),
229
235
        null=False,
230
236
        help_text=_("The language in which this study is given."),
231
237
        )
232
238
    # Information about exam committee
233
239
    chairman = models.ForeignKey(
234
240
        "Joeni.users",
235
-
        on_delete=models.PROTECT,
+
241
        on_delete=models.PROTECT,
236
242
        null=False,
237
243
        limit_choices_to={'is_staff': True},
238
244
        help_text=_("The chairman of this study."),
239
245
        )
+
246
        )
240
247
    vice_chairman = models.ForeignKey(
241
248
        "Joeni.users",
242
-
        on_delete=models.PROTECT,
+
249
        on_delete=models.PROTECT,
243
250
        null=False,
244
251
        help_text=_("The vice-chairman of this study."),
245
252
        limit_choices_to={'is_staff': True},
246
253
        )
+
254
        )
247
255
    secretary = models.ForeignKey(
248
256
        "Joeni.users",
249
-
        on_delete=models.PROTECT,
+
257
        on_delete=models.PROTECT,
250
258
        null=False,
251
259
        help_text=_("The secretary of this study."),
252
260
        limit_choices_to={'is_staff': True},
253
261
        )
+
262
        )
254
263
    ombuds = models.ForeignKey(
255
264
        "Joeni.users",
256
-
        on_delete=models.PROTECT,
+
265
        on_delete=models.PROTECT,
257
266
        null=False,
258
267
        help_text=_("The ombuds person of this study."),
259
268
        limit_choices_to={'is_staff': True},
260
269
        )
+
270
        )
261
271
    vice_ombuds = models.ForeignKey(
262
272
        "Joeni.users",
263
-
        on_delete=models.PROTECT,
+
273
        on_delete=models.PROTECT,
264
274
        null=False,
265
275
        help_text=_("The (replacing) ombuds person of this study."),
266
276
        limit_choices_to={'is_staff': True},
267
277
        )
+
278
        )
268
279
    additional_members = models.ManyToManyField(
269
280
        "Joeni.users",
270
-
        help_text=_("All the other members of the exam committee."),
+
281
        help_text=_("All the other members of the exam committee."),
271
282
        limit_choices_to={'is_staff': True},
272
283
        )
+
284
        )
273
285
    faculty = models.CharField(
274
286
        max_length=6,
275
287
        choices = (
276
288
            ('FoS', FoS),
277
289
            ('FoTS', FoTS),
278
290
            ('FoAaA', FoAaA),
279
291
            ('FoBE', FoBE),
280
292
            ('FoMaLS', FoMaLS),
281
293
            ('FoET', FoET),
282
294
            ('FoL', FoL),
283
295
            ),
284
296
        blank=False,
285
297
        help_text=_("The faculty where this study belongs to."),
286
298
        )
287
299
288
300
    #def study_points(self):
289
301
        """ Returns the amount of study points for this year.
290
-
        This value is inferred based on the study programme information
+
302
        This value is inferred based on the study programme information
291
303
        records that lists this study as their foreign key. """
292
304
        #total_ECTS = 0
293
305
        #for course in ProgrammeInformation.objects.filter(study=self):
294
306
            #total_ECTS += course.ECTS
295
307
        #return total_ECTS
296
308
    # XXX: Commented because this is actually something for the StudyProgramme
297
309
    def years(self):
298
310
        """ Returns the amount of years this study takes.
299
311
        This value is inferred based on the study programme information
300
312
        records that lists this study as their foreign key. """
301
313
        highest_year = 0
302
314
        for course in ProgrammeInformation.objects.filter(study=self):
303
315
            highest_year = max(highest_year, course.year)
304
316
        return highest_year
305
317
306
318
    def __str__(self):
307
319
        return self.name
308
320
309
321
class StudyProgramme(models.Model):
310
322
    """ Represents a programme within a certain study.
311
323
    A good example for this is the different specializations, minors, majors, ...
312
324
    one can follow within the same study. Nevertheless, they're all made of
313
325
    a certain set of courses. This table collects all these, and allows one to name
314
326
    them, so they're distinct from one another. """
315
327
    def name = models.CharField(
316
-
            max_length=64,
+
328
            max_length=64,
317
329
            blank=False,
318
330
            help_text=_("The name of this programme."),
319
331
            )
320
332
321
333
    def courses(self):
322
334
        """ All courses that are part of this study programme. """
323
335
        programmes = ProgrammeInformation.objects.filter(study_programme=self)
324
336
        courses = {}
325
337
        for program in programmes:
326
338
            courses.add(program.course)
327
339
        return courses
328
340
329
341
    def study_points(self, year=None):
330
342
        """ Returns the amount of study points this programme contains.
331
343
        Accepts year as an optional argument. If not given, the study points
332
344
        of all years are returned. """
333
345
        programmes = ProgrammeInformation.objects.filter(study_programme=self)
334
346
        ECTS = 0
335
347
        for program in programmes:
336
348
            if year is None or program.year == year:
337
349
                # XXX: This only works if the used implementation does lazy
338
350
                # evaluation, otherwise this is a type error!
339
351
                ECTS += program.ECTS
340
352
        return ECTS
341
353
342
354
    def __str__(self):
343
355
        return self.name
344
356
345
357
# Tables about things related to the courses:
346
358
347
359
class Assignment(models.Model):
348
360
    """ For courses, it's possible to set up tasks. These tasks are recorded
349
361
    here. """
350
362
    # TODO: Require that only the course team can create assignments for a team.
351
363
    course = models.ForeignKey(
352
364
        "Course",
353
365
        on_delete=models.CASCADE,
354
366
        null=False,
355
367
        editable=False,
356
368
        db_index=True,
357
369
        help_text=_("The course for which this task is assigned."),
358
370
        )
359
371
    information = models.TextField(
360
372
        help_text=_("Any additional information regarding the assignment. Orgmode syntax available."),
361
373
        )
362
374
    deadline = models.DateTimeField(
363
375
        null=False,
364
376
        help_text=_("The date and time this task is due."),
365
377
        )
366
378
    posted = models.DateField(auto_now_add=True)
367
379
    digital_task = models.BooleanField(
368
380
        default=True,
369
381
        help_text=_("This determines whether this assignment requires handing "
370
382
                    "in a digital file."),
371
383
        )
372
384
373
385
    def __str__(self):
374
386
        return str(self.course) +" | "+ str(self.posted)
375
387
376
388
class Announcement(models.Model):
377
389
    """ Courses sometimes have to make announcements for the students. """
378
390
    course = models.ForeignKey(
379
391
        "Course",
380
392
        on_delete=models.CASCADE,
381
393
        null=False,
382
394
        editable=False,
383
395
        db_index=True,
384
396
        help_text=_("The course for which this announcement is made."),
385
397
        )
386
398
    title = models.CharField(
387
399
        max_length=20,  # Keep It Short & Simple®
388
400
        help_text=_("A quick title for what this is about."),
389
401
        )
390
402
    text = models.TextField(
391
403
        blank=False,
392
404
        help_text=_("The announcement itself. Orgmode syntax available."),
393
405
        )
394
406
    posted = models.DateTimeField(auto_now_add=True)
395
407
396
408
    def __str__(self):
397
409
        return str(self.course) +" | "+ self.posted.strftime("%m/%d")
398
410
399
411
class Upload(models.Model):
400
412
    """ For certain assignments, digital hand-ins may be required. These hand
401
413
    ins are recorded per student in this table. """
402
414
    assignment = models.ForeignKey(
403
415
        "Assignment",
404
416
        on_delete=models.CASCADE,
405
417
        null=False,
406
418
        editable=False,
407
419
        db_index=True,
408
420
        limit_choices_to={"digital_task": True},
409
421
        help_text=_("For which assignment this upload is."),
410
422
        )
411
423
    # TODO: Try to find a way to require that, if the upload is made,
412
424
    # only students that have this course in their curriculum can upload.
413
425
    student = models.ForeignKey(
414
426
        "joeni.User",
415
-
        on_delete=models.CASCADE,
+
427
        on_delete=models.CASCADE,
416
428
        null=False,
417
429
        editable=False,
418
430
        limit_choices_to={"is_student": True},
419
431
        help_text=_("The student who handed this in."),
420
432
        )
421
433
    upload_time = models.DateTimeField(auto_now_add=True)
422
434
    comment = models.TextField(
423
435
        blank=True,
424
436
        help_text=_("If you wish to add an additional comment, state it here."),
425
437
        )
426
438
+
439
        upload_to="assignments/uploads/%Y/%m/",
+
440
        null=False,
+
441
        editable=False,
+
442
        help_text=_("The file you want to upload for this assignment."),
+
443
        )
+
444
+
445
427
446
    def __str__(self):
428
447
        deadline = self.assignment.deadline
429
448
        if deadline < self.upload_time
430
-
            return str(self.assignment.course) +" | "+ str(self.student.number) + _("(OVERDUE)")
+
449
            return str(self.assignment.course) +" | "+ str(self.student.number) + _("(OVERDUE)")
431
450
        else:
432
451
            return str(self.assignment.course) +" | "+ str(self.student.number)
433
452
434
453
class StudyGroup(models.Model):
435
454
    """ It may be necessary to make study groups regarding a course. These
436
455
    are recorded here, and blend in seamlessly with the Groups from Agora.
437
456
    Groups that are recorded as a StudyGroup, are given official course status,
438
457
    and thus, cannot be removed until the status of StudyGroup is lifted. """
439
458
    course = models.ForeignKey(
440
459
        "Course",
441
460
        on_delete=models.CASCADE,
442
461
        null=False,
443
462
        editable=False,
444
463
        db_index=True,
445
464
        help_text=_("The course for which this group is."),
446
465
        )
447
466
    group = models.ForeignKey(
448
467
        "agora.Group",
449
468
        on_delete=models.PROTECT,  # See class documentation
450
469
        null=False,
451
470
        editable=False,  # Keep the same group
452
471
        help_text=_("The group that will be seen as the study group."),
453
472
        )
454
473
455
474
    def __str__(self):
456
475
        return str(self.course) +" | "+ str(self.group)
457
476

joeni/admin.py

2 additions and 0 deletions.

View changes Hide changes
+
1
from .models import *
+
2

joeni/apps.py

5 additions and 0 deletions.

View changes Hide changes
+
1
+
2
+
3
class JoeniConfig(AppConfig):
+
4
    name = 'joeni'
+
5

joeni/models.py

0 additions and 156 deletions.

View changes Hide changes
1
-
from django.core.exceptions import ValidationError  # For validating IBAN input
2
-
from django.utils.translation import ugettext_lazy as _
3
-
from django.db import models
4
-
import datetime
5
-
import os
6
-
7
-
class room(models.Model):
8
-
    """ Represents a room in the university.
9
-
    Rooms can have a number of properties, which are stored in the database.
10
-
    """
11
-
    name = models.TextField()
12
-
    seats = models.IntegerField()
13
-
    wheelchair_accessible = models.BooleanField(default=True,blank=False)
14
-
    exams_equipped = models.BooleanField(blank=False)
15
-
16
-
class User(models.Model):
17
-
    """ Replacement for the standard Django User model. """
18
-
    number = models.PositiveIntegerField(
19
-
        primary_key=True,
20
-
        help_text=_("The number assigned to this user."),
21
-
        )
22
-
    created = models.DateField(auto_now_add=True)
23
-
    passphrase = models.CharField(
24
-
        max_length=512,
25
-
        blank=False,
26
-
        help_text=_("The passphrase used for this account. This field must only contain hashed information."),
27
-
        )
28
-
    first_name = models.CharField(max_length=64, blank=False)
29
-
    last_name = models.CharField(max_length=64, blank=False)
30
-
    title = models.CharField(
31
-
        max_length=64,
32
-
        blank=True,
33
-
        help_text=_("The academic title of this user, if applicable."),
34
-
        )
35
-
    DOB = models.DateField(
36
-
        blank=False,
37
-
        editable=False,
38
-
        help_text=_("The date of birth of this user."),
39
-
        )
40
-
    POB = models.CharField(
41
-
        max_length=64,
42
-
        blank=False,
43
-
        editable=False,
44
-
        help_text=_("The place of birth of this user."),
45
-
        )
46
-
    nationality = models.CharField(
47
-
        max_length=64,
48
-
        blank=False,
49
-
        help_text=_("The current nationality of this user."),
50
-
        )
51
-
    # XXX: What if this starts with zeros?
52
-
    national_registry_number = models.BigIntegerField(
53
-
        unique=True,
54
-
        editable=False,
55
-
        help_text=_("The assigned national registry number of this user."),
56
-
        )
57
-
    civil_status = models.CharField(
58
-
        choices = (
59
-
            ("Single", _("Single")),
60
-
            ("Married", _("Married")),
61
-
            ("Divorced", _("Divorced")),
62
-
            ("Widowed", _("Widowed")),
63
-
            ("Partnership", _("Partnership")),
64
-
            ),
65
-
        blank=False,
66
-
        # There may be more; consult http://www.aantrekkingskracht.com/trefwoord/burgerlijke-staat
67
-
        # for more information.
68
-
        help_text=_("The civil/marital status of the user."),
69
-
        )
70
-
71
-
    is_staff = models.BooleanField(
72
-
        default=False,
73
-
        help_text=_("Determines if this user is part of the university's staff."),
74
-
        )
75
-
    is_student = models.BooleanField(
76
-
        default=True,
77
-
        help_text=_("Indicates if this user is a student at the university."),
78
-
        )
79
-
80
-
    # Home address
81
-
    home_street = models.CharField(max_length=64, blank=False)
82
-
    home_number = models.PositiveSmallIntegerField(blank=False)
83
-
    home_bus = models.PositiveSmallIntegerField()
84
-
    home_postal_code = models.PositiveSmallIntegerField(blank=False)
85
-
    home_country = models.CharField(max_length=64, blank=False)
86
-
    home_telephone = models.CharField(
87
-
        max_length=64,
88
-
        help_text=_("The telephone number for the house address. Prefix 0 can be presented with the national call code in the system."),
89
-
        )
90
-
    # Study address
91
-
    study_street = models.CharField(max_length=64, blank=False)
92
-
    study_number = models.PositiveSmallIntegerField(blank=False)
93
-
    study_bus = models.PositiveSmallIntegerField()
94
-
    study_postal_code = models.PositiveSmallIntegerField(blank=False)
95
-
    study_country = models.CharField(max_length=64, blank=False)
96
-
    study_telephone = models.CharField(
97
-
        max_length=64,
98
-
        help_text=_("The telephone number for the study address. Prefix 0 can be presented with the national call code in the system."),
99
-
        )
100
-
    study_cellphone = models.CharField(
101
-
        max_length=64,
102
-
        help_text=_("The cellphone number of the person. Prefix 0 can be presented with then national call code in the system."),
103
-
        )
104
-
    # Titularis address
105
-
    # XXX: These fields are only required if this differs from the user itself.
106
-
    titularis_street = models.CharField(max_length=64)
107
-
    titularis_number = models.PositiveSmallIntegerField()
108
-
    titularis_bus = models.PositiveSmallIntegerField()
109
-
    titularis_postal_code = models.PositiveSmallIntegerField()
110
-
    titularis_country = models.CharField(max_length=64)
111
-
    titularis_telephone = models.CharField(
112
-
        max_length=64,
113
-
        help_text=_("The telephone number of the titularis. Prefix 0 can be presented with the national call code in the system."),
114
-
        )
115
-
116
-
    # Financial details
117
-
    bank_account_number = models.CharField(
118
-
        max_length=34,  # Max length of all IBAN account numbers
119
-
        validators=[validate_IBAN],
120
-
        help_text=_("The IBAN of this user. No spaces!"),
121
-
        )
122
-
    BIC = models.CharField(
123
-
        max_length=11,
124
-
        validators=[validate_BIC],
125
-
        help_text=_("The BIC of this user's bank."),
126
-
        )
127
-
128
-
def validate_IBAN(value):
129
-
    """ Validates if the given value qualifies as a valid IBAN number.
130
-
    This validator checks if the structure is valid, and calculates the control
131
-
    number if the structure is correct. If the control number fails, or the
132
-
    structure is invalid, a ValidationError will be raised. In that case,
133
-
    the Error will specify whether the structure is incorrect, or the control
134
-
    number is not valid.
135
-
    """
136
-
    # FIXME: This function is not complete. When there's time, implement
137
-
    # as specified at https://nl.wikipedia.org/wiki/International_Bank_Account_Number#Structuur
138
-
    if False:
139
-
        raise ValidationError(
140
-
            _('%(value)s is not a valid IBAN number.'),
141
-
            params={'value': value},)
142
-
def validate_BIC(value):
143
-
    """ Same functionality as validate_IBAN, but for BIC-codes. """
144
-
    # FIXME: This function is not complete. When there's time, implement
145
-
    # as specified at https://nl.wikipedia.org/wiki/Business_Identifier_Code
146
-
    pass
147
-
148
-
149
-
""" NOTE: What about all the other features that should be in the administration?
150
-
While there are a lot of things to cover, as of now, I have no way to know which
151
-
ones are still valid, which are deprecated, and so on...
152
-
Additionally, every feature may have a different set of requirements, data,
153
-
and it's very likely making an abstract class won't do any good. Thus I have
154
-
decided to postpone making additional tables and forms for these features until
155
-
I have clearance about certain aspects. """
156
-

joeni/settings.py

7 additions and 4 deletions.

View changes Hide changes
1
1
Django settings for Joeni project.
2
2
3
3
Generated by 'django-admin startproject' using Django 2.0b1.
4
4
5
5
For more information on this file, see
6
6
https://docs.djangoproject.com/en/dev/topics/settings/
7
7
8
8
For the full list of settings and their values, see
9
9
https://docs.djangoproject.com/en/dev/ref/settings/
10
10
"""
11
11
12
12
import os
13
13
14
14
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
15
15
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
16
16
17
17
18
18
# Quick-start development settings - unsuitable for production
19
19
# See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/
20
20
21
21
# SECURITY WARNING: keep the secret key used in production secret!
22
22
SECRET_KEY = '!2634qc=b*lp0=helzcmvb3+1_wcl!6z@mhzi%p(vg7odq&gfz'
23
23
24
24
# SECURITY WARNING: don't run with debug turned on in production!
25
25
DEBUG = True
26
26
27
27
ALLOWED_HOSTS = []
28
28
29
29
30
30
# Application definition
31
31
32
32
INSTALLED_APPS = [
33
33
    'django.contrib.admin',
34
34
    'django.contrib.auth',
35
35
    'django.contrib.contenttypes',
36
36
    'django.contrib.sessions',
37
37
    'django.contrib.messages',
38
38
    'django.contrib.staticfiles',
39
39
    'administation',
40
-
    'agora',
+
40
    'agora',
41
41
    'courses',
42
42
]
+
43
]
43
44
44
45
MIDDLEWARE = [
45
46
    'django.middleware.security.SecurityMiddleware',
46
47
    'django.middleware.locale.LocaleMiddleware',
47
48
    'django.middleware.common.CommonMiddleware',
48
49
    'django.middleware.csrf.CsrfViewMiddleware',
49
50
    'django.contrib.sessions.middleware.SessionMiddleware',
50
51
    'django.contrib.auth.middleware.AuthenticationMiddleware',
51
52
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
52
-
    'django.contrib.messages.middleware.MessageMiddleware',
+
53
    'django.contrib.messages.middleware.MessageMiddleware',
53
54
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
54
55
    'django.middleware.security.SecurityMiddleware',
55
56
    # Caching middleware
56
57
    'django.middleware.cache.UpdateCacheMiddleware',
57
58
    'django.middleware.common.CommonMiddleware',
58
59
    'django.middleware.cache.FetchFromCacheMiddleware',
59
60
]
60
61
61
62
# Caching settings
62
63
CACHE_MIDDLEWARE_ALIAS = 'dummy'
63
-
CACHE_MIDDLEWARE_SECONDS = 300
+
64
CACHE_MIDDLEWARE_SECONDS = 300
64
65
CACHE_MIDDLEWARE_KEY_PREFIX = ''
65
66
66
67
ROOT_URLCONF = 'joeni.urls'
67
68
68
69
TEMPLATES = [
69
70
    {
70
71
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
71
72
        'DIRS': [],
72
73
        'APP_DIRS': True,
73
74
        'OPTIONS': {
74
75
            'context_processors': [
75
76
                'django.template.context_processors.debug',
76
77
                'django.template.context_processors.request',
77
78
                'django.contrib.auth.context_processors.auth',
78
79
                'django.contrib.messages.context_processors.messages',
79
80
            ],
80
81
        },
81
82
    },
82
83
]
83
84
84
85
WSGI_APPLICATION = 'joeni.wsgi.application'
85
-
+
86
86
87
87
88
# Database
88
89
# https://docs.djangoproject.com/en/dev/ref/settings/#databases
89
90
90
91
DATABASES = {
91
92
    'default': {
92
93
        'ENGINE': 'django.db.backends.sqlite3',
93
94
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
94
95
    }
95
96
}
96
97
97
98
+
99
AUTH_USER_MODEL = 'administration.User'
+
100
98
101
# Password validation
99
102
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
100
103
101
104
AUTH_PASSWORD_VALIDATORS = [
102
105
    {
103
106
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
104
107
    },
105
108
    {
106
109
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
107
110
    },
108
111
    {
109
112
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
110
113
    },
111
114
    {
112
115
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
113
116
    },
114
117
]
115
118
116
119
CACHES = {
117
120
    'default': {
118
121
        'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
119
122
    }
120
123
}
121
124
122
125
# Internationalization
123
126
# https://docs.djangoproject.com/en/dev/topics/i18n/
124
127
125
128
LANGUAGE_CODE = 'en-us'
126
129
127
130
TIME_ZONE = 'UTC'
128
131
129
132
USE_I18N = True
130
133
131
134
USE_L10N = True
132
135
133
136
USE_TZ = True
134
137
135
138
136
139
# Static files (CSS, JavaScript, Images)
137
140
# https://docs.djangoproject.com/en/dev/howto/static-files/
138
141
139
142
STATIC_URL = '/static/'
140
143
MEDIA_URL = '/media/'
141
144