Skip to content

Commit

Permalink
Merge branch 'develop' into fix-page-loading
Browse files Browse the repository at this point in the history
# Conflicts:
#	frontend/app/[locale]/components/CourseControls.tsx
#	frontend/app/[locale]/components/ProjectDetailsPage.tsx
#	frontend/app/[locale]/components/SubmitDetailsPage.tsx
  • Loading branch information
Thibaud-Collyn committed May 20, 2024
2 parents bf8e4b7 + bd0811a commit 0435818
Show file tree
Hide file tree
Showing 20 changed files with 345 additions and 100 deletions.
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,8 @@ componenttest:
docker exec -it pigeonhole-frontend npx jest

silentcomponenttest:
docker exec -it pigeonhole-frontend npx jest --silent
docker exec -it pigeonhole-frontend npx jest --silent

resetdb:
docker exec pigeonhole-backend python manage.py flush --noinput
docker exec -it pigeonhole-backend python manage.py runscript mockdata
67 changes: 15 additions & 52 deletions backend/pigeonhole/apps/submissions/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,11 @@ def create(self, request, *args, **kwargs):
else:
violations = check_restrictions(file_urls, project.file_structure.split(","))

if not violations:
complete_message = {"message": "Submission successful"}
if not violations[0] and not violations[2]:
complete_message = {"success": 0}
else:
complete_message = {"message": ", ".join(violations)}
violations.update({'success': 1})
complete_message = violations

return Response(complete_message, status=status.HTTP_201_CREATED)

Expand Down Expand Up @@ -228,63 +229,25 @@ def get_project(self, request, *args, **kwargs):


def check_restrictions(filenames, restrictions):
violations = []
# 0: Required file not found
# 1: Required file found
# 2: Forbidden file found
# 3: No forbidden file found
violations = {0: [], 1: [], 2: [], 3: []}
for restriction_ in restrictions:
restriction = restriction_.strip()
if restriction.startswith('+'):
pattern = restriction[1:]
matching_files = fnmatch.filter(filenames, pattern)
if not matching_files:
violations.append(f"Error: Required file matching pattern '{pattern}' not found.")
violations[0].append(pattern)
else:
violations[1].append(pattern)
elif restriction.startswith('-'):
pattern = restriction[1:]
matching_files = fnmatch.filter(filenames, pattern)
if matching_files:
violations.append(
f"Error: Forbidden file matching pattern '{pattern}' found: {', '.join(matching_files)}.")
else:
violations.append(f"Error: Invalid restriction '{restriction}'.")
violations[2].append(pattern)
else:
violations[3].append(pattern)
return violations

# parsed_submission_files = []
# for file_path in request.FILES.keys():
# if "/" in file_path:
# index = file_path.rfind("/")
# parsed_submission_files.append(file_path[index + 1:])
# else:
# if file_path != "fileList":
# parsed_submission_files.append(file_path)
#
# # example parsed_submission_files = "+extra/verslag.pdf", "-src/*.jar"
#
# if project.file_structure != "" and project.file_structure is not None:
# for condition in project.file_structure.split(","):
# stripped_condition = condition.strip() # condition without whitespace
# if stripped_condition[0] == "+": # check if starts with "+" (file has to be included)
# if "*" in stripped_condition: # check if there is a wildcard
# index = stripped_condition.index("*")
# wildcard_submission = stripped_condition[index + 1:] # "*.py/results"
# wildcard_directory = stripped_condition[1:index] # "/project/"
# for file_to_check in parsed_submission_files: # "/project/main.py/results"
# if wildcard_directory in file_to_check:
# cwd = file_to_check[len(wildcard_directory):]
# file_extension = cwd[cwd.index("."):]
# if file_extension != wildcard_submission:
# message.append(f"File {file_to_check} is not allowed")
# else:
# if stripped_condition[1:] not in parsed_submission_files:
# message.append(f"File {stripped_condition[1:]} not found")
# else:
# if "*" in stripped_condition: # check if there is a wildcard
# index = stripped_condition.index("*")
# wildcard_submission = stripped_condition[index + 1:] # "*.py/results"
# wildcard_directory = stripped_condition[1:index] # "/project/"
# for file_to_check in parsed_submission_files: # "/project/main.py/results"
# if wildcard_directory in file_to_check:
# cwd = file_to_check[len(wildcard_directory):]
# file_extension = cwd[cwd.index("."):]
# if file_extension == wildcard_submission:
# message.append(f"File {file_to_check} is not allowed")
# else:
# if stripped_condition[1:] not in parsed_submission_files:
# message.append(f"File {stripped_condition[1:]} not found")
23 changes: 13 additions & 10 deletions backend/pigeonhole/tests/test_views/test_submission/test_student.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,46 +84,49 @@ def setUp(self):

