Skip to content
Snippets Groups Projects
Commit 80c272eb authored by Jakob Dieterle's avatar Jakob Dieterle
Browse files

Resolve "Make exam a many to many field on StudentInfo model"

parent b1521297
No related branches found
No related tags found
1 merge request!244Resolve "Make exam a many to many field on StudentInfo model"
Pipeline #320699 failed
Showing
with 340 additions and 53 deletions
# Generated by Django 2.2.12 on 2020-07-07 14:56
import core.models.user_account
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0004_feedback_modified'),
]
operations = [
migrations.RemoveField(
model_name='studentinfo',
name='exam',
),
migrations.RemoveField(
model_name='studentinfo',
name='passes_exam',
),
migrations.RemoveField(
model_name='studentinfo',
name='total_score',
),
migrations.AlterField(
model_name='useraccount',
name='exercise_groups',
field=models.ManyToManyField(blank=True, default=core.models.user_account.group_default, related_name='users', to='core.Group'),
),
migrations.CreateModel(
name='StudentsExam',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('total_score', models.PositiveIntegerField(default=0)),
('passes_exam', models.BooleanField(default=False)),
('exam', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exam', to='core.ExamType')),
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='students', to='core.StudentInfo')),
],
),
migrations.AddField(
model_name='studentinfo',
name='exams',
field=models.ManyToManyField(blank=True, related_name='exams', to='core.StudentsExam'),
),
]
# Generated by Django 2.2.12 on 2020-08-10 11:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0005_auto_20200707_1456'),
]
operations = [
migrations.RenameModel(
old_name='StudentsExam',
new_name='ExamInfo',
),
]
# Generated by Django 2.2.12 on 2020-09-22 10:26
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0006_auto_20200810_1115'),
]
operations = [
migrations.RemoveField(
model_name='studentinfo',
name='exams',
),
migrations.AlterField(
model_name='examinfo',
name='student',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exams', to='core.StudentInfo'),
),
]
# Generated by Django 2.2.12 on 2020-09-22 11:48
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0007_auto_20200922_1026'),
]
operations = [
migrations.AlterField(
model_name='examinfo',
name='exam',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exam_infos', to='core.ExamType'),
),
]
# Generated by Django 2.2.16 on 2020-10-22 11:57
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0008_auto_20200922_1148'),
]
operations = [
migrations.AddField(
model_name='submissiontype',
name='exam_type',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='submission_types', to='core.ExamType'),
),
]
# Generated by Django 2.2.16 on 2020-11-03 11:54
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0009_submissiontype_exam_type'),
]
operations = [
migrations.AddField(
model_name='group',
name='examType',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='groups', to='core.ExamType'),
),
]
# Generated by Django 2.2.16 on 2020-11-03 12:11
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0010_group_examtype'),
]
operations = [
migrations.RenameField(
model_name='group',
old_name='examType',
new_name='exam_type',
),
]
# Generated by Django 2.2.16 on 2020-11-03 12:28
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0011_auto_20201103_1211'),
]
operations = [
migrations.AlterModelOptions(
name='group',
options={'verbose_name': 'Group', 'verbose_name_plural': 'Groups'},
),
]
# Generated by Django 2.2.16 on 2020-11-03 12:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0012_auto_20201103_1228'),
]
operations = [
migrations.AlterModelOptions(
name='group',
options={},
),
migrations.RemoveField(
model_name='group',
name='exam_type',
),
]
# Generated by Django 3.1.3 on 2020-11-23 12:52
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0013_auto_20201103_1248'),
('core', '0006_auto_20201027_1234'),
]
operations = [
]
# Generated by Django 3.1.7 on 2021-09-02 11:36
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0014_merge_20201123_1252'),
]
operations = [
migrations.AddField(
model_name='group',
name='exam_type',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='groups', to='core.examtype'),
),
]
# Generated by Django 3.1.7 on 2021-09-02 11:40
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0015_group_exam_type'),
]
operations = [
migrations.RenameField(
model_name='group',
old_name='exam_type',
new_name='exam',
),
]
...@@ -3,7 +3,7 @@ from .exam_type import ExamType # noqa ...@@ -3,7 +3,7 @@ from .exam_type import ExamType # noqa
from .submission_type import SubmissionType, SolutionComment # noqa from .submission_type import SubmissionType, SolutionComment # noqa
from .user_account import UserAccount, TutorReviewerManager # noqa from .user_account import UserAccount, TutorReviewerManager # noqa
from .user_account import UserAccount, TutorReviewerManager # noqa from .user_account import UserAccount, TutorReviewerManager # noqa
from .student_info import StudentInfo, random_matrikel_no # noqa from .student_info import StudentInfo, random_matrikel_no, ExamInfo # noqa
from .test import Test # noqa from .test import Test # noqa
from .submission import Submission, MetaSubmission # noqa from .submission import Submission, MetaSubmission # noqa
from .feedback import Feedback, FeedbackComment # noqa from .feedback import Feedback, FeedbackComment # noqa
......
from django.db import models from django.db import models
from core.models.exam_type import ExamType
import uuid import uuid
...@@ -7,3 +8,7 @@ class Group(models.Model): ...@@ -7,3 +8,7 @@ class Group(models.Model):
default=uuid.uuid4, default=uuid.uuid4,
editable=False) editable=False)
name = models.CharField(max_length=120) name = models.CharField(max_length=120)
exam = models.ForeignKey(ExamType,
on_delete=models.CASCADE,
related_name='groups',
null=True)
...@@ -7,11 +7,11 @@ from typing import Dict ...@@ -7,11 +7,11 @@ from typing import Dict
import constance import constance
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import models from django.db import models
from django.db.models import (BooleanField, Case, F, from django.db.models import BooleanField, F, When, Sum, QuerySet, Value, Case
QuerySet, Sum, Value, When)
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from core.models.submission_type import SubmissionType from core.models.submission_type import SubmissionType
from core.models.exam_type import ExamType
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
config = constance.config config = constance.config
...@@ -26,6 +26,42 @@ def random_matrikel_no() -> str: ...@@ -26,6 +26,42 @@ def random_matrikel_no() -> str:
return str(10_000_000 + randrange(90_000_000)) return str(10_000_000 + randrange(90_000_000))
class ExamInfo(models.Model):
exam = models.ForeignKey(ExamType,
on_delete=models.CASCADE,
related_name='exam_infos',
null=False)
student = models.ForeignKey('StudentInfo',
on_delete=models.CASCADE,
related_name='exams',
null=False)
total_score = models.PositiveIntegerField(default=0)
passes_exam = models.BooleanField(default=False)
def update_total_score(self):
''' This helper is invoked after feedback changes '''
self.total_score = self.student.submissions.aggregate(
Sum('feedback__score'))['feedback__score__sum'] or 0
if self.exam is not None:
self.passes_exam = self.total_score >= self.exam.pass_score
self.save()
def score_per_submission(self) -> Dict[str, int]:
""" TODO: get rid of it and use an annotation. """
if self.student.submissions.all():
return OrderedDict({
s.type.name: s.feedback.score if hasattr(s, 'feedback') else 0
for s in self.student.submissions.filter(type__exam_type=self.exam)
.order_by('type__name')
})
return OrderedDict({
t.name: 0 for t in SubmissionType.objects.all()
})
class StudentInfo(models.Model): class StudentInfo(models.Model):
""" """
The StudentInfo model includes all information of a student, that we got The StudentInfo model includes all information of a student, that we got
...@@ -36,8 +72,9 @@ class StudentInfo(models.Model): ...@@ -36,8 +72,9 @@ class StudentInfo(models.Model):
associated user model. associated user model.
Attributes: Attributes:
exam (ForeignKey): exams (ManyToManyField):
Which module the student wants to be graded in Module the student wants te be graded in, or different exercise
assignments for one module.
has_logged_in (BooleanField): has_logged_in (BooleanField):
Login is permitted once. If this is set the user can not log in. Login is permitted once. If this is set the user can not log in.
...@@ -52,37 +89,15 @@ class StudentInfo(models.Model): ...@@ -52,37 +89,15 @@ class StudentInfo(models.Model):
matrikel_no = models.CharField(unique=True, matrikel_no = models.CharField(unique=True,
max_length=30, max_length=30,
default=random_matrikel_no) default=random_matrikel_no)
exam = models.ForeignKey('ExamType',
on_delete=models.CASCADE,
related_name='students',
null=False)
user = models.OneToOneField(get_user_model(), user = models.OneToOneField(get_user_model(),
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='student') related_name='student')
# Managed by signals def add_exam(self, exam):
total_score = models.PositiveIntegerField(default=0) exam_info = ExamInfo(exam=exam, student=self)
passes_exam = models.BooleanField(default=False) exam_info.save()
self.exams.add(exam_info)
def update_total_score(self):
''' This helper is invoked after feedback changes '''
self.total_score = self.submissions.aggregate(
Sum('feedback__score'))['feedback__score__sum'] or 0
if self.exam is not None:
self.passes_exam = self.total_score >= self.exam.pass_score
self.save()
def score_per_submission(self) -> Dict[str, int]:
""" TODO: get rid of it and use an annotation. """
if self.submissions.all():
return OrderedDict({
s.type.name: s.feedback.score if hasattr(s, 'feedback') else 0
for s in self.submissions.order_by('type__name')
})
return OrderedDict({
t.name: 0 for t in SubmissionType.objects.all()
})
@classmethod @classmethod
def get_annotated_score_submission_list(cls) -> QuerySet: def get_annotated_score_submission_list(cls) -> QuerySet:
......
...@@ -6,6 +6,8 @@ from django.db import models ...@@ -6,6 +6,8 @@ from django.db import models
from django.db.models import (Case, Count, IntegerField, Q, from django.db.models import (Case, Count, IntegerField, Q,
Value, When) Value, When)
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from core.models.exam_type import ExamType
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
config = constance.config config = constance.config
...@@ -56,6 +58,10 @@ class SubmissionType(models.Model): ...@@ -56,6 +58,10 @@ class SubmissionType(models.Model):
full_score = models.PositiveIntegerField(default=0) full_score = models.PositiveIntegerField(default=0)
description = models.TextField() description = models.TextField()
solution = models.TextField() solution = models.TextField()
exam_type = models.ForeignKey(ExamType,
on_delete=models.CASCADE,
related_name='submission_types',
null=True)
programming_language = models.CharField(max_length=25, programming_language = models.CharField(max_length=25,
choices=LANGUAGE_CHOICES, choices=LANGUAGE_CHOICES,
default=C) default=C)
......
from .common_serializers import * # noqa from .common_serializers import * # noqa
from .group import GroupSerializer # noqa
from .submission_type import (SubmissionTypeListSerializer, SubmissionTypeSerializer, # noqa from .submission_type import (SubmissionTypeListSerializer, SubmissionTypeSerializer, # noqa
SolutionCommentSerializer) # noqa SolutionCommentSerializer) # noqa
from .feedback import (FeedbackSerializer, FeedbackWithStudentSerializer, # noqa from .feedback import (FeedbackSerializer, FeedbackWithStudentSerializer, # noqa
......
...@@ -8,7 +8,6 @@ from rest_framework import serializers ...@@ -8,7 +8,6 @@ from rest_framework import serializers
from rest_framework.utils import html from rest_framework.utils import html
from core import models from core import models
from core.serializers.group import GroupSerializer
from .generic import DynamicFieldsModelSerializer from .generic import DynamicFieldsModelSerializer
...@@ -23,6 +22,14 @@ class ExamSerializer(DynamicFieldsModelSerializer): ...@@ -23,6 +22,14 @@ class ExamSerializer(DynamicFieldsModelSerializer):
'pass_score', 'pass_only',) 'pass_score', 'pass_only',)
class GroupSerializer(serializers.ModelSerializer):
exam = ExamSerializer(many=False)
class Meta:
model = models.Group
fields = ('pk', 'name', 'exam')
class TestSerializer(DynamicFieldsModelSerializer): class TestSerializer(DynamicFieldsModelSerializer):
class Meta: class Meta:
......
from rest_framework import serializers
from core.models import Group
class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = Group
fields = ('pk', 'name')
from rest_framework import serializers from rest_framework import serializers
from core.models import StudentInfo from core.models import StudentInfo, ExamInfo
from core.serializers import DynamicFieldsModelSerializer, ExamSerializer from core.serializers import DynamicFieldsModelSerializer, ExamSerializer
from core.serializers.submission import (SubmissionListSerializer, from core.serializers.submission import (SubmissionListSerializer,
SubmissionNoTextFieldsSerializer, SubmissionNoTextFieldsSerializer,
SubmissionNoTypeSerializer) SubmissionNoTypeSerializer)
class ExamInfoListSerializer(DynamicFieldsModelSerializer):
class Meta:
model = ExamInfo
fields = ('exam', 'student', 'total_score', 'passes_exam')
class StudentInfoSerializer(DynamicFieldsModelSerializer): class StudentInfoSerializer(DynamicFieldsModelSerializer):
name = serializers.ReadOnlyField(source='user.fullname') name = serializers.ReadOnlyField(source='user.fullname')
matrikel_no = serializers.ReadOnlyField(source='user.matrikel_no') matrikel_no = serializers.ReadOnlyField(source='user.matrikel_no')
exam = ExamSerializer() exams = ExamInfoListSerializer(many=True)
submissions = SubmissionListSerializer(many=True) submissions = SubmissionListSerializer(many=True)
class Meta: class Meta:
...@@ -19,16 +26,15 @@ class StudentInfoSerializer(DynamicFieldsModelSerializer): ...@@ -19,16 +26,15 @@ class StudentInfoSerializer(DynamicFieldsModelSerializer):
'name', 'name',
'user', 'user',
'matrikel_no', 'matrikel_no',
'exam',
'submissions', 'submissions',
'passes_exam') 'exams')
class StudentInfoForListViewSerializer(DynamicFieldsModelSerializer): class StudentInfoForListViewSerializer(DynamicFieldsModelSerializer):
name = serializers.ReadOnlyField(source='user.fullname') name = serializers.ReadOnlyField(source='user.fullname')
user = serializers.ReadOnlyField(source='user.username') user = serializers.ReadOnlyField(source='user.username')
user_pk = serializers.ReadOnlyField(source='user.pk') user_pk = serializers.ReadOnlyField(source='user.pk')
exam = serializers.ReadOnlyField(source='exam.module_reference') exams = serializers.ReadOnlyField(source='exams.module_reference')
submissions = SubmissionNoTextFieldsSerializer(many=True) submissions = SubmissionNoTextFieldsSerializer(many=True)
is_active = serializers.BooleanField(source='user.is_active') is_active = serializers.BooleanField(source='user.is_active')
...@@ -38,10 +44,9 @@ class StudentInfoForListViewSerializer(DynamicFieldsModelSerializer): ...@@ -38,10 +44,9 @@ class StudentInfoForListViewSerializer(DynamicFieldsModelSerializer):
'name', 'name',
'user', 'user',
'user_pk', 'user_pk',
'exam', 'exams',
'submissions', 'submissions',
'matrikel_no', 'matrikel_no',
'passes_exam',
'is_active') 'is_active')
...@@ -49,7 +54,7 @@ class StudentExportSerializer(DynamicFieldsModelSerializer): ...@@ -49,7 +54,7 @@ class StudentExportSerializer(DynamicFieldsModelSerializer):
name = serializers.ReadOnlyField(source='user.fullname') name = serializers.ReadOnlyField(source='user.fullname')
user = serializers.ReadOnlyField(source='user.username') user = serializers.ReadOnlyField(source='user.username')
user_pk = serializers.ReadOnlyField(source='user.pk') user_pk = serializers.ReadOnlyField(source='user.pk')
exam = serializers.ReadOnlyField(source='exam.pk') exams = ExamInfoListSerializer(many=True)
email = serializers.ReadOnlyField(source='user.email') email = serializers.ReadOnlyField(source='user.email')
is_active = serializers.BooleanField(source='user.is_active') is_active = serializers.BooleanField(source='user.is_active')
submissions = SubmissionNoTypeSerializer(many=True) submissions = SubmissionNoTypeSerializer(many=True)
...@@ -60,9 +65,17 @@ class StudentExportSerializer(DynamicFieldsModelSerializer): ...@@ -60,9 +65,17 @@ class StudentExportSerializer(DynamicFieldsModelSerializer):
'name', 'name',
'user', 'user',
'user_pk', 'user_pk',
'exam', 'exams',
'email', 'email',
'submissions', 'submissions',
'matrikel_no', 'matrikel_no',
'passes_exam',
'is_active') 'is_active')
class ExamInfoSerializer(DynamicFieldsModelSerializer):
exam = ExamSerializer()
student = StudentInfoSerializer()
class Meta:
model = ExamInfo
fields = ('exam', 'student', 'total_score', 'passes_exam')
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment