diff --git a/backend/.flake8 b/backend/.flake8 index ee3e436d..9c2d9f85 100644 --- a/backend/.flake8 +++ b/backend/.flake8 @@ -3,7 +3,7 @@ # Ignore unused imports ignore = F401 -max-line-length = 119 +max-line-length = 120 max-complexity = 10 diff --git a/backend/api/fixtures/checks.yaml b/backend/api/fixtures/checks.yaml index ac162764..134302e7 100644 --- a/backend/api/fixtures/checks.yaml +++ b/backend/api/fixtures/checks.yaml @@ -1,15 +1,3 @@ -- model: api.structurecheck - pk: 1 - fields: - name: '.' - project: 123456 - obligated_extensions: - - 3 - - 4 - blocked_extensions: - - 1 - - 2 - - model: api.extracheck pk: 1 fields: diff --git a/backend/api/helpers/__init__.py b/backend/api/helpers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/api/helpers/check_folder_structure.py b/backend/api/helpers/check_folder_structure.py new file mode 100644 index 00000000..b1bf70a0 --- /dev/null +++ b/backend/api/helpers/check_folder_structure.py @@ -0,0 +1,194 @@ +import zipfile +import os +from api.models.checks import StructureCheck +from api.models.extension import FileExtension +from django.utils.translation import gettext +from django.conf import settings + + +def parse_zip_file(project, dir_path): # TODO block paths that start with .. + dir_path = os.path.normpath(os.path.join(settings.MEDIA_ROOT, dir_path)) + struct = get_zip_structure(dir_path) + + sorted_keys = sorted(struct.keys()) + + for key in sorted_keys: + value = struct[key] + check = StructureCheck.objects.create( + name=key, + project=project + ) + for ext in value["obligated_extensions"]: + extensie, _ = FileExtension.objects.get_or_create( + extension=ext + ) + check.obligated_extensions.add(extensie.id) + project.structure_checks.add(check) + + +# TODO block paths that start with .. +def check_zip_file(project, dir_path, restrict_extra_folders=False): + dir_path = os.path.normpath(os.path.join(settings.MEDIA_ROOT, dir_path)) + project_structure_checks = StructureCheck.objects.filter(project=project.id) + structuur = {} + for struct in project_structure_checks: + structuur[struct.name] = { + 'obligated_extensions': set(), + 'blocked_extensions': set() + } + for ext in struct.obligated_extensions.all(): + structuur[struct.name]["obligated_extensions"].add(ext.extension) + for ext in struct.blocked_extensions.all(): + structuur[struct.name]["blocked_extensions"].add(ext.extension) + return check_zip_structure( + structuur, dir_path, restrict_extra_folders=restrict_extra_folders) + + +def get_parent_directories(dir_path): + components = dir_path.split('/') + parents = set() + current_path = "" + for component in components: + if current_path != "": + current_path = current_path + "/" + component + else: + current_path = component + parents.add(current_path) + return parents + + +def list_zip_directories(zip_file_path): + """ + List all directories in a zip file. + :param zip_file_path: Path where the zip file is located. + :return: List of directory names. + """ + dirs = set() + with zipfile.ZipFile(zip_file_path, 'r') as zip_ref: + info_list = zip_ref.infolist() + for file in info_list: + if file.is_dir(): + dir_path = file.filename[:-1] + parent_dirs = get_parent_directories(dir_path) + dirs.update(parent_dirs) + return dirs + + +def get_zip_structure(root_path): + directory_structure = {} + base, _ = os.path.splitext(root_path) + inhoud = list_zip_directories(root_path) + inhoud.add(".") + for inh in inhoud: + directory_structure[inh] = { + 'obligated_extensions': set(), + 'blocked_extensions': set() + } + with zipfile.ZipFile(root_path, 'r') as zip_file: + file_names = zip_file.namelist() + for file_name in file_names: + parts = file_name.rsplit('/', 1) + if len(parts) == 2: + map, file = parts + _, file_extension = os.path.splitext(file) + file_extension = file_extension[1:] + if not file_extension == "": + directory_structure[map]["obligated_extensions"].add(file_extension) + else: + _, file_extension = os.path.splitext(file_name) + file_extension = file_extension[1:] + directory_structure["."]["obligated_extensions"].add(file_extension) + return directory_structure + + +def check_zip_content( + root_path, + dir_path, + obligated_extensions, + blocked_extensions): + """ + Check the content of a directory without traversing subdirectories. + parameters: + dir_path: The path of the zip we need to check. + obligated_extensions: The file extensions that are obligated to be present. + blocked_extensions: The file extensions that are forbidden to be present. + :return: + True: All checks pass. + False: At least 1 blocked extension is found + or 1 obligated extension is not found. + """ + dir_path = dir_path.replace('\\', '/') + with zipfile.ZipFile(root_path, 'r') as zip_file: + zip_contents = set(zip_file.namelist()) + found_obligated = set() # To track found obligated extensions + if dir_path == ".": + files_in_subdirectory = [file for file in zip_contents if "/" not in file] + else: + files_in_subdirectory = [ + file[len(dir_path) + 1:] for file in zip_contents + if file.startswith(dir_path) and '/' not in file[len(dir_path) + 1:] and bool(file[len(dir_path) + 1:])] + + for file in files_in_subdirectory: + _, file_extension = os.path.splitext(file) + # file_extension[1:] to remove the . + file_extension = file_extension[1:] + + if file_extension in blocked_extensions: + # print( + # f"Error: {file_extension} found in + # '{dir_path}' is not allowed.") TODO + return False, gettext( + 'zip.errors.invalid_structure.blocked_extension_found') + elif file_extension in obligated_extensions: + found_obligated.add(file_extension) + if set(obligated_extensions) <= found_obligated: + return True, gettext('zip.success') + else: + return False, gettext( + 'zip.errors.invalid_structure.obligated_extension_not_found') + + +def check_zip_structure( + folder_structure, + zip_file_path, + restrict_extra_folders=False): + """ + Check the structure of a zip file. + + parameters: + zip_file_path: Path to the zip file. + folder_structure: Dictionary representing the expected folder structure. + :return: + True: Zip file structure matches the expected structure. + False: Zip file structure does not match the expected structure. + """ + base, _ = os.path.splitext(zip_file_path) + struc = [f for f in folder_structure.keys() if not f == "."] + + dirs = list_zip_directories(zip_file_path) + for dir in struc: + if dir not in dirs: + # print(f"Error: Directory '{dir}' not defined.") TODO + return False, gettext( + 'zip.errors.invalid_structure.directory_not_defined') + + for directory, info in folder_structure.items(): + obligated_extensions = info.get('obligated_extensions', set()) + blocked_extensions = info.get('blocked_extensions', set()) + + result, message = check_zip_content( + zip_file_path, + directory, + obligated_extensions, + blocked_extensions) + if not result: + return result, message + # Check for any directories not present in the folder structure dictionary + if restrict_extra_folders: + for actual_directory in dirs: + if actual_directory not in struc: + # print(f"Error: Directory '{actual_directory}' + # not defined in the folder structure dictionary.") TODO + return False, gettext( + 'zip.errors.invalid_structure.directory_not_found_in_template') + return True, gettext('zip.success') diff --git a/backend/api/models/project.py b/backend/api/models/project.py index 88e587fc..839907a1 100644 --- a/backend/api/models/project.py +++ b/backend/api/models/project.py @@ -1,4 +1,3 @@ -from datetime import datetime from django.db import models from django.utils import timezone from api.models.course import Course @@ -21,7 +20,7 @@ class Project(models.Model): start_date = models.DateTimeField( # The default value is the current date and time - default=datetime.now, + default=timezone.now, blank=True, ) diff --git a/backend/api/serializers/project_serializer.py b/backend/api/serializers/project_serializer.py index f9c458d5..5f4ee39f 100644 --- a/backend/api/serializers/project_serializer.py +++ b/backend/api/serializers/project_serializer.py @@ -7,12 +7,14 @@ class ProjectSerializer(serializers.ModelSerializer): many=False, read_only=True, view_name="course-detail" ) - structure_checks = serializers.HyperlinkedRelatedField( - many=True, read_only=True, view_name="structure-check-detail" + structure_checks = serializers.HyperlinkedIdentityField( + view_name="project-structure-checks", + read_only=True ) - extra_checks = serializers.HyperlinkedRelatedField( - many=True, read_only=True, view_name="extra-check-detail" + extra_checks = serializers.HyperlinkedIdentityField( + view_name="project-extra-checks", + read_only=True ) groups = serializers.HyperlinkedIdentityField( diff --git a/backend/api/tests/test_checks.py b/backend/api/tests/test_checks.py index b66251b8..d1c66580 100644 --- a/backend/api/tests/test_checks.py +++ b/backend/api/tests/test_checks.py @@ -7,6 +7,7 @@ from api.models.extension import FileExtension from api.models.project import Project from api.models.course import Course +from django.conf import settings def create_fileExtension(id, extension): @@ -300,4 +301,4 @@ def test_extra_checks_exists(self): # Assert the details of the retrieved Checks match the created Checks retrieved_checks = content_json[0] self.assertEqual(int(retrieved_checks["id"]), checks.id) - self.assertEqual(retrieved_checks["run_script"], "http://testserver" + checks.run_script.url) + self.assertEqual(retrieved_checks["run_script"], settings.TESTING_BASE_LINK + checks.run_script.url) diff --git a/backend/api/tests/test_file_structure.py b/backend/api/tests/test_file_structure.py new file mode 100644 index 00000000..d16b2ea2 --- /dev/null +++ b/backend/api/tests/test_file_structure.py @@ -0,0 +1,216 @@ +import os +import json +from django.utils import timezone +from django.urls import reverse +from rest_framework.test import APITestCase +from api.helpers.check_folder_structure import check_zip_file, parse_zip_file +from api.models.checks import StructureCheck +from api.models.extension import FileExtension +from api.models.course import Course +from api.models.project import Project +from authentication.models import User +from django.conf import settings + + +def create_course(id, name, academic_startyear): + """ + Create a Course with the given arguments. + """ + return Course.objects.create( + id=id, name=name, academic_startyear=academic_startyear + ) + + +def create_file_extension(extension): + """ + Create a FileExtension with the given arguments. + """ + return FileExtension.objects.create(extension=extension) + + +def create_project(name, description, visible, archived, days, course): + """Create a Project with the given arguments.""" + deadline = timezone.now() + timezone.timedelta(days=days) + + return Project.objects.create( + name=name, + description=description, + visible=visible, + archived=archived, + deadline=deadline, + course=course, + ) + + +def create_structure_check(name, project, obligated, blocked): + """ + Create a StructureCheck with the given arguments. + """ + structure_check = StructureCheck.objects.create( + name=name, + project=project, + ) + for ch in obligated: + structure_check.obligated_extensions.add(ch) + for ch in blocked: + structure_check.blocked_extensions.add(ch) + + return structure_check + + +class FileTestsTests(APITestCase): + def setUp(self): + self.client.force_authenticate( + User.get_dummy_admin() + ) + # Set up a temporary directory for MEDIA_ROOT during tests + self.old_media_root = settings.MEDIA_ROOT + settings.MEDIA_ROOT = os.path.normpath(os.path.join(settings.MEDIA_ROOT, '../testing')) + + def tearDown(self): + # Restore the original MEDIA_ROOT after tests + settings.MEDIA_ROOT = self.old_media_root + + def test_your_parsing(self): + course = create_course(id=3, name="test course", academic_startyear=2024) + project = create_project( + name="test", + description="descr", + visible=True, + archived=False, + days=100, + course=course, + ) + parse_zip_file(project=project, dir_path="structures/zip_struct1.zip") + + response = self.client.get( + reverse("project-detail", args=[str(project.id)]), follow=True + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.accepted_media_type, "application/json") + + content_json = json.loads(response.content.decode("utf-8")) + + response = self.client.get( + content_json["structure_checks"], follow=True + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.accepted_media_type, "application/json") + + content_json = json.loads(response.content.decode("utf-8")) + + self.assertEqual(len(content_json), 7) + + expected_project_url = settings.TESTING_BASE_LINK + reverse( + "project-detail", args=[str(project.id)] + ) + + content = content_json[0] + self.assertEqual(content["name"], ".") + self.assertEqual(content["project"], expected_project_url) + self.assertEqual(len(content["obligated_extensions"]), 0) + self.assertEqual(len(content["blocked_extensions"]), 0) + + content = content_json[1] + self.assertEqual(content["name"], "folder_struct1") + self.assertEqual(content["project"], expected_project_url) + self.assertEqual(len(content["obligated_extensions"]), 1) + self.assertEqual(len(content["blocked_extensions"]), 0) + + content = content_json[2] + self.assertEqual(content["name"], "folder_struct1/submap1") + self.assertEqual(content["project"], expected_project_url) + self.assertEqual(len(content["obligated_extensions"]), 2) + self.assertEqual(len(content["blocked_extensions"]), 0) + + content = content_json[3] + self.assertEqual(content["name"], "folder_struct1/submap1/templates") + self.assertEqual(content["project"], expected_project_url) + self.assertEqual(len(content["obligated_extensions"]), 1) + self.assertEqual(len(content["blocked_extensions"]), 0) + + content = content_json[4] + self.assertEqual(content["name"], "folder_struct1/submap2") + self.assertEqual(content["project"], expected_project_url) + self.assertEqual(len(content["obligated_extensions"]), 1) + self.assertEqual(len(content["blocked_extensions"]), 0) + + content = content_json[5] + self.assertEqual(content["name"], "folder_struct1/submap2/src") + self.assertEqual(content["project"], expected_project_url) + self.assertEqual(len(content["obligated_extensions"]), 3) + self.assertEqual(len(content["blocked_extensions"]), 0) + + content = content_json[6] + self.assertEqual(content["name"], "folder_struct1/submap3") + self.assertEqual(content["project"], expected_project_url) + self.assertEqual(len(content["obligated_extensions"]), 2) + self.assertEqual(len(content["blocked_extensions"]), 0) + + def test_your_checking(self): + course = create_course(id=3, name="test course", academic_startyear=2024) + project = create_project( + name="test", + description="descr", + visible=True, + archived=False, + days=100, + course=course, + ) + + fileExtensionHS = create_file_extension(extension="hs") + fileExtensionPDF = create_file_extension(extension="pdf") + fileExtensionDOCX = create_file_extension(extension="docx") + fileExtensionLATEX = create_file_extension(extension="latex") + fileExtensionMD = create_file_extension(extension="md") + fileExtensionPY = create_file_extension(extension="py") + fileExtensionHPP = create_file_extension(extension="hpp") + fileExtensionCPP = create_file_extension(extension="cpp") + fileExtensionTS = create_file_extension(extension="ts") + fileExtensionTSX = create_file_extension(extension="tsx") + + create_structure_check( + name=".", + project=project, + obligated=[], + blocked=[]) + + create_structure_check( + name="folder_struct1", + project=project, + obligated=[fileExtensionHS], + blocked=[]) + + create_structure_check( + name="folder_struct1/submap1", + project=project, + obligated=[fileExtensionPDF, fileExtensionDOCX], + blocked=[]) + + create_structure_check( + name="folder_struct1/submap1/templates", + project=project, + obligated=[fileExtensionLATEX], + blocked=[]) + + create_structure_check( + name="folder_struct1/submap2", + project=project, + obligated=[fileExtensionMD], + blocked=[]) + + create_structure_check( + name="folder_struct1/submap2/src", + project=project, + obligated=[fileExtensionPY, fileExtensionHPP, fileExtensionCPP], + blocked=[]) + + create_structure_check( + name="folder_struct1/submap3", + project=project, + obligated=[fileExtensionTS, fileExtensionTSX], + blocked=[]) + + succes = (True, 'zip.success') + self.assertEqual(check_zip_file(project=project, dir_path="structures/zip_struct1.zip"), succes) diff --git a/backend/api/tests/test_group.py b/backend/api/tests/test_group.py index f10e87d4..e90e80de 100644 --- a/backend/api/tests/test_group.py +++ b/backend/api/tests/test_group.py @@ -8,6 +8,7 @@ from api.models.student import Student from api.models.group import Group from api.models.course import Course +from django.conf import settings def create_course(name, academic_startyear, description=None, parent_course=None): @@ -83,7 +84,7 @@ def test_group_exists(self): self.assertEqual(len(content_json), 1) retrieved_group = content_json[0] - expected_project_url = "http://testserver" + reverse( + expected_project_url = settings.TESTING_BASE_LINK + reverse( "project-detail", args=[str(project.id)] ) @@ -122,10 +123,10 @@ def test_multiple_groups(self): self.assertEqual(len(content_json), 2) retrieved_group1, retrieved_group2 = content_json - expected_project_url1 = "http://testserver" + reverse( + expected_project_url1 = settings.TESTING_BASE_LINK + reverse( "project-detail", args=[str(project1.id)] ) - expected_project_url2 = "http://testserver" + reverse( + expected_project_url2 = settings.TESTING_BASE_LINK + reverse( "project-detail", args=[str(project2.id)] ) @@ -159,7 +160,7 @@ def test_group_detail_view(self): content_json = json.loads(response.content.decode("utf-8")) - expected_project_url = "http://testserver" + reverse( + expected_project_url = settings.TESTING_BASE_LINK + reverse( "project-detail", args=[str(project.id)] ) @@ -203,7 +204,7 @@ def test_group_project(self): # Parse the JSON content from the response content_json = json.loads(response.content.decode("utf-8")) - expected_course_url = "http://testserver" + reverse( + expected_course_url = settings.TESTING_BASE_LINK + reverse( "course-detail", args=[str(course.id)] ) diff --git a/backend/api/tests/test_project.py b/backend/api/tests/test_project.py index 03dfe7cd..a5885712 100644 --- a/backend/api/tests/test_project.py +++ b/backend/api/tests/test_project.py @@ -7,6 +7,7 @@ from api.models.course import Course from api.models.checks import StructureCheck, ExtraCheck from api.models.extension import FileExtension +from django.conf import settings def create_course(id, name, academic_startyear): @@ -212,7 +213,7 @@ def test_project_exists(self): retrieved_project = content_json[0] - expected_course_url = "http://testserver" + reverse( + expected_course_url = settings.TESTING_BASE_LINK + reverse( "course-detail", args=[str(course.id)] ) @@ -256,7 +257,7 @@ def test_multiple_project(self): retrieved_project = content_json[0] - expected_course_url = "http://testserver" + reverse( + expected_course_url = settings.TESTING_BASE_LINK + reverse( "course-detail", args=[str(course.id)] ) @@ -268,7 +269,7 @@ def test_multiple_project(self): retrieved_project = content_json[1] - expected_course_url = "http://testserver" + reverse( + expected_course_url = settings.TESTING_BASE_LINK + reverse( "course-detail", args=[str(course.id)] ) @@ -361,7 +362,7 @@ def test_project_structure_checks(self): retrieved_project = content_json[0] - expected_course_url = "http://testserver" + reverse( + expected_course_url = settings.TESTING_BASE_LINK + reverse( "course-detail", args=[str(course.id)] ) @@ -371,7 +372,7 @@ def test_project_structure_checks(self): self.assertEqual(retrieved_project["archived"], project.archived) self.assertEqual(retrieved_project["course"], expected_course_url) - response = self.client.get(retrieved_project["structure_checks"][0], follow=True) + response = self.client.get(retrieved_project["structure_checks"], follow=True) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -380,7 +381,7 @@ def test_project_structure_checks(self): self.assertEqual(response.accepted_media_type, "application/json") # Parse the JSON content from the response - content_json = json.loads(response.content.decode("utf-8")) + content_json = json.loads(response.content.decode("utf-8"))[0] self.assertEqual(int(content_json["id"]), checks.id) @@ -437,7 +438,7 @@ def test_project_extra_checks(self): retrieved_project = content_json[0] - response = self.client.get(retrieved_project["extra_checks"][0], follow=True) + response = self.client.get(retrieved_project["extra_checks"], follow=True) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -446,10 +447,10 @@ def test_project_extra_checks(self): self.assertEqual(response.accepted_media_type, "application/json") # Parse the JSON content from the response - content_json = json.loads(response.content.decode("utf-8")) + content_json = json.loads(response.content.decode("utf-8"))[0] self.assertEqual(int(content_json["id"]), checks.id) - self.assertEqual(content_json["project"], "http://testserver" + reverse( + self.assertEqual(content_json["project"], settings.TESTING_BASE_LINK + reverse( "project-detail", args=[str(project.id)] )) - self.assertEqual(content_json["run_script"], "http://testserver" + checks.run_script.url) + self.assertEqual(content_json["run_script"], settings.TESTING_BASE_LINK + checks.run_script.url) diff --git a/backend/api/tests/test_submission.py b/backend/api/tests/test_submission.py index ac0b52ab..ec426a03 100644 --- a/backend/api/tests/test_submission.py +++ b/backend/api/tests/test_submission.py @@ -9,6 +9,7 @@ from api.models.group import Group from api.models.course import Course from api.models.checks import ExtraCheck +from django.conf import settings def create_course(name, academic_startyear, description=None, parent_course=None): @@ -98,7 +99,7 @@ def test_submission_exists(self): # Assert the details of the retrieved submission # match the created submission retrieved_submission = content_json[0] - expected_group_url = "http://testserver" + reverse( + expected_group_url = settings.TESTING_BASE_LINK + reverse( "group-detail", args=[str(group.id)] ) self.assertEqual(int(retrieved_submission["id"]), submission.id) @@ -139,7 +140,7 @@ def test_multiple_submission_exists(self): # Assert the details of the retrieved submission # match the created submission retrieved_submission = content_json[0] - expected_group_url = "http://testserver" + reverse( + expected_group_url = settings.TESTING_BASE_LINK + reverse( "group-detail", args=[str(group.id)] ) self.assertEqual(int(retrieved_submission["id"]), submission1.id) @@ -150,7 +151,7 @@ def test_multiple_submission_exists(self): self.assertEqual(retrieved_submission["group"], expected_group_url) retrieved_submission = content_json[1] - expected_group_url = "http://testserver" + reverse( + expected_group_url = settings.TESTING_BASE_LINK + reverse( "group-detail", args=[str(group.id)] ) self.assertEqual(int(retrieved_submission["id"]), submission2.id) @@ -188,7 +189,7 @@ def test_submission_detail_view(self): # Assert the details of the retrieved submission # match the created submission retrieved_submission = content_json - expected_group_url = "http://testserver" + reverse( + expected_group_url = settings.TESTING_BASE_LINK + reverse( "group-detail", args=[str(group.id)] ) self.assertEqual(int(retrieved_submission["id"]), submission.id) @@ -241,7 +242,7 @@ def test_submission_group(self): # Parse the JSON content from the response content_json = json.loads(response.content.decode("utf-8")) - expected_project_url = "http://testserver" + reverse( + expected_project_url = settings.TESTING_BASE_LINK + reverse( "project-detail", args=[str(project.id)] ) diff --git a/backend/api/views/project_view.py b/backend/api/views/project_view.py index 1b2c37d6..0b041875 100644 --- a/backend/api/views/project_view.py +++ b/backend/api/views/project_view.py @@ -1,9 +1,12 @@ -from rest_framework import viewsets, status +from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response +from rest_framework.exceptions import NotFound +from django.utils.translation import gettext_lazy as _ from ..models.project import Project from ..serializers.project_serializer import ProjectSerializer from ..serializers.group_serializer import GroupSerializer +from ..serializers.checks_serializer import StructureCheckSerializer, ExtraCheckSerializer class ProjectViewSet(viewsets.ModelViewSet): @@ -26,6 +29,40 @@ def groups(self, request, pk=None): except Project.DoesNotExist: # Invalid project ID - return Response( - status=status.HTTP_404_NOT_FOUND, data={"message": "Project not found"} + raise NotFound(_('project.status.not_found')) + + @action(detail=True, methods=["get"]) + def structure_checks(self, request, pk=None): + """Returns the structure checks for the given project""" + + try: + queryset = Project.objects.get(id=pk) + checks = queryset.structure_checks.all() + + # Serialize the check objects + serializer = StructureCheckSerializer( + checks, many=True, context={"request": request} ) + return Response(serializer.data) + + except Project.DoesNotExist: + # Invalid project ID + raise NotFound(_('project.status.not_found')) + + @action(detail=True, methods=["get"]) + def extra_checks(self, request, pk=None): + """Returns the extra checks for the given project""" + + try: + queryset = Project.objects.get(id=pk) + checks = queryset.extra_checks.all() + + # Serialize the check objects + serializer = ExtraCheckSerializer( + checks, many=True, context={"request": request} + ) + return Response(serializer.data) + + except Project.DoesNotExist: + # Invalid project ID + raise NotFound(_('project.status.not_found')) diff --git a/backend/ypovoli/settings.py b/backend/ypovoli/settings.py index 32355200..f06ed720 100644 --- a/backend/ypovoli/settings.py +++ b/backend/ypovoli/settings.py @@ -12,9 +12,13 @@ from datetime import timedelta from pathlib import Path +import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +MEDIA_ROOT = os.path.normpath(os.path.join(BASE_DIR, "../data/production")) + +TESTING_BASE_LINK = "http://testserver" # Quick-start development settings - unsuitable for production diff --git a/data/production/structures/mixed_template.zip b/data/production/structures/mixed_template.zip new file mode 100644 index 00000000..456c258b Binary files /dev/null and b/data/production/structures/mixed_template.zip differ diff --git a/data/production/structures/only_files_template.zip b/data/production/structures/only_files_template.zip new file mode 100644 index 00000000..f135233a Binary files /dev/null and b/data/production/structures/only_files_template.zip differ diff --git a/data/production/structures/only_folders_template.zip b/data/production/structures/only_folders_template.zip new file mode 100644 index 00000000..e15d7f53 Binary files /dev/null and b/data/production/structures/only_folders_template.zip differ diff --git a/data/production/structures/remake.zip b/data/production/structures/remake.zip new file mode 100644 index 00000000..7969b3c0 Binary files /dev/null and b/data/production/structures/remake.zip differ diff --git a/data/production/structures/root.zip b/data/production/structures/root.zip new file mode 100644 index 00000000..4e703157 Binary files /dev/null and b/data/production/structures/root.zip differ diff --git a/data/production/structures/zip_struct1.zip b/data/production/structures/zip_struct1.zip new file mode 100644 index 00000000..bd156cd2 Binary files /dev/null and b/data/production/structures/zip_struct1.zip differ diff --git a/data/production/tests/mixed.zip b/data/production/tests/mixed.zip new file mode 100644 index 00000000..50991100 Binary files /dev/null and b/data/production/tests/mixed.zip differ diff --git a/data/production/tests/only_files.zip b/data/production/tests/only_files.zip new file mode 100644 index 00000000..55a7402c Binary files /dev/null and b/data/production/tests/only_files.zip differ diff --git a/data/production/tests/only_folders.zip b/data/production/tests/only_folders.zip new file mode 100644 index 00000000..012a0304 Binary files /dev/null and b/data/production/tests/only_folders.zip differ diff --git a/data/production/tests/test_zip1struct1.zip b/data/production/tests/test_zip1struct1.zip new file mode 100644 index 00000000..c6a73671 Binary files /dev/null and b/data/production/tests/test_zip1struct1.zip differ diff --git a/data/production/tests/test_zip2struct1.zip b/data/production/tests/test_zip2struct1.zip new file mode 100644 index 00000000..fbce9306 Binary files /dev/null and b/data/production/tests/test_zip2struct1.zip differ diff --git a/data/production/tests/test_zip3struct1.zip b/data/production/tests/test_zip3struct1.zip new file mode 100644 index 00000000..315fd52e Binary files /dev/null and b/data/production/tests/test_zip3struct1.zip differ diff --git a/data/production/tests/test_zip4struct1.zip b/data/production/tests/test_zip4struct1.zip new file mode 100644 index 00000000..9acfea02 Binary files /dev/null and b/data/production/tests/test_zip4struct1.zip differ diff --git a/data/testing/structures/mixed_template.zip b/data/testing/structures/mixed_template.zip new file mode 100644 index 00000000..456c258b Binary files /dev/null and b/data/testing/structures/mixed_template.zip differ diff --git a/data/testing/structures/only_files_template.zip b/data/testing/structures/only_files_template.zip new file mode 100644 index 00000000..f135233a Binary files /dev/null and b/data/testing/structures/only_files_template.zip differ diff --git a/data/testing/structures/only_folders_template.zip b/data/testing/structures/only_folders_template.zip new file mode 100644 index 00000000..e15d7f53 Binary files /dev/null and b/data/testing/structures/only_folders_template.zip differ diff --git a/data/testing/structures/remake.zip b/data/testing/structures/remake.zip new file mode 100644 index 00000000..7969b3c0 Binary files /dev/null and b/data/testing/structures/remake.zip differ diff --git a/data/testing/structures/root.zip b/data/testing/structures/root.zip new file mode 100644 index 00000000..4e703157 Binary files /dev/null and b/data/testing/structures/root.zip differ diff --git a/data/testing/structures/zip_struct1.zip b/data/testing/structures/zip_struct1.zip new file mode 100644 index 00000000..bd156cd2 Binary files /dev/null and b/data/testing/structures/zip_struct1.zip differ diff --git a/data/testing/tests/mixed.zip b/data/testing/tests/mixed.zip new file mode 100644 index 00000000..50991100 Binary files /dev/null and b/data/testing/tests/mixed.zip differ diff --git a/data/testing/tests/only_files.zip b/data/testing/tests/only_files.zip new file mode 100644 index 00000000..55a7402c Binary files /dev/null and b/data/testing/tests/only_files.zip differ diff --git a/data/testing/tests/only_folders.zip b/data/testing/tests/only_folders.zip new file mode 100644 index 00000000..012a0304 Binary files /dev/null and b/data/testing/tests/only_folders.zip differ diff --git a/data/testing/tests/test_zip1struct1.zip b/data/testing/tests/test_zip1struct1.zip new file mode 100644 index 00000000..c6a73671 Binary files /dev/null and b/data/testing/tests/test_zip1struct1.zip differ diff --git a/data/testing/tests/test_zip2struct1.zip b/data/testing/tests/test_zip2struct1.zip new file mode 100644 index 00000000..fbce9306 Binary files /dev/null and b/data/testing/tests/test_zip2struct1.zip differ diff --git a/data/testing/tests/test_zip3struct1.zip b/data/testing/tests/test_zip3struct1.zip new file mode 100644 index 00000000..315fd52e Binary files /dev/null and b/data/testing/tests/test_zip3struct1.zip differ diff --git a/data/testing/tests/test_zip4struct1.zip b/data/testing/tests/test_zip4struct1.zip new file mode 100644 index 00000000..9acfea02 Binary files /dev/null and b/data/testing/tests/test_zip4struct1.zip differ