diff --git a/backend/pigeonhole/apps/projects/permissions.py b/backend/pigeonhole/apps/projects/permissions.py index 5e03ef11..e05c47a4 100644 --- a/backend/pigeonhole/apps/projects/permissions.py +++ b/backend/pigeonhole/apps/projects/permissions.py @@ -24,7 +24,9 @@ def has_permission(self, request, view): return False elif view.action in ['get_group_submissions']: return True - elif view.action in ['download_submissions', 'download_testfiles']: + elif view.action in ['download_submissions', 'download_testfiles', + 'get_last_group_submissions', + 'get_submissions']: return user.is_teacher or user.is_admin or user.is_superuser elif view.action in ['get_group']: return True diff --git a/backend/pigeonhole/apps/projects/views.py b/backend/pigeonhole/apps/projects/views.py index dae5b424..8f606cab 100644 --- a/backend/pigeonhole/apps/projects/views.py +++ b/backend/pigeonhole/apps/projects/views.py @@ -222,6 +222,28 @@ def get_last_submission(self, request, *args, **kwargs): SubmissionsSerializer(submissions.first()).data, status=status.HTTP_200_OK ) + @action(detail=True, methods=["get"]) + def get_last_group_submissions(self, request, *args, **kwargs): + project = self.get_object() + groups = Group.objects.filter(project_id=project) + latest_submission_ids = [] + + for group in groups: + latest_submission = Submissions.objects.filter(group_id=group).order_by('-timestamp').first() + if latest_submission: + latest_submission_ids.append(latest_submission.submission_id) + + queryset = Submissions.objects.filter(submission_id__in=latest_submission_ids) + submissions_filter = SubmissionFilter(request.GET, queryset=queryset) + filtered_submissions = submissions_filter.qs + paginator = CustomPageNumberPagination() + paginated_submissions = paginator.paginate_queryset( + filtered_submissions, request + ) + + serializer = SubmissionsSerializer(paginated_submissions, many=True) + return paginator.get_paginated_response(serializer.data) + @action(detail=True, methods=["get"]) def download_submissions(self, request, *args, **kwargs): project = self.get_object() diff --git a/backend/pigeonhole/tests/test_views/test_user/test_teacher.py b/backend/pigeonhole/tests/test_views/test_user/test_teacher.py index e44e80c5..326f1926 100644 --- a/backend/pigeonhole/tests/test_views/test_user/test_teacher.py +++ b/backend/pigeonhole/tests/test_views/test_user/test_teacher.py @@ -60,7 +60,7 @@ def test_update_self(self): 'course': [self.course.course_id] } response = self.client.put(f'{API_ENDPOINT}{self.teacher.id}/', updated_data, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_delete_self(self): response = self.client.delete(f'{API_ENDPOINT}{self.teacher.id}/') diff --git a/frontend/app/[locale]/components/LatestSubmissionList.tsx b/frontend/app/[locale]/components/LatestSubmissionList.tsx new file mode 100644 index 00000000..01644829 --- /dev/null +++ b/frontend/app/[locale]/components/LatestSubmissionList.tsx @@ -0,0 +1,42 @@ +"use client" + +import ListView from "@app/[locale]/components/ListView"; +import React from "react"; +import {useTranslation} from "react-i18next"; +import GroupsIcon from '@mui/icons-material/Groups'; +import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'; +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; + + +const LatestSubmissionList = ({project_id, page_size = 5, search}: { + project_id: number, + page_size: number, + search: string +}) => { + const {t} = useTranslation() + const headers = [ + {" " + t('group_number')} + , + {" " + t('submission_date')} + , + {" " + t('Status')} + , ""] + const headers_backend = ["group_nr", "submission_date", "status", ""] + const sortable = [true, true, false] + + return ( + + ) +} + +export default LatestSubmissionList; \ No newline at end of file diff --git a/frontend/app/[locale]/components/ListView.tsx b/frontend/app/[locale]/components/ListView.tsx index d2b40b73..84be9180 100644 --- a/frontend/app/[locale]/components/ListView.tsx +++ b/frontend/app/[locale]/components/ListView.tsx @@ -46,7 +46,7 @@ import { getUsers, postData, getOpenCourses, - fetchUserData + fetchUserData, getLatestSubmissions } from '@lib/api'; import baseTheme from "../../../styles/theme"; import {useTranslation} from "react-i18next"; @@ -191,6 +191,7 @@ const ListView: NextPage = ({ }, 'submissions': (data) => [data.submission_id, data.group_id, convertDate(t, data.timestamp), data?.output_simple_test && data?.eval_result], 'submissions_group': (data) => [data.submission_id, data.group_id, convertDate(t, data.timestamp), data?.output_simple_test && data?.eval_result], + 'submissions_latest': (data) => [data.submission_id, data.group_id, convertDate(t, data.timestamp), data?.output_simple_test && data?.eval_result], 'archived_courses': (data) => [data.course_id, data.name, data.description, data.open_course], }; @@ -219,6 +220,9 @@ const ListView: NextPage = ({ 'submissions_group': async () => { return parse_pages(await getGroupSubmissions(get_id, currentPage, page_size, searchTerm, sortConfig.key.toLowerCase(), sortConfig.direction === 'asc' ? 'asc' : 'desc')); }, + 'submissions_latest': async () => { + return parse_pages(await getLatestSubmissions(get_id, currentPage, page_size, searchTerm, sortConfig.key.toLowerCase(), sortConfig.direction === 'asc' ? 'asc' : 'desc')); + }, 'archived_courses': async () => { return parse_pages(await getArchivedCourses(currentPage, page_size, searchTerm)); } @@ -591,7 +595,7 @@ const ListView: NextPage = ({ )} - {(get == 'submissions' || get == 'submissions_group') && ( + {(get == 'submissions' || get == 'submissions_group' || get == 'submissions_latest') && (