diff --git a/.gitignore b/.gitignore index d115f15abb..caa4789b98 100644 --- a/.gitignore +++ b/.gitignore @@ -26,10 +26,6 @@ evap/localsettings.py evap/static/css/evap.css evap/static/css/evap.css.map evap/static/ts/rendered -evap/static_collected -evap/media -evap/database.sqlite3 -evap/upload evap/evaluation/templates/legal_notice_text.html diff --git a/evap/upload/grades/1525/test_results_01.txt b/evap/development/fixtures/upload/grades/1525/test_results_01.txt similarity index 100% rename from evap/upload/grades/1525/test_results_01.txt rename to evap/development/fixtures/upload/grades/1525/test_results_01.txt diff --git a/evap/upload/grades/1577/test_results_03.txt b/evap/development/fixtures/upload/grades/1577/test_results_03.txt similarity index 100% rename from evap/upload/grades/1577/test_results_03.txt rename to evap/development/fixtures/upload/grades/1577/test_results_03.txt diff --git a/evap/upload/grades/1694/test_results_02.txt b/evap/development/fixtures/upload/grades/1694/test_results_02.txt similarity index 100% rename from evap/upload/grades/1694/test_results_02.txt rename to evap/development/fixtures/upload/grades/1694/test_results_02.txt diff --git a/evap/development/management/commands/dump_testdata.py b/evap/development/management/commands/dump_testdata.py index f20a9d564f..392bed20c6 100644 --- a/evap/development/management/commands/dump_testdata.py +++ b/evap/development/management/commands/dump_testdata.py @@ -1,5 +1,3 @@ -import os - from django.conf import settings from django.core.management.base import BaseCommand @@ -12,7 +10,7 @@ class Command(BaseCommand): requires_migrations_checks = True def handle(self, *args, **options): - outfile_name = os.path.join(settings.BASE_DIR, "development", "fixtures", "test_data.json") + outfile_name = settings.MODULE / "development" / "fixtures" / "test_data.json" logged_call_command( self.stdout, "dumpdata", diff --git a/evap/development/management/commands/reload_testdata.py b/evap/development/management/commands/reload_testdata.py index 6bd6b2f246..d1206beef2 100644 --- a/evap/development/management/commands/reload_testdata.py +++ b/evap/development/management/commands/reload_testdata.py @@ -1,16 +1,21 @@ +import shutil + +from django.conf import settings from django.core.management.base import BaseCommand from evap.evaluation.management.commands.tools import confirm_harmful_operation, logged_call_command class Command(BaseCommand): - args = "" - help = "Drops the database, recreates it and then loads the testdata." + help = "Drops the database, recreates it, and then loads the testdata. Also resets the upload directory." + + def add_arguments(self, parser): + parser.add_argument("--noinput", action="store_true") def handle(self, *args, **options): self.stdout.write("") - self.stdout.write("WARNING! This will drop the database and cause IRREPARABLE DATA LOSS.") - if not confirm_harmful_operation(self.stdout): + self.stdout.write("WARNING! This will drop the database and upload directory and cause IRREPARABLE DATA LOSS.") + if not options["noinput"] and not confirm_harmful_operation(self.stdout): return logged_call_command(self.stdout, "reset_db", interactive=False) @@ -27,4 +32,9 @@ def handle(self, *args, **options): logged_call_command(self.stdout, "refresh_results_cache") + upload_dir = settings.MEDIA_ROOT + if upload_dir.exists(): + shutil.rmtree(upload_dir) + shutil.copytree(settings.MODULE / "development" / "fixtures" / "upload", upload_dir) + self.stdout.write("Done.") diff --git a/evap/development/tests/test_commands.py b/evap/development/tests/test_commands.py index 476bf88beb..13731a2904 100644 --- a/evap/development/tests/test_commands.py +++ b/evap/development/tests/test_commands.py @@ -1,4 +1,3 @@ -import os from io import StringIO from unittest.mock import call, patch @@ -14,7 +13,7 @@ def test_dumpdata_called(): with patch("evap.evaluation.management.commands.tools.call_command") as mock: management.call_command("dump_testdata", stdout=StringIO()) - outfile_name = os.path.join(settings.BASE_DIR, "development", "fixtures", "test_data.json") + outfile_name = settings.MODULE / "development" / "fixtures" / "test_data.json" mock.assert_called_once_with( "dumpdata", "auth.group", @@ -31,20 +30,20 @@ def test_dumpdata_called(): class TestReloadTestdataCommand(TestCase): - @patch("builtins.input") + @patch("builtins.input", return_value="not yes") + @patch("evap.development.management.commands.reload_testdata.shutil") @patch("evap.evaluation.management.commands.tools.call_command") - def test_aborts(self, mock_call_command, mock_input): - mock_input.return_value = "not yes" - + def test_aborts(self, mock_call_command, mock_shutil, _mock_input): management.call_command("reload_testdata", stdout=StringIO()) self.assertEqual(mock_call_command.call_count, 0) + self.assertFalse(mock_shutil.method_calls) - @patch("builtins.input") + @patch("builtins.input", return_value="yes") + @patch("pathlib.Path.exists", return_value=True) + @patch("evap.development.management.commands.reload_testdata.shutil") @patch("evap.evaluation.management.commands.tools.call_command") - def test_executes_key_commands(self, mock_call_command, mock_input): - mock_input.return_value = "yes" - + def test_executes_key_commands(self, mock_call_command, mock_shutil, mock_exists, _mock_input): management.call_command("reload_testdata", stdout=StringIO()) mock_call_command.assert_any_call("reset_db", interactive=False) @@ -56,6 +55,11 @@ def test_executes_key_commands(self, mock_call_command, mock_input): self.assertEqual(mock_call_command.call_count, 6) + # The directory for uploads is cleared and reinitialized + mock_exists.assert_called_once() + mock_shutil.rmtree.assert_called_once() + mock_shutil.copytree.assert_called_once() + class TestRunCommand(TestCase): def test_calls_runserver(self): diff --git a/evap/development/views.py b/evap/development/views.py index 379c522c58..d83ce28518 100644 --- a/evap/development/views.py +++ b/evap/development/views.py @@ -1,5 +1,3 @@ -import os - from django.conf import settings from django.core.exceptions import BadRequest from django.http import HttpResponse @@ -16,9 +14,9 @@ def development_components(request): def development_rendered(request, filename): - fixtures_directory = os.path.join(settings.STATICFILES_DIRS[0], "ts", "rendered") + fixtures_directory = settings.STATICFILES_DIRS[0] / "ts" / "rendered" try: - with open(os.path.join(fixtures_directory, filename), encoding="utf-8") as fixture: + with open(fixtures_directory / filename, encoding="utf-8") as fixture: return HttpResponse(fixture) except (FileNotFoundError, ValueError, OSError) as e: raise BadRequest from e diff --git a/evap/evaluation/management/commands/scss.py b/evap/evaluation/management/commands/scss.py index 1da8507e9f..e376273482 100644 --- a/evap/evaluation/management/commands/scss.py +++ b/evap/evaluation/management/commands/scss.py @@ -1,4 +1,3 @@ -import os import subprocess # nosec from django.conf import settings @@ -24,8 +23,8 @@ def handle(self, *args, **options): command = [ "npx", "sass", - os.path.join(static_directory, "scss", "evap.scss"), - os.path.join(static_directory, "css", "evap.css"), + static_directory / "scss" / "evap.scss", + static_directory / "css" / "evap.css", ] if options["watch"]: diff --git a/evap/evaluation/management/commands/ts.py b/evap/evaluation/management/commands/ts.py index bdc7c9ff3c..7a4725e027 100644 --- a/evap/evaluation/management/commands/ts.py +++ b/evap/evaluation/management/commands/ts.py @@ -1,5 +1,4 @@ import argparse -import os import subprocess # nosec import unittest @@ -67,17 +66,14 @@ def compile(self, watch=False, fresh=False, **_options): "npx", "tsc", "--project", - os.path.join(static_directory, "ts", "tsconfig.compile.json"), + static_directory / "ts" / "tsconfig.compile.json", ] if watch: command += ["--watch"] if fresh: - try: - os.remove(os.path.join(static_directory, "ts", ".tsbuildinfo.json")) - except FileNotFoundError: - pass + (static_directory / "ts" / ".tsbuildinfo.json").unlink(missing_ok=True) self.run_command(command) diff --git a/evap/evaluation/tests/test_commands.py b/evap/evaluation/tests/test_commands.py index 8509b6753c..20cc6f03e6 100644 --- a/evap/evaluation/tests/test_commands.py +++ b/evap/evaluation/tests/test_commands.py @@ -1,4 +1,3 @@ -import os import random from collections import defaultdict from datetime import date, datetime, timedelta @@ -204,8 +203,8 @@ def test_calls_cache_results(self): class TestScssCommand(TestCase): def setUp(self): - self.scss_path = os.path.join(settings.STATICFILES_DIRS[0], "scss", "evap.scss") - self.css_path = os.path.join(settings.STATICFILES_DIRS[0], "css", "evap.css") + self.scss_path = settings.STATICFILES_DIRS[0] / "scss" / "evap.scss" + self.css_path = settings.STATICFILES_DIRS[0] / "css" / "evap.css" @patch("subprocess.run") def test_scss_called(self, mock_subprocess_run): @@ -246,14 +245,14 @@ def test_scss_called_with_no_sass_installed(self, mock_subprocess_run): class TestTsCommand(TestCase): def setUp(self): - self.ts_path = os.path.join(settings.STATICFILES_DIRS[0], "ts") + self.ts_path = settings.STATICFILES_DIRS[0] / "ts" @patch("subprocess.run") def test_ts_compile(self, mock_subprocess_run): management.call_command("ts", "compile") mock_subprocess_run.assert_called_once_with( - ["npx", "tsc", "--project", os.path.join(self.ts_path, "tsconfig.compile.json")], + ["npx", "tsc", "--project", self.ts_path / "tsconfig.compile.json"], check=True, ) @@ -264,7 +263,7 @@ def test_ts_compile_with_watch(self, mock_subprocess_run): management.call_command("ts", "compile", "--watch") mock_subprocess_run.assert_called_once_with( - ["npx", "tsc", "--project", os.path.join(self.ts_path, "tsconfig.compile.json"), "--watch"], + ["npx", "tsc", "--project", self.ts_path / "tsconfig.compile.json", "--watch"], check=True, ) @@ -280,7 +279,7 @@ def test_ts_test(self, mock_render_pages, mock_call_command, mock_subprocess_run mock_subprocess_run.assert_has_calls( [ call( - ["npx", "tsc", "--project", os.path.join(self.ts_path, "tsconfig.compile.json")], + ["npx", "tsc", "--project", self.ts_path / "tsconfig.compile.json"], check=True, ), call(["npx", "jest"], check=True), diff --git a/evap/evaluation/tests/test_misc.py b/evap/evaluation/tests/test_misc.py index 07bef0f4d8..b6e9be55ca 100644 --- a/evap/evaluation/tests/test_misc.py +++ b/evap/evaluation/tests/test_misc.py @@ -1,4 +1,3 @@ -import os.path from io import StringIO from django.conf import settings @@ -29,7 +28,7 @@ def test_sample_semester_file(self): original_user_count = UserProfile.objects.count() form = page.forms["semester-import-form"] - form["excel_file"] = (os.path.join(settings.BASE_DIR, "static", "sample.xlsx"),) + form["excel_file"] = (str(settings.MODULE / "static" / "sample.xlsx"),) page = form.submit(name="operation", value="test") form = page.forms["semester-import-form"] @@ -45,7 +44,7 @@ def test_sample_user_file(self): original_user_count = UserProfile.objects.count() form = page.forms["user-import-form"] - form["excel_file"] = (os.path.join(settings.BASE_DIR, "static", "sample_user.xlsx"),) + form["excel_file"] = (str(settings.MODULE / "static" / "sample_user.xlsx"),) page = form.submit(name="operation", value="test") form = page.forms["user-import-form"] diff --git a/evap/evaluation/tests/tools.py b/evap/evaluation/tests/tools.py index 64c116bf73..495e81a05b 100644 --- a/evap/evaluation/tests/tools.py +++ b/evap/evaluation/tests/tools.py @@ -103,9 +103,9 @@ def let_user_vote_for_evaluation(user, evaluation, create_answers=False): def store_ts_test_asset(relative_path: str, content) -> None: - absolute_path = os.path.join(settings.STATICFILES_DIRS[0], "ts", "rendered", relative_path) + absolute_path = settings.STATICFILES_DIRS[0] / "ts" / "rendered" / relative_path - os.makedirs(os.path.dirname(absolute_path), exist_ok=True) + absolute_path.parent.mkdir(parents=True, exist_ok=True) with open(absolute_path, "wb") as file: file.write(content) diff --git a/evap/logs/.gitignore b/evap/logs/.gitignore deleted file mode 100644 index 5e7d2734cf..0000000000 --- a/evap/logs/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore diff --git a/evap/settings.py b/evap/settings.py index 77c5cd7641..8677b7fbf8 100644 --- a/evap/settings.py +++ b/evap/settings.py @@ -9,17 +9,18 @@ """ import logging -import os import sys from fractions import Fraction +from pathlib import Path from typing import Any from django.contrib.staticfiles.storage import ManifestStaticFilesStorage from evap.tools import MonthAndDay -BASE_DIR = os.path.dirname(os.path.realpath(__file__)) - +MODULE = Path(__file__).parent.resolve() +CWD = Path(".").resolve() +DATADIR = CWD / "data" ### Debugging @@ -184,7 +185,7 @@ class ManifestStaticFilesStorageWithJsReplacement(ManifestStaticFilesStorage): "file": { "level": "DEBUG", "class": "logging.handlers.RotatingFileHandler", - "filename": BASE_DIR + "/logs/evap.log", + "filename": DATADIR / "evap.log", "maxBytes": 1024 * 1024 * 10, "backupCount": 5, "formatter": "default", @@ -334,7 +335,7 @@ class ManifestStaticFilesStorageWithJsReplacement(ManifestStaticFilesStorage): USE_TZ = False -LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")] +LOCALE_PATHS = [MODULE / "locale"] FORMAT_MODULE_PATH = ["evap.locale"] @@ -351,17 +352,17 @@ class ManifestStaticFilesStorageWithJsReplacement(ManifestStaticFilesStorage): # Additional locations of static files STATICFILES_DIRS = [ - os.path.join(BASE_DIR, "static"), + MODULE / "static", ] # Absolute path to the directory static files should be collected to. -STATIC_ROOT = os.path.join(BASE_DIR, "static_collected") +STATIC_ROOT = DATADIR / "static_collected" ### User-uploaded files # Absolute filesystem path to the directory that will hold user-uploaded files. -MEDIA_ROOT = os.path.join(BASE_DIR, "upload") +MEDIA_ROOT = DATADIR / "upload" ### Evaluation progress rewards GLOBAL_EVALUATION_PROGRESS_REWARDS: list[tuple[Fraction, str]] = ( diff --git a/evap/staff/tests/test_views.py b/evap/staff/tests/test_views.py index 514d367a6d..c0fa4520c8 100644 --- a/evap/staff/tests/test_views.py +++ b/evap/staff/tests/test_views.py @@ -1,6 +1,5 @@ import csv import datetime -import os from abc import ABC, abstractmethod from io import BytesIO from typing import Literal @@ -420,7 +419,7 @@ def test_shows_swap_users_option(self): class TestUserBulkUpdateView(WebTestStaffMode): url = "/staff/user/bulk_update" - filename = os.path.join(settings.BASE_DIR, "staff/fixtures/test_user_bulk_update_file.txt") + filename = str(settings.MODULE / "staff" / "fixtures" / "test_user_bulk_update_file.txt") @classmethod def setUpTestData(cls): diff --git a/evap/staff/tests/utils.py b/evap/staff/tests/utils.py index ed972462e5..f099d8a8a3 100644 --- a/evap/staff/tests/utils.py +++ b/evap/staff/tests/utils.py @@ -1,9 +1,8 @@ -import os import time from contextlib import contextmanager from evap.evaluation.tests.tools import WebTest, WebTestWith200Check -from evap.staff.tools import ImportType, generate_import_filename +from evap.staff.tools import ImportType, generate_import_path def helper_enter_staff_mode(webtest): @@ -46,11 +45,8 @@ def setUp(self): def helper_delete_all_import_files(user_id): for import_type in ImportType: - filename = generate_import_filename(user_id, import_type) - try: - os.remove(filename) - except FileNotFoundError: - pass + path = generate_import_path(user_id, import_type) + path.unlink(missing_ok=True) # For some form fields, like a