diff --git a/backend/api/serializers/project_serializer.py b/backend/api/serializers/project_serializer.py index ad2fdab5..9787c0b8 100644 --- a/backend/api/serializers/project_serializer.py +++ b/backend/api/serializers/project_serializer.py @@ -1,19 +1,16 @@ -from django.core.files.uploadedfile import InMemoryUploadedFile - from api.logic.parse_zip_files import parse_zip from api.models.group import Group from api.models.project import Project from api.models.submission import Submission, ExtraCheckResult, StructureCheckResult, StateEnum from api.models.checks import ExtraCheck, StructureCheck from api.serializers.course_serializer import CourseSerializer +from django.core.files.uploadedfile import InMemoryUploadedFile from django.utils import timezone from django.utils.translation import gettext from nh3 import clean from rest_framework import serializers from rest_framework.exceptions import ValidationError -from api.serializers.fields.expandable_hyperlinked_field import ExpandableHyperlinkedIdentityField - class SubmissionStatusSerializer(serializers.Serializer): non_empty_groups = serializers.IntegerField(read_only=True) @@ -28,18 +25,25 @@ def to_representation(self, instance: Project): non_empty_groups = Group.objects.filter(project=instance, students__isnull=False).distinct().count() + # groups_submitted groups_submitted_ids = Submission.objects.filter(group__project=instance).values_list('group__id', flat=True) unique_groups = set(groups_submitted_ids) groups_submitted = len(unique_groups) # The total amount of groups with at least one submission should never exceed the total number of non empty groups # (the seeder does not account for this restriction) - if groups_submitted > non_empty_groups: + if (groups_submitted > non_empty_groups): non_empty_groups = groups_submitted - extra_checks_count = instance.extra_checks.count() + # has_structure_checks + has_structure_checks = instance.structure_checks.count() != 0 + + # has_extra checks + has_extra_checks = instance.extra_checks.count() != 0 - if extra_checks_count: + # extra_checks_passed: only calculate if the project actually contains extra checks + extra_checks_passed = 0 + if has_extra_checks: passed_extra_checks_submission_ids = ExtraCheckResult.objects.filter( submission__group__project=instance, submission__is_valid=True, @@ -53,42 +57,41 @@ def to_representation(self, instance: Project): unique_groups = set(passed_extra_checks_group_ids) extra_checks_passed = len(unique_groups) - passed_structure_checks_submission_ids = StructureCheckResult.objects.filter( - submission__group__project=instance, - submission__is_valid=True, - result=StateEnum.SUCCESS - ).values_list('submission__id', flat=True) - - passed_structure_checks_group_ids = Submission.objects.filter( - id__in=passed_structure_checks_submission_ids - ).values_list('group_id', flat=True) - - unique_groups = set(passed_structure_checks_group_ids) - structure_checks_passed = len(unique_groups) + # structure_checks_passed: only calculate if the project actually contains structure checks + structure_checks_passed = 0 + if has_structure_checks: + passed_structure_checks_submission_ids = StructureCheckResult.objects.filter( + submission__group__project=instance, + submission__is_valid=True, + result=StateEnum.SUCCESS + ).values_list('submission__id', flat=True) - # If there are no extra checks, we can set extra_checks_passed equal to structure_checks_passed - if not extra_checks_count: - extra_checks_passed = structure_checks_passed + passed_structure_checks_group_ids = Submission.objects.filter( + id__in=passed_structure_checks_submission_ids + ).values_list('group_id', flat=True) - # If the extra checks succeed, the structure checks also succeed - structure_checks_passed -= extra_checks_passed + unique_groups = set(passed_structure_checks_group_ids) + structure_checks_passed = len(unique_groups) - # The total number of passed extra checks combined with the number of passed structure checks - # can never exceed the total number of submissions (the seeder does not account for this restriction) - if structure_checks_passed + extra_checks_passed > groups_submitted: - extra_checks_passed = groups_submitted - structure_checks_passed + # We can assume that if the extra checks pass, the struture checks pass as well + if (structure_checks_passed < extra_checks_passed): + structure_checks_passed = extra_checks_passed return { "non_empty_groups": non_empty_groups, "groups_submitted": groups_submitted, + "has_structure_checks": has_structure_checks, + "has_extra_checks": has_extra_checks, "structure_checks_passed": structure_checks_passed, - "extra_checks_passed": extra_checks_passed + "extra_checks_passed": extra_checks_passed, } class Meta: fields = [ "non_empty_groups", "groups_submitted", + "has_structure_checks", + "has_extra_checks", "structure_checks_passed", "extra_checks_passed" ] @@ -107,11 +110,13 @@ class ProjectSerializer(serializers.ModelSerializer): ) structure_checks = serializers.HyperlinkedIdentityField( - view_name="project-structure-checks" + view_name="project-structure-checks", + read_only=True ) extra_checks = serializers.HyperlinkedIdentityField( - view_name="project-extra-checks" + view_name="project-extra-checks", + read_only=True ) groups = serializers.HyperlinkedIdentityField( @@ -158,8 +163,6 @@ def validate(self, attrs): return attrs def create(self, validated_data): - """Create the project object and create groups for the project if specified""" - # Pop the 'number_groups' field from validated_data number_groups = validated_data.pop('number_groups', None) @@ -176,6 +179,7 @@ def create(self, validated_data): group.students.add(student) elif number_groups: + for _ in range(number_groups): Group.objects.create(project=project) @@ -185,11 +189,10 @@ def create(self, validated_data): group_size = project.group_size for _ in range(0, number_students, group_size): - Group.objects.create(project=project) + group = Group.objects.create(project=project) # If a zip_structure is provided, parse it to create the structure checks zip_structure: InMemoryUploadedFile | None = self.context['request'].FILES.get('zip_structure') - if zip_structure: result = parse_zip(project, zip_structure) @@ -211,4 +214,4 @@ class CreateProjectSerializer(ProjectSerializer): class TeacherCreateGroupSerializer(serializers.Serializer): - number_groups = serializers.IntegerField(min_value=1) + number_groups = serializers.IntegerField(min_value=1) \ No newline at end of file diff --git a/frontend/src/components/submissions/ProjectMeter.vue b/frontend/src/components/submissions/ProjectMeter.vue index 2a5e8f58..52070d28 100644 --- a/frontend/src/components/submissions/ProjectMeter.vue +++ b/frontend/src/components/submissions/ProjectMeter.vue @@ -17,30 +17,45 @@ const { t } = useI18n(); const meterItems = computed(() => { const groups = props.project !== null ? props.project.status.non_empty_groups : 0; const groupsSubmitted = props.project !== null ? props.project.status.groups_submitted : 0; - const structureChecksPassed = props.project !== null ? props.project.status.structure_checks_passed : 0; + const hasStructurechecks = props.project !== null ? props.project.status.has_structure_checks : false; + const hasExtraChecks = props.project !== null ? props.project.status.has_extra_checks : false; const extraChecksPassed = props.project !== null ? props.project.status.extra_checks_passed : 0; + const structureChecksPassed = props.project !== null ? props.project.status.structure_checks_passed : 0; const submissionsFailed = groupsSubmitted - structureChecksPassed; - return [ - { - value: (extraChecksPassed / groups) * 100, - color: '#8fb682', - label: t('components.card.extraTestsSucceed'), - icon: 'pi pi-check', - }, - { - value: ((structureChecksPassed - extraChecksPassed) / groups) * 100, - color: '#FFB84F', - label: t('components.card.structureTestsSucceed'), - icon: 'pi pi-exclamation-circle', - }, - { + const extraChecksFailed = structureChecksPassed - extraChecksPassed; + + const green = '#76DD78' + const orange = '#FFB84F' + const red = '#F37142' + + const submissionsFailedItem = { value: (submissionsFailed / groups) * 100, - color: 'indianred', + color: red, label: t('components.card.testsFail'), icon: 'pi pi-times', - }, - ]; + } + const structureChecksPassedItem = { + value: (extraChecksFailed / groups) * 100, + color: orange, + label: t('components.card.structureTestsSucceed'), + icon: 'pi pi-exclamation-circle', + } + const extraChecksPassedItem = { + value: (extraChecksPassed / groups) * 100, + color: green, + label: t('components.card.extraTestsSucceed'), + icon: 'pi pi-check', + } + + if (hasStructurechecks) { + if (hasExtraChecks) { + return [submissionsFailedItem, structureChecksPassedItem, extraChecksPassedItem] + } + structureChecksPassedItem.color = green + return [submissionsFailedItem, structureChecksPassedItem] + } + return [{ value: (groupsSubmitted / groups) * 100, color: green, label: t('components.card.testsFail'), icon: 'pi pi-times' }] }); diff --git a/frontend/src/types/SubmisionStatus.ts b/frontend/src/types/SubmisionStatus.ts index 3cbb4913..7a50b5f6 100644 --- a/frontend/src/types/SubmisionStatus.ts +++ b/frontend/src/types/SubmisionStatus.ts @@ -1,6 +1,8 @@ export interface SubmissionStatusJSON { non_empty_groups: number; groups_submitted: number; + has_structure_checks: boolean; + has_extra_checks: boolean; structure_checks_passed: number; extra_checks_passed: number; } @@ -9,6 +11,8 @@ export class SubmissionStatus { constructor( public non_empty_groups: number = 0, public groups_submitted: number = 0, + public has_structure_checks: boolean = false, + public has_extra_checks: boolean = false, public structure_checks_passed: number = 0, public extra_checks_passed: number = 0, ) {} @@ -22,6 +26,8 @@ export class SubmissionStatus { return new SubmissionStatus( submissionStatus.non_empty_groups, submissionStatus.groups_submitted, + submissionStatus.has_structure_checks, + submissionStatus.has_extra_checks, submissionStatus.structure_checks_passed, submissionStatus.extra_checks_passed, );