def test_can_create_submission_without(self):
response = self.client.post(API_ENDPOINT, {"group_id": self.group_1.group_id, "file_urls": ""})
self.assertIn("\'extra/verslag.pdf\' not found.", response.data['message'])
self.assertEqual(1, response.data['success'])
self.assertEqual("extra/verslag.pdf", response.data[0][0])
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

def test_can_create_submission_withfile(self):
response = self.client.post(API_ENDPOINT, {"group_id": self.group_1.group_id, "file_urls": "extra/verslag.pdf"})
self.assertEqual("Submission successful", response.data['message'])
self.assertEqual(0, response.data['success'])
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

def test_can_create_submission_without_forbidden(self):
response = self.client.post(API_ENDPOINT, {"group_id": self.group_2.group_id, "file_urls": ""})
self.assertIn("Submission successful", response.data['message'])
self.assertEqual(0, response.data['success'])
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

def test_can_create_submission_with_forbidden(self):
response = self.client.post(API_ENDPOINT, {"group_id": self.group_2.group_id, "file_urls": "extra/verslag.pdf"})
self.assertEqual("Error: Forbidden file matching pattern 'extra/verslag.pdf' found: extra/verslag.pdf.",
response.data['message'])
self.assertEqual(1, response.data['success'])
self.assertIn("extra/verslag.pdf", response.data[2])
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

def test_can_create_submission_without_wildcard(self):
response = self.client.post(API_ENDPOINT, {"group_id": self.group_3.group_id,
"file_urls": "src/main.jar, src/test.dockerfile"})
self.assertIn("\'src/*.py\' not found.", response.data['message'])
self.assertEqual(1, response.data['success'])
self.assertIn("src/*.py", response.data[0])
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

def test_can_create_submission_with_wildcard(self):
response = self.client.post(API_ENDPOINT, {"group_id": self.group_3.group_id, "file_urls": "src/main.py"})
self.assertEqual("Submission successful", response.data['message'])
self.assertEqual(0, response.data['success'])
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

def test_can_create_submission_without_forbidden_wildcard(self):
response = self.client.post(API_ENDPOINT, {"group_id": self.group_4.group_id,
"file_urls": "src/main.jar, src/test.dockerfile"})
self.assertIn("Submission successful", response.data['message'])
self.assertEqual(0, response.data['success'])
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

#
def test_can_create_submission_with_forbidden_wildcard(self):
response = self.client.post(API_ENDPOINT, {"group_id": self.group_4.group_id, "file_urls": "src/main.py"})
self.assertEqual("Error: Forbidden file matching pattern 'src/*.py' found: src/main.py.",
response.data['message'])
self.assertEqual(1, response.data['success'])
self.assertIn("src/*.py", response.data[2])
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

