From eb14137ade7bd6a8108f97d3288481ff449c907f Mon Sep 17 00:00:00 2001 From: reppuli92 Date: Wed, 22 Jul 2026 12:16:57 +0300 Subject: [PATCH 1/2] Improve unit tests --- course/test_visibility_enroll.py | 10 ++++------ e2e_tests/test_points_goal_set.py | 4 ++-- exercise/tests.py | 10 ++++++++-- locale/en/LC_MESSAGES/django.po | 2 +- userprofile/api/views.py | 4 +--- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/course/test_visibility_enroll.py b/course/test_visibility_enroll.py index 5633c150d..f5ad42803 100644 --- a/course/test_visibility_enroll.py +++ b/course/test_visibility_enroll.py @@ -158,8 +158,10 @@ def setUpTestData(cls): submission.submitters.add(cls.student.userprofile) cls.submissions[exercise.id].append(submission) - # disable all logging - logging.disable(logging.CRITICAL) + def setUp(self): + super().setUp() + logging.disable(logging.CRITICAL) # Disable all logging + self.addCleanup(logging.disable, logging.NOTSET) # Return previous logging settings def test_redirect_to_enroll(self): url = self.enrolled_course_instance.get_absolute_url() @@ -758,7 +760,3 @@ def test_enrollment_exercise_external_users(self): self.assertEqual(response.status_code, 403) self.assertFalse(instance.is_student(self.user)) self.client.logout() - - def tearDown(self): - # return previous logging settings - logging.disable(logging.NOTSET) diff --git a/e2e_tests/test_points_goal_set.py b/e2e_tests/test_points_goal_set.py index 649b24a94..2c7dfb0b4 100644 --- a/e2e_tests/test_points_goal_set.py +++ b/e2e_tests/test_points_goal_set.py @@ -13,7 +13,7 @@ def test_points_goal_set(page: Page) -> None: page.get_by_label("Input personalized goal as").fill("50") page.get_by_label("Input personalized goal as").press("Tab") page.get_by_role("button", name="Save").click() - expect(page.locator("#success-alert")).to_contain_text("Succesfully set personalized points goal") + expect(page.locator("#success-alert")).to_contain_text("Successfully set personalized points goal") page.get_by_role("button", name="Close", exact=True).click() expect(page.get_by_text("Points goal: 50")) expect(page.locator("#goal-points")) @@ -37,7 +37,7 @@ def test_points_goal_reached(page: Page) -> None: page.locator("#progress-questionnaires").get_by_role("button", name="Points goal").click() page.get_by_label("Input personalized goal as").fill("30") page.get_by_role("button", name="Save").click() - expect(page.locator("#success-alert")).to_contain_text("Succesfully set personalized points goal") + expect(page.locator("#success-alert")).to_contain_text("Successfully set personalized points goal") page.get_by_role("button", name="Close", exact=True).click() progress_bar_locator = page.locator("#progress-questionnaires .progress > .aplus-progress-bar") expect(progress_bar_locator).\ diff --git a/exercise/tests.py b/exercise/tests.py index 44655fee8..118acf9f4 100644 --- a/exercise/tests.py +++ b/exercise/tests.py @@ -2,6 +2,7 @@ import urllib from datetime import datetime, timedelta from io import BytesIO, StringIO +from unittest.mock import patch from django.conf import settings from django.contrib.auth.models import User @@ -786,7 +787,8 @@ def test_exercise_staff_views(self) -> None: self.assertEqual(response.status_code, 200) response = self.client.get(inspect_submission_url) self.assertEqual(response.status_code, 200) - response = self.client.post(inspect_submission_url, assessment_data) + with self.assertLogs('django.request', level='WARNING'): + response = self.client.post(inspect_submission_url, assessment_data) self.assertEqual(response.status_code, 403) self.base_exercise.allow_assistant_grading = True @@ -823,7 +825,11 @@ def test_uploading_and_viewing_file(self): py_file = StringIO('print("Tekijät ja Hyyppö")') py_file.name = 'test.py' - with png_file, py_file: + with png_file, py_file, patch( + 'exercise.exercise_models.load_feedback_page', + autospec=True, + return_value=ExercisePage(exercise), + ): response = self.client.post(exercise.get_absolute_url(), { "key": "value", "file1": png_file, diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index e8b313981..836620bd9 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -4593,7 +4593,7 @@ msgstr "Your submission has been received." #: exercise/templates/exercise/module_goal_modal.html msgid "PERSONALIZED_POINTS_MODAL_SUCCESS" -msgstr "Succesfully set personalized points goal" +msgstr "Successfully set personalized points goal" #: exercise/templates/exercise/module_goal_modal.html msgid "PERSONALIZED_POINTS_MODAL_FAILURE" diff --git a/userprofile/api/views.py b/userprofile/api/views.py index fe8c83474..318af4e1d 100644 --- a/userprofile/api/views.py +++ b/userprofile/api/views.py @@ -73,7 +73,5 @@ class MeDetail(APIView): def get(self, request, version, format=None): # pylint: disable=unused-argument redefined-builtin userinstance = self.request.user.userprofile - serializer = UserSerializer(userinstance, context={ - 'request': request, - }) + serializer = UserSerializer(userinstance, context={'request': request}) return Response(serializer.data) From 098b514ab042f21667c636aad3e5c3eb7966185e Mon Sep 17 00:00:00 2001 From: Jimmy Lumiaho Date: Fri, 24 Jul 2026 12:16:34 +0300 Subject: [PATCH 2/2] Fix duplicate modules in cache hierarchy Exclude already-loaded modules from incremental ContentDBData fetches, preserve CourseModule ordering after merging batches, and add regression coverage for loading a module before its containing instance. --- exercise/cache/basetypes.py | 9 ++++++++- exercise/tests_cache.py | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/exercise/cache/basetypes.py b/exercise/cache/basetypes.py index 463ccbea5..6416ba3ad 100644 --- a/exercise/cache/basetypes.py +++ b/exercise/cache/basetypes.py @@ -83,7 +83,7 @@ def add(self, proxy: Union[CachedDataBase, ModuleEntryBase, LearningObjectEntryB self.needed_exercises.add(model_id) def _load_modules_qs(self, module_qs: QuerySet[CourseModule]) -> Iterable[CourseModule]: - new_modules = module_qs.prefetch_related( + new_modules = module_qs.exclude(id__in=self.modules).prefetch_related( Prefetch( "requirements", queryset=( @@ -106,8 +106,15 @@ def _load_modules_qs(self, module_qs: QuerySet[CourseModule]) -> Iterable[Course for module in new_modules ) + affected_instances = set() for module in new_modules: self.instance_modules.setdefault(module.course_instance_id, []).append(module) + affected_instances.add(module.course_instance_id) + + for instance_id in affected_instances: + self.instance_modules[instance_id].sort( + key=lambda module: (module.order, module.closing_time, module.id) + ) new_exercises = LearningObject.objects.filter(course_module__in=new_modules).select_related("category") self.exercises.update( diff --git a/exercise/tests_cache.py b/exercise/tests_cache.py index 98fba60d7..7ee539794 100644 --- a/exercise/tests_cache.py +++ b/exercise/tests_cache.py @@ -2,6 +2,7 @@ from course.models import CourseInstance, CourseModule, LearningObjectCategory from deviations.models import MaxSubmissionsRuleDeviation from exercise.tests import ExerciseTestBase +from .cache.basetypes import ContentDBData from .cache.content import CachedContent, InstanceContent, LearningObjectContent, ModuleContent from .cache.hierarchy import previous_iterator from .cache.points import ( @@ -144,6 +145,22 @@ def test_invalidation_delete_category(self): class CachedContentTest(CourseTestCase): + def test_module_loaded_before_instance_is_not_duplicated(self): + prefetched_data = ContentDBData() + prefetched_data.add(ModuleContent.proxy(self.module.id)) + prefetched_data.fetch() + prefetched_data.add(InstanceContent.proxy(self.instance.id)) + prefetched_data.fetch() + + module_ids = [ + module.id + for module in prefetched_data.get_instance_modules(self.instance.id) + ] + expected_ids = list( + self.instance.course_modules.values_list("id", flat=True) + ) + self.assertEqual(module_ids, expected_ids) + def test_no_invalidation(self): entry = InstanceContent.get(self.instance) entry2 = InstanceContent.get(self.instance)