Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: better submission status + favicon #454

Merged
merged 8 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions backend/api/fixtures/realistic/realistic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
- model: api.project
pk: 1
fields:
name: Lean Java
description: This project will teach you the basics of Object Oriented Programming by using Java.
name: Learn Java
description: This project will teach you the basics of Object Oriented Programming using Java.
visible: true
archived: false
locked_groups: false
Expand All @@ -56,8 +56,8 @@
- model: api.project
pk: 2
fields:
name: Lean Javascript
description: This project will show you would you should avoid javascript at all cost.
name: Learn JavaScript
description: This project will show you should avoid JavaScript at all cost.
visible: true
archived: false
locked_groups: true
Expand Down
73 changes: 38 additions & 35 deletions backend/api/serializers/project_serializer.py
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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,
Expand All @@ -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"
]
Expand All @@ -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(
Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -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)

Expand Down
18 changes: 16 additions & 2 deletions backend/api/tests/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -827,7 +827,14 @@ def test_submission_status_non_empty_groups(self):
# Only two of the three created groups contain at least one student
self.assertEqual(
content_json["status"],
{"non_empty_groups": 2, "groups_submitted": 0, "extra_checks_passed": 0, "structure_checks_passed": 0},
{
"non_empty_groups": 2,
"groups_submitted": 0,
'has_extra_checks': False,
'has_structure_checks': False,
"extra_checks_passed": 0,
"structure_checks_passed": 0
},
)

def test_submission_status_groups_submitted_and_not_passed_checks(self):
Expand Down Expand Up @@ -891,7 +898,14 @@ def test_submission_status_groups_submitted_and_not_passed_checks(self):

self.assertEqual(
content_json["status"],
{"non_empty_groups": 3, "groups_submitted": 2, "extra_checks_passed": 0, "structure_checks_passed": 0},
{
"non_empty_groups": 3,
"groups_submitted": 2,
'has_extra_checks': False,
'has_structure_checks': False,
"extra_checks_passed": 0,
"structure_checks_passed": 0
},
)

def test_retrieve_list_submissions(self):
Expand Down
2 changes: 1 addition & 1 deletion frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8"/>
<link rel="icon" type="image/svg+xml" href="/vite.svg"/>
<link rel="shortcut icon" type="image/png" href="src/assets/img/favicon.ico"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Ypovoli</title>
</head>
Expand Down
Binary file added frontend/src/assets/img/favicon.ico
Binary file not shown.
3 changes: 2 additions & 1 deletion frontend/src/assets/lang/app/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,12 +195,13 @@
"card": {
"open": "Details",
"newProject": "New project",
"noSubmissions": "This project does not have any submissions",
"noSubmissions": "This project does not have any submissions \uD83D\uDE2D",
"submissions": "Submission | Submissions",
"groups": "Group | Groups",
"structureTestsSucceed": "Succeeded structure tests",
"extraTestsSucceed": "Succeeded extra tests",
"testsFail": "Failed tests",
"successfulSubmission": "Successful submissions",
"submit": "Submit"
},
"list": {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/assets/lang/app/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@
"structureTestsSucceed": "Geslaagde structuur testen",
"extraTestsSucceed": "Geslaagde extra testen",
"testsFail": "Gefaalde testen",
"successfulSubmission": "Geslaagde indieningen",
"submit": "Indienen"
},
"list": {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/projects/ProjectDetailCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ function isInGroup(): boolean {
<Card class="border-round project-card">
<template #header>
<h2 class="text-primary m-0 text-xl flex align-items-center gap-2">
<span class="pi pi-book text-xl mr-2" /> {{ course.name }}
<span class="pi pi-book text-xl mr-2" /> {{ project.name }}
</h2>
</template>
<template #subtitle>
{{ project.name }}
{{ course.name }}
</template>
<template #content>
<div class="mb-2">
Expand Down
55 changes: 39 additions & 16 deletions frontend/src/components/submissions/ProjectMeter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,52 @@ 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;

const extraChecksFailed = structureChecksPassed - extraChecksPassed;

const green = '#76DD78';
const orange = '#FFB84F';
const red = '#F37142';

const submissionsFailedItem = {
value: (submissionsFailed / groups) * 100,
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;
structureChecksPassedItem.icon = 'pi pi-check';
return [submissionsFailedItem, structureChecksPassedItem];
}
return [
{
value: (extraChecksPassed / groups) * 100,
color: '#8fb682',
label: t('components.card.extraTestsSucceed'),
value: (groupsSubmitted / groups) * 100,
color: green,
label: t('components.card.successfulSubmission'),
icon: 'pi pi-check',
},
{
value: ((structureChecksPassed - extraChecksPassed) / groups) * 100,
color: '#FFB84F',
label: t('components.card.structureTestsSucceed'),
icon: 'pi pi-exclamation-circle',
},
{
value: (submissionsFailed / groups) * 100,
color: 'indianred',
label: t('components.card.testsFail'),
icon: 'pi pi-times',
},
];
});
</script>
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/types/SubmisionStatus.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Expand All @@ -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,
) {}
Expand All @@ -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,
);
Expand Down