def test_cant_create_invalid_submission(self):
Expand Down
2 changes: 1 addition & 1 deletion frontend/__test__/project/edit/Requiredfiles.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('Requiredfiles', () => {
files={["First", "Second"]}
setFiles={jest.fn()}
translations={translations.en}
/>
file_status={["+", "-"]} setFileStatus={jest.fn()}/>
);

// check that the required files were rendered properly
Expand Down
15 changes: 8 additions & 7 deletions frontend/app/[locale]/components/CourseBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, {useEffect, useState} from 'react';
import {Box, Typography, Skeleton} from "@mui/material";
import EditCourseButton from "@app/[locale]/components/EditCourseButton";
import {APIError, Course, getCourse, UserData, getUserData} from "@lib/api";
import defaultBanner from "../../../public/ugent_banner.png";

interface CourseBannerProps {
course_id: number;
Expand Down Expand Up @@ -46,16 +47,16 @@ const CourseBanner = ({course_id}: CourseBannerProps) => {
) : (
<Box
sx={{
backgroundColor: 'primary.main',
color: 'white',
backgroundImage: `url(${course?.banner || defaultBanner})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
color: 'whiteS',
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
justifyContent: 'space-between',
justifyContent: 'center',
alignItems: 'center',
height: { xs: 'auto', sm: '150px' },
padding: 2,
height: '150px',
borderRadius: '16px',
textAlign: 'center',
}}
>
<Box
Expand Down
79 changes: 68 additions & 11 deletions frontend/app/[locale]/components/ProjectDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'
import React, { useEffect, useState } from "react";
import { getProject, getUserData, Project, UserData } from "@lib/api";
import {checkGroup, getGroup, getProject, getUserData, Project, UserData} from "@lib/api";
import { useTranslation } from "react-i18next";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
Expand Down Expand Up @@ -37,6 +37,7 @@ const ProjectDetailsPage: React.FC<ProjectDetailsPageProps> = ({
const [user, setUser] = useState<UserData | null>(null);
const [isExpanded, setIsExpanded] = useState(false);
const [loadingUser, setLoadingUser] = useState(true);
const [isInGroup, setIsInGroup] = useState(false);
const previewLength = 300;
const deadlineColorType = project?.deadline
? checkDeadline(project.deadline)
Expand Down Expand Up @@ -67,6 +68,7 @@ const ProjectDetailsPage: React.FC<ProjectDetailsPageProps> = ({
};

fetchProject().then(() => setLoadingProject(false));
checkGroup(project_id).then((response) => setIsInGroup(response));
}, [project_id]);

useEffect(() => {
Expand Down Expand Up @@ -195,7 +197,16 @@ const ProjectDetailsPage: React.FC<ProjectDetailsPageProps> = ({
</IconButton>
)}
<Typography variant="h6">{t("required_files")}</Typography>
<Typography>{project?.file_structure}</Typography>
<Typography variant={"body1"}>
<pre>
{generateDirectoryTree(project?.file_structure).split('\n').map((line: string, index: number) => (
<React.Fragment key={index}>
{line}
<br/>
</React.Fragment>
))}
</pre>
</Typography>
<Typography variant="h6">{t("conditions")}</Typography>
<Typography>{project?.conditions}</Typography>
<Typography>
Expand Down Expand Up @@ -231,15 +242,21 @@ const ProjectDetailsPage: React.FC<ProjectDetailsPageProps> = ({
</Typography>
</div>
{user?.role === 3 ? (
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
href={`/${locale}/project/${project_id}/submit`}
sx={{ my: 1 }}
>
{t("add_submission")}
</Button>
isInGroup ? (
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
href={`/${locale}/project/${project_id}/submit`}
sx={{ my: 1 }}
>
{t("add_submission")}
</Button>
) : (
<Typography variant="body1" style={{ color: "red", marginTop: "5px" }}>
{t("not_in_group")}
</Typography>
)
) : null}
</Grid>
<Grid item xs={12}>
Expand All @@ -263,4 +280,44 @@ const ProjectDetailsPage: React.FC<ProjectDetailsPageProps> = ({
);
};

function buildTree(paths) {
const tree = {};
const paths_list = paths.split(',');
paths_list.forEach(path => {
const parts = path.split('/');
let current = tree;

parts.forEach((part, index) => {
if (!current[part]) {
if (index === parts.length - 1) {
current[part] = {};
} else {
current[part] = current[part] || {};
}
}
current = current[part];
});
});

return tree;
}

function buildTreeString(tree, indent = '') {
let treeString = '';

const keys = Object.keys(tree);
keys.forEach((key, index) => {
const isLast = index === keys.length - 1;
treeString += `${indent}${isLast ? '└── ' : '├── '}${key}\n`;
treeString += buildTreeString(tree[key], indent + (isLast ? ' ' : '│ '));
});

return treeString;
}

function generateDirectoryTree(filePaths) {
const tree = buildTree(filePaths);
return `.\n${buildTreeString(tree)}`;
}

export default ProjectDetailsPage;
57 changes: 57 additions & 0 deletions frontend/app/[locale]/components/StatusButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {ClearIcon } from '@mui/x-date-pickers/icons';
import React, { useState } from 'react';
import CheckIcon from "@mui/icons-material/Check";
import {Button, Typography} from "@mui/material";
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";

interface StatusButtonProps {
files: any[],
setFiles: (value: (((prevState: any[]) => any[]) | any[])) => void,
fileIndex: number,
}

function StatusButton(
{files, setFiles, fileIndex}: StatusButtonProps,
) {
const [statusIndex, setStatusIndex] = useState(getStart(files[fileIndex]));
const statuses = [
{ icon: <CheckIcon style={{ color: '#66bb6a' }} /> },
{ icon: <HelpOutlineIcon style={{ color: '#000000' }} />},
{ icon: <ClearIcon style={{ color: '#ef5350' }} /> },
];
const status_valeus = ['+', '~', '-'];

const handleClick = () => {
const newStatusIndex = (statusIndex + 1) % statuses.length;
setStatusIndex(newStatusIndex);
const newFiles = [...files];
newFiles[fileIndex] = status_valeus[newStatusIndex];
setFiles(newFiles);
};

return (
<Button
variant="contained"
onClick={handleClick}
style={{
border: 'none',
backgroundColor: 'transparent',
margin: 10,
}}
>
{statuses[statusIndex].icon}
</Button>
);
}

function getStart(file: string) {
if (file[0] === '+') {
return 0;
} else if (file[0] === '~') {
return 1;
} else {
return 2;
}
}

export default StatusButton;
Loading

0 comments on commit 0435818

Please sign in to comment.