diff --git a/.gitignore b/.gitignore index 02a2ba98..4e8c4ed7 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,3 @@ frontend/cypress/videos/* !data/nginx/nginx.dev.conf !data/nginx/nginx.test.conf !data/nginx/nginx.prod.conf - -docs/.vitepress/dist -docs/.vitepress/cache \ No newline at end of file diff --git a/backend/api/locale/en/LC_MESSAGES/django.po b/backend/api/locale/en/LC_MESSAGES/django.po index 51b4c470..9015d015 100755 --- a/backend/api/locale/en/LC_MESSAGES/django.po +++ b/backend/api/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-05-20 12:28+0200\n" +"POT-Creation-Date: 2024-05-15 19:49+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -149,74 +149,78 @@ msgid "docker.errors.no_staff" msgstr "User is not allowed to assign othher owners than himself to the image." #: serializers/docker_serializer.py:31 +#: serializers/course_serializer.py:116 +msgid "courses.error.invitation_link" +msgstr "The invitation link is not unique, please try again." + +msgid "feedback.error.no_teacher" +msgstr "The user is no teacher." + +#: serializers/docker_serializer.py:19 msgid "docker.errors.custom" msgstr "User is not allowed to create public images" -#: serializers/group_serializer.py:57 +#: serializers/group_serializer.py:56 msgid "group.errors.score_exceeds_max" msgstr "The score exceeds the group's max score." -#: serializers/group_serializer.py:67 serializers/group_serializer.py:97 +#: serializers/group_serializer.py:66 serializers/group_serializer.py:96 msgid "group.error.context" msgstr "The group is not supplied in the context." -#: serializers/group_serializer.py:75 serializers/group_serializer.py:113 +#: serializers/group_serializer.py:74 serializers/group_serializer.py:108 msgid "group.errors.locked" msgstr "The group is currently locked." -#: serializers/group_serializer.py:79 +#: serializers/group_serializer.py:78 msgid "group.errors.full" msgstr "The group is already full." -#: serializers/group_serializer.py:83 +#: serializers/group_serializer.py:82 msgid "group.errors.not_in_course" msgstr "The student is not present in the related course." -#: serializers/group_serializer.py:87 +#: serializers/group_serializer.py:86 msgid "group.errors.already_in_group" msgstr "The student is already in the group." -#: serializers/group_serializer.py:105 -msgid "group.errors.size_one" -msgstr "Unable to leave a group with size 1." - -#: serializers/group_serializer.py:109 +#: serializers/group_serializer.py:104 msgid "group.errors.not_present" msgstr "The student is currently not in the group." -#: serializers/project_serializer.py:23 +#: serializers/project_serializer.py:22 msgid "project.errors.invalid_instance" msgstr "Error while parsing the provided zip." -#: serializers/project_serializer.py:122 +#: serializers/project_serializer.py:81 msgid "project.errors.context" msgstr "The project is not supplied in the context." -#: serializers/project_serializer.py:127 +#: serializers/project_serializer.py:86 msgid "project.errors.start_date_in_past" msgstr "The start date of the project lies in the past." -#: serializers/project_serializer.py:141 +#: serializers/project_serializer.py:100 msgid "project.errors.deadline_before_start_date" msgstr "The deadline of the project lies before the start date of the project." -#: serializers/project_serializer.py:183 +#: serializers/project_serializer.py:142 msgid "project.errors.zip_structure" msgstr "Error while parsing the provided zip." -#: serializers/submission_serializer.py:99 tests/test_submission.py:275 +#: serializers/submission_serializer.py:96 tests/test_submission.py:275 msgid "project.error.submissions.past_project" msgstr "The deadline of the project has already passed." -#: serializers/submission_serializer.py:102 tests/test_submission.py:346 +#: serializers/submission_serializer.py:99 tests/test_submission.py:346 msgid "project.error.submissions.non_visible_project" msgstr "The project is currently in a non-visible state." -#: serializers/submission_serializer.py:105 tests/test_submission.py:376 +#: serializers/submission_serializer.py:102 tests/test_submission.py:376 msgid "project.error.submissions.archived_project" msgstr "The project is archived." -#: serializers/submission_serializer.py:108 +#: serializers/submission_serializer.py:105 msgid "project.error.submissions.no_files" msgstr "The submission is empty." @@ -232,39 +236,39 @@ msgstr "The teacher was successfully added." msgid "teachers.success.destroy" msgstr "The teacher was successfully destroyed." -#: views/course_view.py:136 +#: views/course_view.py:137 msgid "courses.success.assistants.add" msgstr "The assistant was successfully added to the course." -#: views/course_view.py:163 +#: views/course_view.py:164 msgid "courses.success.assistants.remove" msgstr "The assistant was successfully removed from the course." -#: views/course_view.py:225 +#: views/course_view.py:226 msgid "courses.success.students.add" msgstr "The student was successfully added to the course." -#: views/course_view.py:246 +#: views/course_view.py:247 msgid "courses.success.students.remove" msgstr "The student was successfully removed from the course." -#: views/course_view.py:291 +#: views/course_view.py:292 msgid "courses.success.teachers.add" msgstr "The teacher was successfully added to the course." -#: views/course_view.py:315 +#: views/course_view.py:316 msgid "courses.success.teachers.remove" msgstr "The teacher was successfully removed from the course." -#: views/group_view.py:73 +#: views/group_view.py:74 msgid "group.success.students.add" msgstr "The student was successfully added to the group." -#: views/group_view.py:93 +#: views/group_view.py:94 msgid "group.success.students.remove" msgstr "The student was successfully removed from the group." -#: views/group_view.py:112 +#: views/group_view.py:113 msgid "group.success.submissions.add" msgstr "The submission was successfully added to the group." @@ -292,6 +296,6 @@ msgstr "No zip file available." msgid "extra_check_result.download.log" msgstr "No log file available." -#: views/submission_view.py:59 +#: views/submission_view.py:60 msgid "extra_check_result.download.artifact" msgstr "No artifact available." diff --git a/backend/api/management/commands/teacher_join_course.py b/backend/api/management/commands/teacher_join_course.py deleted file mode 100644 index a5edf26c..00000000 --- a/backend/api/management/commands/teacher_join_course.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.core.management.base import BaseCommand -from api.models.teacher import Teacher -from api.models.course import Course - - -class Command(BaseCommand): - - help = 'make a teacher join a course' - - def add_arguments(self, parser): - parser.add_argument('username', type=str, help='The username of the teacher to join the course') - parser.add_argument('course_id', type=str, help='The id of the course you want to join') - - def handle(self, *args, **options): - username = options['username'] - course_id = options['course_id'] - teacher = Teacher.objects.filter(username=username) - - if teacher.count() == 0: - self.stdout.write(self.style.ERROR('Teacher not found, first log in !')) - return - - teacher = teacher.get() - - course = Course.objects.filter(id=course_id) - - if course.count() == 0: - self.stdout.write(self.style.ERROR('Course not found, first create it !')) - return - - course = course.get() - - teacher.courses.add(course_id) - - self.stdout.write(self.style.SUCCESS('Successfully add teacher to course!')) diff --git a/backend/api/migrations/0019_feedback.py b/backend/api/migrations/0019_feedback.py new file mode 100644 index 00000000..7c3f9d7a --- /dev/null +++ b/backend/api/migrations/0019_feedback.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.4 on 2024-05-02 15:15 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0018_course_invitation_link_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Feedback', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('message', models.TextField()), + ('creation_date', models.DateTimeField(auto_now_add=True)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedback_messages', to=settings.AUTH_USER_MODEL)), + ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedback', to='api.submission')), + ], + ), + ] diff --git a/backend/api/migrations/0024_merge_20240507_1230.py b/backend/api/migrations/0024_merge_20240507_1230.py new file mode 100644 index 00000000..aef0e84b --- /dev/null +++ b/backend/api/migrations/0024_merge_20240507_1230.py @@ -0,0 +1,14 @@ +# Generated by Django 5.0.4 on 2024-05-07 12:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0019_feedback'), + ('api', '0023_submission_zip_alter_checkresult_error_message_and_more'), + ] + + operations = [ + ] diff --git a/backend/api/migrations/0026_merge_20240518_1058.py b/backend/api/migrations/0026_merge_20240518_1058.py new file mode 100644 index 00000000..e148a5e2 --- /dev/null +++ b/backend/api/migrations/0026_merge_20240518_1058.py @@ -0,0 +1,14 @@ +# Generated by Django 5.0.4 on 2024-05-18 10:58 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0024_merge_20240507_1230'), + ('api', '0025_extracheckresult_artifact'), + ] + + operations = [ + ] diff --git a/backend/api/models/feedback.py b/backend/api/models/feedback.py new file mode 100644 index 00000000..d765b8ac --- /dev/null +++ b/backend/api/models/feedback.py @@ -0,0 +1,38 @@ +from django.db import models +from api.models.submission import Submission +from authentication.models import User + + +class Feedback(models.Model): + """Model that represents a feedback message.""" + + # ID should be generated automatically + + # Feedback message + message = models.TextField(null=False) + + # Feedback message author + author = models.ForeignKey( + User, + # If the author is deleted, the feedback message should be deleted as well + on_delete=models.CASCADE, + related_name="feedback_messages", + blank=False, + null=False, + ) + + # Feedback message creation date + creation_date = models.DateTimeField( + # The default value is the current date and time + auto_now_add=True, + blank=False, + null=False + ) + + submission = models.ForeignKey( + Submission, + on_delete=models.CASCADE, + related_name="feedback", + blank=False, + null=False + ) diff --git a/backend/api/permissions/feedback_permissions.py b/backend/api/permissions/feedback_permissions.py new file mode 100644 index 00000000..8bcdcfa9 --- /dev/null +++ b/backend/api/permissions/feedback_permissions.py @@ -0,0 +1,17 @@ +from rest_framework import permissions + +from api.permissions.role_permissions import is_teacher + + +class IsAdminOrTeacherForPatch(permissions.BasePermission): + """ + Custom permission to only allow admins to access objects in general, + but teachers can only make PATCH requests. + """ + + def has_permission(self, request, view): + if request.user.is_authenticated and request.user.is_staff: + return True + elif request.method == 'PATCH' and is_teacher(request.user): + return True + return False diff --git a/backend/api/permissions/submission_permissions.py b/backend/api/permissions/submission_permissions.py index 7180a0fa..f69c81ef 100644 --- a/backend/api/permissions/submission_permissions.py +++ b/backend/api/permissions/submission_permissions.py @@ -12,12 +12,15 @@ class SubmissionPermission(BasePermission): def has_permission(self, request: Request, view: APIView) -> bool: - if request.method not in SAFE_METHODS: + submission_id = view.kwargs.get("pk") + + if request.method not in SAFE_METHODS or submission_id is None: return False user: User = cast(User, request.user) - - return user.is_staff or is_teacher(user) or is_assistant(user) + # check if user is in group of submission + group = Submission.objects.get(id=submission_id).group + return user.is_staff or is_teacher(user) or is_assistant(user) or group.students.filter(id=user.id).exists() def has_object_permission(self, request: Request, view: APIView, obj: Submission) -> bool: if request.method not in SAFE_METHODS: diff --git a/backend/api/seeders/seeder.py b/backend/api/seeders/seeder.py index c5991fa6..3c81c82e 100644 --- a/backend/api/seeders/seeder.py +++ b/backend/api/seeders/seeder.py @@ -3,8 +3,12 @@ from time import time from typing import Literal +from api.models.submission import StructureCheckResult +from api.models.submission import ExtraCheckResult + from django.db import connection from django.utils import timezone +from django.contrib.contenttypes.models import ContentType generated_usernames = set() @@ -268,7 +272,7 @@ def seed_projects( visible_prob=80, archived_prob=10, score_visible_prob=30, - locked_groups_prob=30, + locked_groups_prob=0, min_max_score=1, max_max_score=100, min_group_size=1, @@ -435,7 +439,7 @@ def seed_structure_checks(faker, count: int = 1_500): while len(blocked_extensions) < count * 2: project = faker.pyint(min_value=0, max_value=count - 1) extension = choice(extensions)[0] - if [project, extension] not in blocked_extensions: + if ([project, extension] not in blocked_extensions) and ([project, extension] not in obligated_extensions): blocked_extensions.append([project, extension]) cursor.executemany( @@ -536,6 +540,17 @@ def seed_submission_results(faker): results = [] structure_results = [] extra_results = [] + # Get the content type for the StructureCheckResult model + structure_content_type = ContentType.objects.get_for_model(StructureCheckResult) + + # Get the ID of the content type + structure_content_type_id = structure_content_type.id + + # Get the content type for the ExtraCheckResult model + extra_content_type = ContentType.objects.get_for_model(ExtraCheckResult) + + # Get the ID of the content type + extra_content_type_id = extra_content_type.id for submission in submissions: project = next(filter(lambda group: group[0] == submission[1], groups))[1] @@ -549,6 +564,7 @@ def seed_submission_results(faker): id, result, choice(error_structure) if result == "FAILED" else None, + structure_content_type_id, submission[0], ]) @@ -564,6 +580,7 @@ def seed_submission_results(faker): id, result, choice(error_extra) if result == "FAILED" else None, + extra_content_type_id, submission[0], ]) @@ -574,7 +591,9 @@ def seed_submission_results(faker): ]) cursor.executemany( - "INSERT INTO api_checkresult(id, result, error_message, submission_id) VALUES (?, ?, ?, ?)", + "INSERT INTO api_checkresult(" + "id, result, error_message, polymorphic_ctype_id, submission_id" + ") VALUES (?, ?, ?, ?, ?)", results ) cursor.executemany( diff --git a/backend/api/serializers/feedback_serializer.py b/backend/api/serializers/feedback_serializer.py new file mode 100644 index 00000000..7ba899c1 --- /dev/null +++ b/backend/api/serializers/feedback_serializer.py @@ -0,0 +1,25 @@ +from django.utils.translation import gettext +from rest_framework import serializers +from rest_framework.exceptions import ValidationError +from api.models.feedback import Feedback +from api.permissions.role_permissions import is_teacher +from api.serializers.submission_serializer import SubmissionSerializer +from api.serializers.teacher_serializer import TeacherSerializer + + +class FeedbackSerializer(serializers.ModelSerializer): + + submission = SubmissionSerializer(read_only=True) + author = TeacherSerializer(read_only=True) + + """Serializer for the feedback message""" + def to_internal_value(self, data): + if "user" in self.context: + if not is_teacher(self.context["user"]): + raise ValidationError(gettext("feedback.error.no_teacher")) + return super().to_internal_value(data) + + class Meta: + model = Feedback + fields = "__all__" + read_only_fields = ["author", "submission"] diff --git a/backend/api/serializers/submission_serializer.py b/backend/api/serializers/submission_serializer.py index acdf26f8..adf5f42e 100644 --- a/backend/api/serializers/submission_serializer.py +++ b/backend/api/serializers/submission_serializer.py @@ -61,6 +61,10 @@ class SubmissionSerializer(serializers.ModelSerializer): many=False, read_only=True, view_name="group-detail" ) + feedback = serializers.HyperlinkedIdentityField( + many=True, read_only=True, view_name="feedback-detail" + ) + results = CheckResultPolymorphicSerializer(many=True, read_only=True) class Meta: diff --git a/backend/api/tests/test_feedback.py b/backend/api/tests/test_feedback.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/api/urls.py b/backend/api/urls.py index 85c661a6..816e0855 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -5,6 +5,7 @@ from api.views.course_view import CourseViewSet from api.views.docker_view import DockerImageViewSet from api.views.faculty_view import FacultyViewSet +from api.views.feedback_view import FeedbackViewSet from api.views.group_view import GroupViewSet from api.views.project_view import ProjectViewSet from api.views.student_view import StudentViewSet @@ -33,6 +34,8 @@ router.register(r"docker-images", DockerImageViewSet, basename="docker-image") router.register(r"structure-check-results", StructureCheckResultViewSet, basename="structure-check-result") router.register(r"extra-check-results", ExtraCheckResultViewSet, basename="extra-check-result") +router.register(r"feedback", FeedbackViewSet, basename="feedback") + urlpatterns = [ path("", include(router.urls)), diff --git a/backend/api/views/feedback_view.py b/backend/api/views/feedback_view.py new file mode 100644 index 00000000..dbf3f253 --- /dev/null +++ b/backend/api/views/feedback_view.py @@ -0,0 +1,12 @@ +from rest_framework import viewsets + +from api.models.feedback import Feedback +from api.permissions.feedback_permissions import IsAdminOrTeacherForPatch +from api.permissions.role_permissions import is_teacher +from api.serializers.feedback_serializer import FeedbackSerializer + + +class FeedbackViewSet(viewsets.ModelViewSet): + queryset = Feedback.objects.all() + serializer_class = FeedbackSerializer + permission_classes = [IsAdminOrTeacherForPatch] diff --git a/backend/api/views/group_view.py b/backend/api/views/group_view.py index cf300cfd..b4c67383 100644 --- a/backend/api/views/group_view.py +++ b/backend/api/views/group_view.py @@ -12,7 +12,7 @@ from notifications.signals import NotificationType, notification_create from rest_framework.decorators import action from rest_framework.mixins import (CreateModelMixin, DestroyModelMixin, - RetrieveModelMixin, UpdateModelMixin) + RetrieveModelMixin, UpdateModelMixin, ListModelMixin) from rest_framework.permissions import IsAdminUser from rest_framework.request import Request from rest_framework.response import Response @@ -23,6 +23,7 @@ class GroupViewSet(CreateModelMixin, RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, + ListModelMixin, GenericViewSet): queryset = Group.objects.all() diff --git a/backend/api/views/submission_view.py b/backend/api/views/submission_view.py index 37911477..aef617d7 100644 --- a/backend/api/views/submission_view.py +++ b/backend/api/views/submission_view.py @@ -1,12 +1,3 @@ -from api.models.submission import (ExtraCheckResult, StructureCheckResult, - Submission) -from api.permissions.submission_permissions import ( - ExtraCheckResultArtifactPermission, ExtraCheckResultLogPermission, - ExtraCheckResultPermission, StructureCheckResultPermission, - SubmissionPermission) -from api.serializers.submission_serializer import ( - ExtraCheckResultSerializer, StructureCheckResultSerializer, - SubmissionSerializer) from django.http import FileResponse from django.utils.translation import gettext as _ from rest_framework.decorators import action @@ -15,6 +6,14 @@ from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet +from api.models.submission import Submission, StructureCheckResult, ExtraCheckResult +from api.permissions.submission_permissions import SubmissionPermission, StructureCheckResultPermission, \ + ExtraCheckResultPermission, ExtraCheckResultArtifactPermission, ExtraCheckResultLogPermission +from api.serializers.feedback_serializer import FeedbackSerializer +from api.serializers.submission_serializer import ( + ExtraCheckResultSerializer, StructureCheckResultSerializer, + SubmissionSerializer) + class SubmissionViewSet(RetrieveModelMixin, GenericViewSet): queryset = Submission.objects.all() @@ -30,6 +29,36 @@ def zip(self, request, **__): return FileResponse(open(submission.zip.path, "rb"), as_attachment=True) + @action(detail=True, methods=["get"]) + def feedback(self, request, **_) -> Response: + """Returns all the feedback for the given submission""" + submission = self.get_object() + feedback = submission.feedback.all() + + # Serialize the feedback object + serializer = FeedbackSerializer( + feedback, many=True, context={"request": request} + ) + + return Response(serializer.data) + + @feedback.mapping.post + @feedback.mapping.put + def _add_feedback(self, request, **_) -> Response: + """Adds feedback to the given submission""" + submission = self.get_object() + context = {"request": request, "user": request.user, "submission": submission} + serializer = FeedbackSerializer(data=request.data, context=context) + + if serializer.is_valid(): + serializer.save(submission=submission) + return Response({ + "message": "Success", + "feedback": serializer.data + }) + + return Response(serializer.errors, status=400) + class StructureCheckResultViewSet(RetrieveModelMixin, GenericViewSet): queryset = StructureCheckResult.objects.all() diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts deleted file mode 100755 index 5eedf8e4..00000000 --- a/docs/.vitepress/config.mts +++ /dev/null @@ -1,31 +0,0 @@ -import { defineConfig } from "vitepress"; - -// https://vitepress.dev/reference/site-config -export default defineConfig({ - base: "/docs", - locales: { - root: { - label: "English", - lang: "en", - link: "/en", - }, - nl: { - label: "Nederlands", - lang: "nl", - link: "/nl", - }, - }, - title: "Ypovoli", - description: "A ugent site", - themeConfig: { - // https://vitepress.dev/reference/default-theme-config - nav: [ - // { text: 'Home', link: '/en/' }, - // { text: 'Examples', link: '/markdown-examples' } - ], - - socialLinks: [ - { icon: "github", link: "https://github.com/SELab-2/UGent-7" }, - ], - }, -}); diff --git a/docs/assets/course-card.png b/docs/assets/course-card.png deleted file mode 100644 index fc83c26a..00000000 Binary files a/docs/assets/course-card.png and /dev/null differ diff --git a/docs/assets/en/lang-change.png b/docs/assets/en/lang-change.png deleted file mode 100644 index 160cf69d..00000000 Binary files a/docs/assets/en/lang-change.png and /dev/null differ diff --git a/docs/assets/en/logout-button.png b/docs/assets/en/logout-button.png deleted file mode 100644 index 0d7fbfeb..00000000 Binary files a/docs/assets/en/logout-button.png and /dev/null differ diff --git a/docs/assets/en/project-card.png b/docs/assets/en/project-card.png deleted file mode 100644 index fa41b2e5..00000000 Binary files a/docs/assets/en/project-card.png and /dev/null differ diff --git a/docs/assets/en/project-list.png b/docs/assets/en/project-list.png deleted file mode 100644 index 07363551..00000000 Binary files a/docs/assets/en/project-list.png and /dev/null differ diff --git a/docs/assets/login-button.png b/docs/assets/login-button.png deleted file mode 100644 index 8b2272a0..00000000 Binary files a/docs/assets/login-button.png and /dev/null differ diff --git a/docs/assets/nl/lang-change.png b/docs/assets/nl/lang-change.png deleted file mode 100644 index 945a38fb..00000000 Binary files a/docs/assets/nl/lang-change.png and /dev/null differ diff --git a/docs/assets/nl/logout-button.png b/docs/assets/nl/logout-button.png deleted file mode 100644 index 7ecaaad5..00000000 Binary files a/docs/assets/nl/logout-button.png and /dev/null differ diff --git a/docs/assets/nl/project-card.png b/docs/assets/nl/project-card.png deleted file mode 100644 index cb60464b..00000000 Binary files a/docs/assets/nl/project-card.png and /dev/null differ diff --git a/docs/assets/nl/project-list.png b/docs/assets/nl/project-list.png deleted file mode 100644 index ed117d2a..00000000 Binary files a/docs/assets/nl/project-list.png and /dev/null differ diff --git a/docs/en/api-examples.md b/docs/en/api-examples.md deleted file mode 100644 index 6bd8bb5c..00000000 --- a/docs/en/api-examples.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -outline: deep ---- - -# Runtime API Examples - -This page demonstrates usage of some of the runtime APIs provided by VitePress. - -The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files: - -```md - - -## Results - -### Theme Data -
{{ theme }}
- -### Page Data -
{{ page }}
- -### Page Frontmatter -
{{ frontmatter }}
-``` - - - -## Results - -### Theme Data -
{{ theme }}
- -### Page Data -
{{ page }}
- -### Page Frontmatter -
{{ frontmatter }}
- -## More - -Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata). diff --git a/docs/en/markdown-examples.md b/docs/en/markdown-examples.md deleted file mode 100644 index f9258a55..00000000 --- a/docs/en/markdown-examples.md +++ /dev/null @@ -1,85 +0,0 @@ -# Markdown Extension Examples - -This page demonstrates some of the built-in markdown extensions provided by VitePress. - -## Syntax Highlighting - -VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting: - -**Input** - -````md -```js{4} -export default { - data () { - return { - msg: 'Highlighted!' - } - } -} -``` -```` - -**Output** - -```js{4} -export default { - data () { - return { - msg: 'Highlighted!' - } - } -} -``` - -## Custom Containers - -**Input** - -```md -::: info -This is an info box. -::: - -::: tip -This is a tip. -::: - -::: warning -This is a warning. -::: - -::: danger -This is a dangerous warning. -::: - -::: details -This is a details block. -::: -``` - -**Output** - -::: info -This is an info box. -::: - -::: tip -This is a tip. -::: - -::: warning -This is a warning. -::: - -::: danger -This is a dangerous warning. -::: - -::: details -This is a details block. -::: - -## More - -Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown). diff --git a/docs/nl/api-examples.md b/docs/nl/api-examples.md deleted file mode 100644 index 6bd8bb5c..00000000 --- a/docs/nl/api-examples.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -outline: deep ---- - -# Runtime API Examples - -This page demonstrates usage of some of the runtime APIs provided by VitePress. - -The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files: - -```md - - -## Results - -### Theme Data -
{{ theme }}
- -### Page Data -
{{ page }}
- -### Page Frontmatter -
{{ frontmatter }}
-``` - - - -## Results - -### Theme Data -
{{ theme }}
- -### Page Data -
{{ page }}
- -### Page Frontmatter -
{{ frontmatter }}
- -## More - -Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata). diff --git a/docs/nl/markdown-examples.md b/docs/nl/markdown-examples.md deleted file mode 100644 index f9258a55..00000000 --- a/docs/nl/markdown-examples.md +++ /dev/null @@ -1,85 +0,0 @@ -# Markdown Extension Examples - -This page demonstrates some of the built-in markdown extensions provided by VitePress. - -## Syntax Highlighting - -VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting: - -**Input** - -````md -```js{4} -export default { - data () { - return { - msg: 'Highlighted!' - } - } -} -``` -```` - -**Output** - -```js{4} -export default { - data () { - return { - msg: 'Highlighted!' - } - } -} -``` - -## Custom Containers - -**Input** - -```md -::: info -This is an info box. -::: - -::: tip -This is a tip. -::: - -::: warning -This is a warning. -::: - -::: danger -This is a dangerous warning. -::: - -::: details -This is a details block. -::: -``` - -**Output** - -::: info -This is an info box. -::: - -::: tip -This is a tip. -::: - -::: warning -This is a warning. -::: - -::: danger -This is a dangerous warning. -::: - -::: details -This is a details block. -::: - -## More - -Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown). diff --git a/frontend/src/assets/lang/app/en.json b/frontend/src/assets/lang/app/en.json index e15bc785..e74820f7 100644 --- a/frontend/src/assets/lang/app/en.json +++ b/frontend/src/assets/lang/app/en.json @@ -108,8 +108,14 @@ "title": "Submissions", "submit": "Submit", "course": "Course", + "file": "file", + "files": "files", "chooseFile": "Choose a file", "noSubmissions": "No submissions available", + "passed": "Passed", + "failed": "The submission is not passed.", + "downloadZip": "Download all files of this submission as a zip.", + "downloadLog": "Log of check {0}", "hoverText": { "allChecksFailed": "Structure and extra checks failed", "allChecksPassed": "All checks passed", @@ -121,6 +127,25 @@ "daysAgo": "day(s) ago", "weekAgo": "More than a week ago", "monthAgo": "More than a month ago" + }, + "feedback": { + "writeFeedback": "Write your feedback here", + "addFeedback": "Add feedback", + "dateAndAuthor": "On {0} wrote {1}:", + "edit": "Edit feedback", + "noFeedback": "No feedback given yet" + }, + "error": { + "blockedExtension": "There is a blocked extension in your solution", + "obligatedExtensionNotFound": "Obligated extension not found in your solution", + "fileDirNotFound": "File directory not found in your solution", + "dockerImageError": "Error in Docker image", + "timeLimit": "Time limit exceeded", + "memoryLimit": "Memory limit exceeded", + "checkError": "Check error", + "runtimeError": "Runtime error", + "unknown": "Unknown error", + "failedStructureCheck": "Structure check failed" } }, "courses": { @@ -180,6 +205,9 @@ "networkDetail": "Unable to reach the server.", "request": "Request error", "requestDetail": "An error occurred while creating the request." + }, + "success": { + "creation": "A {0} is correctly created." } } }, diff --git a/frontend/src/assets/lang/app/nl.json b/frontend/src/assets/lang/app/nl.json index 20760948..42608062 100644 --- a/frontend/src/assets/lang/app/nl.json +++ b/frontend/src/assets/lang/app/nl.json @@ -109,8 +109,14 @@ "title": "Indieningen", "submit": "Indienen", "course": "Vak", + "file": "Bestand", + "files": "Bestanden", "chooseFile": "Kies bestand(en)", "noSubmissions": "Geen indieningen beschikbaar", + "passed": "Geslaagd", + "failed": "De indiening is mislukt", + "downloadZip": "Download alle bestanden als zip.", + "downloadLog": "Log bestand van check {0}", "hoverText": { "allChecksFailed": "Structuur en extra checks gefaald", "allChecksPassed": "Alle checks geslaagd", @@ -122,7 +128,27 @@ "daysAgo": "dag(en) geleden", "weekAgo": "Meer dan een week geleden", "monthAgo": "Meer dan een maand geleden" - } + }, + "feedback": { + "addFeedback": "Feedback toevoegen", + "writeFeedback": "Schrijf hier je feedback", + "dateAndAuthor": "Geschreven op {0} door {1}", + "edit": "Bewerk feedback", + "noFeedback": "Geen feedback beschikbaar" + }, + "error":{ + "blockedExtension": "Er zit een geblokkeerde extensie in je oplossing", + "obligatedExtensionNotFound": "Verplichte extensie niet gevonden in je oplossing", + "fileDirNotFound": "Bestandsdirectory niet gevonden in je oplossing", + "DockerImageError": "Fout in Docker-afbeelding", + "timeLimit": "Tijdslimiet overschreden", + "memoryLimit": "Geheugenlimiet overschreden", + "checkError": "Controlefout", + "runtimeError": "Runtime fout", + "unknown": "Onbekende fout", + "failedStructureCheck": "Structuurcontrole mislukt" + } + }, "courses": { "create": "Creƫer vak", @@ -181,7 +207,10 @@ "networkDetail": "Kan de server niet bereiken.", "request": "Fout verzoek", "requestDetail": "Een fout vond plaats tijdens het maken van het verzoek." - } + }, + "success": { + "creation": "Een {0} is correct aangemaakt." + } } }, "components": { diff --git a/frontend/src/assets/lang/info/en.json b/frontend/src/assets/lang/info/en.json index f376b91d..9e26dfee 100644 --- a/frontend/src/assets/lang/info/en.json +++ b/frontend/src/assets/lang/info/en.json @@ -1,8 +1 @@ -{ - "header": { - "student": "Student", - "teacher": "Teacher", - "assistant": "Assistant", - "admin": "Admin" - } -} \ No newline at end of file +{} \ No newline at end of file diff --git a/frontend/src/components/projects/SubmissionCard.vue b/frontend/src/components/projects/SubmissionCard.vue new file mode 100644 index 00000000..a389e14c --- /dev/null +++ b/frontend/src/components/projects/SubmissionCard.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/frontend/src/components/submissions/AllSubmission.vue b/frontend/src/components/submissions/AllSubmission.vue index 72b2be1a..c4ea7b91 100644 --- a/frontend/src/components/submissions/AllSubmission.vue +++ b/frontend/src/components/submissions/AllSubmission.vue @@ -1,75 +1,22 @@