From 447ca4af68683902ec9d8c96922e5b412568b1b4 Mon Sep 17 00:00:00 2001 From: Colin Sonnenberg Date: Sun, 15 Feb 2026 12:41:20 -0500 Subject: [PATCH 1/6] Refactored /backend/domain/services/base.py from BaseMetricService class with static methods to functions. Updated the related import statements and function invokation lines in backend/domain/services files, including tags_service.py, difficulty_distribution.py, and abandoned_problem_service.py. --- backend/domain/services/__init__.py | 5 +- .../services/abandoned_problems_service.py | 3 +- backend/domain/services/base.py | 70 +++++++++---------- .../difficulty_distribution_service.py | 8 +-- backend/domain/services/tags_service.py | 8 +-- 5 files changed, 44 insertions(+), 50 deletions(-) diff --git a/backend/domain/services/__init__.py b/backend/domain/services/__init__.py index da775f5..bbe5101 100644 --- a/backend/domain/services/__init__.py +++ b/backend/domain/services/__init__.py @@ -1,13 +1,14 @@ """Domain services package.""" -from .base import BaseMetricService +from .base import _deduplicate_problems, _filter_successful_submissions from .abandoned_problems_service import AbandonedProblemsService from .difficulty_distribution_service import DifficultyDistributionService from .tags_service import TagsService __all__ = [ "AbandonedProblemsService", - "BaseMetricService", + "_filter_successful_submissions", + "_deduplicate_problems", "DifficultyDistributionService", "TagsService", ] diff --git a/backend/domain/services/abandoned_problems_service.py b/backend/domain/services/abandoned_problems_service.py index dca2c80..054c730 100644 --- a/backend/domain/services/abandoned_problems_service.py +++ b/backend/domain/services/abandoned_problems_service.py @@ -10,10 +10,9 @@ TagAbandonedStats, ) from backend.domain.models.codeforces import Submission -from backend.domain.services.base import BaseMetricService -class AbandonedProblemsService(BaseMetricService): +class AbandonedProblemsService(): """Service for analyzing abandoned problems (attempted but never solved).""" @staticmethod diff --git a/backend/domain/services/base.py b/backend/domain/services/base.py index 7574521..344e912 100644 --- a/backend/domain/services/base.py +++ b/backend/domain/services/base.py @@ -2,41 +2,35 @@ from backend.domain.models.codeforces import Submission - -class BaseMetricService: - """Base class for all metric services with shared functionality.""" - - @staticmethod - def _deduplicate_problems(submissions: List[Submission]) -> List[Submission]: - """ - Keep only the first successful solve for each unique problem. - - Args: - submissions: List of submissions to deduplicate - - Returns: - List of submissions with only first solve per problem - """ - seen_problems = set() - unique_submissions = [] - - for submission in submissions: - problem_key = submission.problem.problem_key - if problem_key not in seen_problems: - seen_problems.add(problem_key) - unique_submissions.append(submission) - - return unique_submissions - - @staticmethod - def _filter_successful_submissions(submissions: List[Submission]) -> List[Submission]: - """ - Filter submissions to only include solved problems. - - Args: - submissions: List of all submissions - - Returns: - List of only solved submissions - """ - return [s for s in submissions if s.is_solved] +def _deduplicate_problems(submissions: List[Submission]) -> List[Submission]: + """ + Keep only the first successful solve for each unique problem. + + Args: + submissions: List of submissions to deduplicate + + Returns: + List of submissions with only first solve per problem + """ + seen_problems = set() + unique_submissions = [] + + for submission in submissions: + problem_key = submission.problem.problem_key + if problem_key not in seen_problems: + seen_problems.add(problem_key) + unique_submissions.append(submission) + + return unique_submissions + +def _filter_successful_submissions(submissions: List[Submission]) -> List[Submission]: + """ + Filter submissions to only include solved problems. + + Args: + submissions: List of all submissions + + Returns: + List of only solved submissions + """ + return [s for s in submissions if s.is_solved] \ No newline at end of file diff --git a/backend/domain/services/difficulty_distribution_service.py b/backend/domain/services/difficulty_distribution_service.py index acacb28..aaf4112 100644 --- a/backend/domain/services/difficulty_distribution_service.py +++ b/backend/domain/services/difficulty_distribution_service.py @@ -8,10 +8,10 @@ DifficultyDistribution, RatingRange, ) -from backend.domain.services.base import BaseMetricService +from backend.domain.services.base import _deduplicate_problems, _filter_successful_submissions -class DifficultyDistributionService(BaseMetricService): +class DifficultyDistributionService(): """Service for generating difficulty distribution analytics.""" # Standard rating bins for Codeforces (100-point intervals) @@ -61,7 +61,7 @@ def analyze_difficulty_distribution( DifficultyDistribution with analyzed data """ # Filter successful submissions - successful_submissions = DifficultyDistributionService._filter_successful_submissions( + successful_submissions = _filter_successful_submissions( submissions ) @@ -73,7 +73,7 @@ def analyze_difficulty_distribution( ) # Remove duplicate problems (keep first solve) - unique_solves = DifficultyDistributionService._deduplicate_problems(successful_submissions) + unique_solves = _deduplicate_problems(successful_submissions) # Group problems by rating bins bin_counts = DifficultyDistributionService._create_bin_distribution(unique_solves) diff --git a/backend/domain/services/tags_service.py b/backend/domain/services/tags_service.py index d5ecda0..a3ddbca 100644 --- a/backend/domain/services/tags_service.py +++ b/backend/domain/services/tags_service.py @@ -6,10 +6,10 @@ from backend.domain.models.codeforces import Submission from backend.domain.models.tags import TagInfo, TagsAnalysis -from backend.domain.services.base import BaseMetricService +from backend.domain.services.base import _deduplicate_problems, _filter_successful_submissions -class TagsService(BaseMetricService): +class TagsService(): """Service for generating tags analytics.""" @staticmethod @@ -25,7 +25,7 @@ def analyze_tags(handle: str, submissions: List[Submission]) -> TagsAnalysis: TagsAnalysis with analyzed data """ # Filter successful submissions - successful_submissions = TagsService._filter_successful_submissions(submissions) + successful_submissions = _filter_successful_submissions(submissions) if not successful_submissions: return TagsAnalysis( @@ -37,7 +37,7 @@ def analyze_tags(handle: str, submissions: List[Submission]) -> TagsAnalysis: ) # Remove duplicate problems (keep first solve) - unique_solves = TagsService._deduplicate_problems(successful_submissions) + unique_solves = _deduplicate_problems(successful_submissions) # Group problems by tags and calculate statistics tags_data, overall_ratings = TagsService._analyze_tags(unique_solves) From e54b5096734db4e19636ce32854d3521e6051401 Mon Sep 17 00:00:00 2001 From: Colin Sonnenberg Date: Sun, 15 Feb 2026 12:43:55 -0500 Subject: [PATCH 2/6] Updated test_deduplicate_problems.py and test_filter_successful_submissions.py files to correctly call the functions in backend/domain/services/base.py. Change due to shift from BaseMetricService class with static methods to standalone functions. --- .../test_deduplicate_problems.py | 18 +++++++++--------- .../test_filter_successful_submissions.py | 12 ++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/backend/tests/unit/base_metric_service/test_deduplicate_problems.py b/backend/tests/unit/base_metric_service/test_deduplicate_problems.py index 8abe255..df575ad 100644 --- a/backend/tests/unit/base_metric_service/test_deduplicate_problems.py +++ b/backend/tests/unit/base_metric_service/test_deduplicate_problems.py @@ -1,4 +1,4 @@ -from backend.domain.services import BaseMetricService +from backend.domain.services import _deduplicate_problems def test_deduplicate_problems_happy_path(mock_submission): @@ -20,11 +20,11 @@ def test_deduplicate_problems_happy_path(mock_submission): is_solved=True, ) - result = BaseMetricService._deduplicate_problems([submission1, submission2]) + result = _deduplicate_problems([submission1, submission2]) assert result == [submission1] def test_deduplicate_problems_empty_submissions_list(mock_submission): - result = BaseMetricService._deduplicate_problems([]) + result = _deduplicate_problems([]) assert result == [] def test_deduplicate_problems_single_submission(mock_submission): @@ -37,7 +37,7 @@ def test_deduplicate_problems_single_submission(mock_submission): is_solved=False, ) - result = BaseMetricService._deduplicate_problems([submission1]) + result = _deduplicate_problems([submission1]) assert result == [submission1] @@ -69,7 +69,7 @@ def test_deduplicate_problems_all_unique_submissions(mock_submission): is_solved=True, ) - result = BaseMetricService._deduplicate_problems([submission1, submission2, submission3]) + result = _deduplicate_problems([submission1, submission2, submission3]) assert result == [submission1, submission2, submission3] @@ -101,7 +101,7 @@ def test_deduplicate_problems_multiple_duplicates(mock_submission): is_solved=False, ) - result = BaseMetricService._deduplicate_problems([submission1, submission2, submission3]) + result = _deduplicate_problems([submission1, submission2, submission3]) assert result == [submission1] @@ -133,7 +133,7 @@ def test_deduplicate_problems_preserves_order(mock_submission): is_solved=False, ) - result = BaseMetricService._deduplicate_problems([submission1, submission2, submission3]) + result = _deduplicate_problems([submission1, submission2, submission3]) assert result == [submission1, submission2] @@ -156,7 +156,7 @@ def test_deduplicate_problems_same_index_different_contest_id(mock_submission): is_solved=True, ) - result = BaseMetricService._deduplicate_problems([submission1, submission2]) + result = _deduplicate_problems([submission1, submission2]) assert result == [submission1, submission2] def test_deduplicate_problems_different_index_same_contest_id(mock_submission): @@ -179,5 +179,5 @@ def test_deduplicate_problems_different_index_same_contest_id(mock_submission): is_solved=True, ) - result = BaseMetricService._deduplicate_problems([submission1, submission2]) + result = _deduplicate_problems([submission1, submission2]) assert result == [submission1, submission2] diff --git a/backend/tests/unit/base_metric_service/test_filter_successful_submissions.py b/backend/tests/unit/base_metric_service/test_filter_successful_submissions.py index 5b706e4..3c3ed52 100644 --- a/backend/tests/unit/base_metric_service/test_filter_successful_submissions.py +++ b/backend/tests/unit/base_metric_service/test_filter_successful_submissions.py @@ -1,4 +1,4 @@ -from backend.domain.services import BaseMetricService +from backend.domain.services import _filter_successful_submissions def test_filter_successful_submissions_happy_path(mock_submission): @@ -29,12 +29,12 @@ def test_filter_successful_submissions_happy_path(mock_submission): is_solved=True, ) - result = BaseMetricService._filter_successful_submissions([submission1, submission2, submission3]) + result = _filter_successful_submissions([submission1, submission2, submission3]) assert result == [submission2, submission3] def test_filter_successful_submissions_no_submissions(mock_submission): - result = BaseMetricService._filter_successful_submissions([]) + result = _filter_successful_submissions([]) assert result == [] def test_filter_successful_submissions_only_not_solved_submissions(mock_submission): @@ -65,7 +65,7 @@ def test_filter_successful_submissions_only_not_solved_submissions(mock_submissi is_solved=False, ) - result = BaseMetricService._filter_successful_submissions([submission1, submission2, submission3]) + result = _filter_successful_submissions([submission1, submission2, submission3]) assert result == [] @@ -97,7 +97,7 @@ def test_filter_successful_submissions_only_solved_submissions(mock_submission): is_solved=True, ) - result = BaseMetricService._filter_successful_submissions([submission1, submission2, submission3]) + result = _filter_successful_submissions([submission1, submission2, submission3]) assert result == [submission1, submission2, submission3] @@ -129,7 +129,7 @@ def test_filter_successful_submissions_one_solved_submission_to_the_problem(mock is_solved=False, ) - result = BaseMetricService._filter_successful_submissions([submission1, submission2, submission3]) + result = _filter_successful_submissions([submission1, submission2, submission3]) assert result == [submission2] From 107f9a4582da92dc234ea4e0e98ae046586bfcf1 Mon Sep 17 00:00:00 2001 From: Colin Sonnenberg Date: Tue, 17 Feb 2026 20:07:01 -0500 Subject: [PATCH 3/6] Created class SubmissionProcessor in base.py and updated services files + test files to accommodate. --- backend/domain/services/__init__.py | 5 +- backend/domain/services/base.py | 72 ++++++++++--------- .../difficulty_distribution_service.py | 9 ++- backend/domain/services/tags_service.py | 7 +- .../test_deduplicate_problems.py | 26 ++++--- .../test_filter_successful_submissions.py | 17 +++-- 6 files changed, 78 insertions(+), 58 deletions(-) diff --git a/backend/domain/services/__init__.py b/backend/domain/services/__init__.py index bbe5101..edc4597 100644 --- a/backend/domain/services/__init__.py +++ b/backend/domain/services/__init__.py @@ -1,14 +1,13 @@ """Domain services package.""" -from .base import _deduplicate_problems, _filter_successful_submissions +from .base import SubmissionProcessor from .abandoned_problems_service import AbandonedProblemsService from .difficulty_distribution_service import DifficultyDistributionService from .tags_service import TagsService __all__ = [ + "SubmissionProcessor", "AbandonedProblemsService", - "_filter_successful_submissions", - "_deduplicate_problems", "DifficultyDistributionService", "TagsService", ] diff --git a/backend/domain/services/base.py b/backend/domain/services/base.py index 344e912..5e48fb3 100644 --- a/backend/domain/services/base.py +++ b/backend/domain/services/base.py @@ -2,35 +2,43 @@ from backend.domain.models.codeforces import Submission -def _deduplicate_problems(submissions: List[Submission]) -> List[Submission]: - """ - Keep only the first successful solve for each unique problem. - - Args: - submissions: List of submissions to deduplicate - - Returns: - List of submissions with only first solve per problem - """ - seen_problems = set() - unique_submissions = [] - - for submission in submissions: - problem_key = submission.problem.problem_key - if problem_key not in seen_problems: - seen_problems.add(problem_key) - unique_submissions.append(submission) - - return unique_submissions - -def _filter_successful_submissions(submissions: List[Submission]) -> List[Submission]: - """ - Filter submissions to only include solved problems. - - Args: - submissions: List of all submissions - - Returns: - List of only solved submissions - """ - return [s for s in submissions if s.is_solved] \ No newline at end of file +class SubmissionProcessor: + """Deduplicates problems and filters successful submissions.""" + + def __init__(self, submissions: List[Submission]): + """ + Initialize the instance which contains submissions + + Args: + submissions: List of submissions to perform operations on + """ + self._submissions = submissions + + def _deduplicate_problems(self) -> List[Submission]: + """ + Keep only the first successful solve for each unique problem. + + Returns: + List of submissions with only first solve per problem + """ + seen_problems = set() + unique_submissions = [] + + for submission in self._submissions: + problem_key = submission.problem.problem_key + if problem_key not in seen_problems: + seen_problems.add(problem_key) + unique_submissions.append(submission) + + self._submissions = unique_submissions + return self._submissions + + def _filter_successful_submissions(self) -> List[Submission]: + """ + Filter submissions to only include solved problems. + + Returns: + List of only solved submissions + """ + self._submissions = [s for s in self._submissions if s.is_solved] + return self._submissions \ No newline at end of file diff --git a/backend/domain/services/difficulty_distribution_service.py b/backend/domain/services/difficulty_distribution_service.py index aaf4112..364d547 100644 --- a/backend/domain/services/difficulty_distribution_service.py +++ b/backend/domain/services/difficulty_distribution_service.py @@ -8,7 +8,7 @@ DifficultyDistribution, RatingRange, ) -from backend.domain.services.base import _deduplicate_problems, _filter_successful_submissions +from backend.domain.services.base import SubmissionProcessor class DifficultyDistributionService(): @@ -61,9 +61,8 @@ def analyze_difficulty_distribution( DifficultyDistribution with analyzed data """ # Filter successful submissions - successful_submissions = _filter_successful_submissions( - submissions - ) + processor = SubmissionProcessor(submissions) + successful_submissions = processor._filter_successful_submissions() if not successful_submissions: return DifficultyDistribution( @@ -73,7 +72,7 @@ def analyze_difficulty_distribution( ) # Remove duplicate problems (keep first solve) - unique_solves = _deduplicate_problems(successful_submissions) + unique_solves = processor._deduplicate_problems() # Group problems by rating bins bin_counts = DifficultyDistributionService._create_bin_distribution(unique_solves) diff --git a/backend/domain/services/tags_service.py b/backend/domain/services/tags_service.py index a3ddbca..80f8e89 100644 --- a/backend/domain/services/tags_service.py +++ b/backend/domain/services/tags_service.py @@ -6,7 +6,7 @@ from backend.domain.models.codeforces import Submission from backend.domain.models.tags import TagInfo, TagsAnalysis -from backend.domain.services.base import _deduplicate_problems, _filter_successful_submissions +from backend.domain.services.base import SubmissionProcessor class TagsService(): @@ -25,7 +25,8 @@ def analyze_tags(handle: str, submissions: List[Submission]) -> TagsAnalysis: TagsAnalysis with analyzed data """ # Filter successful submissions - successful_submissions = _filter_successful_submissions(submissions) + processor = SubmissionProcessor(submissions) + successful_submissions = processor._filter_successful_submissions() if not successful_submissions: return TagsAnalysis( @@ -37,7 +38,7 @@ def analyze_tags(handle: str, submissions: List[Submission]) -> TagsAnalysis: ) # Remove duplicate problems (keep first solve) - unique_solves = _deduplicate_problems(successful_submissions) + unique_solves = processor._deduplicate_problems() # Group problems by tags and calculate statistics tags_data, overall_ratings = TagsService._analyze_tags(unique_solves) diff --git a/backend/tests/unit/base_metric_service/test_deduplicate_problems.py b/backend/tests/unit/base_metric_service/test_deduplicate_problems.py index df575ad..58c3dea 100644 --- a/backend/tests/unit/base_metric_service/test_deduplicate_problems.py +++ b/backend/tests/unit/base_metric_service/test_deduplicate_problems.py @@ -1,4 +1,4 @@ -from backend.domain.services import _deduplicate_problems +from backend.domain.services.base import SubmissionProcessor def test_deduplicate_problems_happy_path(mock_submission): @@ -20,11 +20,13 @@ def test_deduplicate_problems_happy_path(mock_submission): is_solved=True, ) - result = _deduplicate_problems([submission1, submission2]) + processor = SubmissionProcessor([submission1, submission2]) + result = processor._deduplicate_problems() assert result == [submission1] def test_deduplicate_problems_empty_submissions_list(mock_submission): - result = _deduplicate_problems([]) + processor = SubmissionProcessor([]) + result = processor._deduplicate_problems() assert result == [] def test_deduplicate_problems_single_submission(mock_submission): @@ -37,7 +39,8 @@ def test_deduplicate_problems_single_submission(mock_submission): is_solved=False, ) - result = _deduplicate_problems([submission1]) + processor = SubmissionProcessor([submission1]) + result = processor._deduplicate_problems() assert result == [submission1] @@ -69,7 +72,8 @@ def test_deduplicate_problems_all_unique_submissions(mock_submission): is_solved=True, ) - result = _deduplicate_problems([submission1, submission2, submission3]) + processor = SubmissionProcessor([submission1, submission2, submission3]) + result = processor._deduplicate_problems() assert result == [submission1, submission2, submission3] @@ -101,7 +105,8 @@ def test_deduplicate_problems_multiple_duplicates(mock_submission): is_solved=False, ) - result = _deduplicate_problems([submission1, submission2, submission3]) + processor = SubmissionProcessor([submission1, submission2, submission3]) + result = processor._deduplicate_problems() assert result == [submission1] @@ -133,7 +138,8 @@ def test_deduplicate_problems_preserves_order(mock_submission): is_solved=False, ) - result = _deduplicate_problems([submission1, submission2, submission3]) + processor = SubmissionProcessor([submission1, submission2, submission3]) + result = processor._deduplicate_problems() assert result == [submission1, submission2] @@ -156,7 +162,8 @@ def test_deduplicate_problems_same_index_different_contest_id(mock_submission): is_solved=True, ) - result = _deduplicate_problems([submission1, submission2]) + processor = SubmissionProcessor([submission1, submission2]) + result = processor._deduplicate_problems() assert result == [submission1, submission2] def test_deduplicate_problems_different_index_same_contest_id(mock_submission): @@ -179,5 +186,6 @@ def test_deduplicate_problems_different_index_same_contest_id(mock_submission): is_solved=True, ) - result = _deduplicate_problems([submission1, submission2]) + processor = SubmissionProcessor([submission1, submission2]) + result = processor._deduplicate_problems() assert result == [submission1, submission2] diff --git a/backend/tests/unit/base_metric_service/test_filter_successful_submissions.py b/backend/tests/unit/base_metric_service/test_filter_successful_submissions.py index 3c3ed52..3d222c0 100644 --- a/backend/tests/unit/base_metric_service/test_filter_successful_submissions.py +++ b/backend/tests/unit/base_metric_service/test_filter_successful_submissions.py @@ -1,4 +1,4 @@ -from backend.domain.services import _filter_successful_submissions +from backend.domain.services.base import SubmissionProcessor def test_filter_successful_submissions_happy_path(mock_submission): @@ -29,12 +29,14 @@ def test_filter_successful_submissions_happy_path(mock_submission): is_solved=True, ) - result = _filter_successful_submissions([submission1, submission2, submission3]) + processor = SubmissionProcessor([submission1, submission2, submission3]) + result = processor._filter_successful_submissions() assert result == [submission2, submission3] def test_filter_successful_submissions_no_submissions(mock_submission): - result = _filter_successful_submissions([]) + processor = SubmissionProcessor([]) + result = processor._filter_successful_submissions() assert result == [] def test_filter_successful_submissions_only_not_solved_submissions(mock_submission): @@ -65,7 +67,8 @@ def test_filter_successful_submissions_only_not_solved_submissions(mock_submissi is_solved=False, ) - result = _filter_successful_submissions([submission1, submission2, submission3]) + processor = SubmissionProcessor([submission1, submission2, submission3]) + result = processor._filter_successful_submissions() assert result == [] @@ -97,7 +100,8 @@ def test_filter_successful_submissions_only_solved_submissions(mock_submission): is_solved=True, ) - result = _filter_successful_submissions([submission1, submission2, submission3]) + processor = SubmissionProcessor([submission1, submission2, submission3]) + result = processor._filter_successful_submissions() assert result == [submission1, submission2, submission3] @@ -129,7 +133,8 @@ def test_filter_successful_submissions_one_solved_submission_to_the_problem(mock is_solved=False, ) - result = _filter_successful_submissions([submission1, submission2, submission3]) + processor = SubmissionProcessor([submission1, submission2, submission3]) + result = processor._filter_successful_submissions() assert result == [submission2] From 54bf387d94a2c1a81620c50cb7431bc8ab714b4d Mon Sep 17 00:00:00 2001 From: Colin Sonnenberg Date: Wed, 18 Feb 2026 18:52:24 -0500 Subject: [PATCH 4/6] Renamed class to SubmissionCollection for better clarity. Shifted from private to public functions. Retain original submissions and perform operations on working copy. --- backend/domain/services/base.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/backend/domain/services/base.py b/backend/domain/services/base.py index 5e48fb3..d7753b8 100644 --- a/backend/domain/services/base.py +++ b/backend/domain/services/base.py @@ -2,19 +2,27 @@ from backend.domain.models.codeforces import Submission -class SubmissionProcessor: - """Deduplicates problems and filters successful submissions.""" +class SubmissionCollection: + """Encapsulates submissions with filtering and deduplication operations.""" - def __init__(self, submissions: List[Submission]): + def __init__(self, submissions: List[Submission]): """ - Initialize the instance which contains submissions + Initialize an instance which contains original list of submissions and + a working copy to filter and/or deduplicate. Args: submissions: List of submissions to perform operations on """ + self._original_submissions = submissions self._submissions = submissions - def _deduplicate_problems(self) -> List[Submission]: + def reset_submissions(self) -> None: + """ + Reset the working copy of submissions to the original list. + """ + self._submissions = self._original_submissions + + def deduplicate_problems(self) -> List[Submission]: """ Keep only the first successful solve for each unique problem. @@ -33,7 +41,7 @@ def _deduplicate_problems(self) -> List[Submission]: self._submissions = unique_submissions return self._submissions - def _filter_successful_submissions(self) -> List[Submission]: + def filter_successful_submissions(self) -> List[Submission]: """ Filter submissions to only include solved problems. From 99551701be5c17cee76c93cab387921b474870c5 Mon Sep 17 00:00:00 2001 From: Colin Sonnenberg Date: Wed, 18 Feb 2026 18:54:29 -0500 Subject: [PATCH 5/6] Updated object references in service and test files. Updated class name in ARCHITECTURE.md. Removed class reference in __init__.py as SubmissionCollection is a helper. --- backend/domain/services/__init__.py | 3 -- .../domain/services/daily_activity_service.py | 3 +- .../difficulty_distribution_service.py | 8 ++--- backend/domain/services/tags_service.py | 8 ++--- .../test_deduplicate_problems.py | 34 +++++++++---------- .../test_filter_successful_submissions.py | 22 ++++++------ docs/ARCHITECTURE.md | 2 +- 7 files changed, 38 insertions(+), 42 deletions(-) diff --git a/backend/domain/services/__init__.py b/backend/domain/services/__init__.py index 88bbfcf..317c180 100644 --- a/backend/domain/services/__init__.py +++ b/backend/domain/services/__init__.py @@ -1,13 +1,10 @@ """Domain services package.""" - -from .base import SubmissionProcessor from .abandoned_problems_service import AbandonedProblemsService from .daily_activity_service import DailyActivityService from .difficulty_distribution_service import DifficultyDistributionService from .tags_service import TagsService __all__ = [ - "SubmissionProcessor", "AbandonedProblemsService", "DailyActivityService", "DifficultyDistributionService", diff --git a/backend/domain/services/daily_activity_service.py b/backend/domain/services/daily_activity_service.py index 36dacd9..01e4c03 100644 --- a/backend/domain/services/daily_activity_service.py +++ b/backend/domain/services/daily_activity_service.py @@ -7,7 +7,6 @@ from backend.domain.models.codeforces import Submission from backend.domain.models.daily_activity import DailyActivity, DailyActivityAnalysis from backend.domain.models.time_period import TimePeriod -from backend.domain.services.base import BaseMetricService _GRANULARITY_MAP = { TimePeriod.HOUR: "minute", @@ -61,7 +60,7 @@ def _advance(current: datetime, granularity: str) -> datetime: return current + _FIXED_STEPS[granularity] -class DailyActivityService(BaseMetricService): +class DailyActivityService(): """Service for generating activity analytics with variable granularity.""" @staticmethod diff --git a/backend/domain/services/difficulty_distribution_service.py b/backend/domain/services/difficulty_distribution_service.py index 364d547..284f43a 100644 --- a/backend/domain/services/difficulty_distribution_service.py +++ b/backend/domain/services/difficulty_distribution_service.py @@ -8,7 +8,7 @@ DifficultyDistribution, RatingRange, ) -from backend.domain.services.base import SubmissionProcessor +from backend.domain.services.base import SubmissionCollection class DifficultyDistributionService(): @@ -61,8 +61,8 @@ def analyze_difficulty_distribution( DifficultyDistribution with analyzed data """ # Filter successful submissions - processor = SubmissionProcessor(submissions) - successful_submissions = processor._filter_successful_submissions() + submission_collection = SubmissionCollection(submissions) + successful_submissions = submission_collection.filter_successful_submissions() if not successful_submissions: return DifficultyDistribution( @@ -72,7 +72,7 @@ def analyze_difficulty_distribution( ) # Remove duplicate problems (keep first solve) - unique_solves = processor._deduplicate_problems() + unique_solves = submission_collection.deduplicate_problems() # Group problems by rating bins bin_counts = DifficultyDistributionService._create_bin_distribution(unique_solves) diff --git a/backend/domain/services/tags_service.py b/backend/domain/services/tags_service.py index 80f8e89..8f16cd0 100644 --- a/backend/domain/services/tags_service.py +++ b/backend/domain/services/tags_service.py @@ -6,7 +6,7 @@ from backend.domain.models.codeforces import Submission from backend.domain.models.tags import TagInfo, TagsAnalysis -from backend.domain.services.base import SubmissionProcessor +from backend.domain.services.base import SubmissionCollection class TagsService(): @@ -25,8 +25,8 @@ def analyze_tags(handle: str, submissions: List[Submission]) -> TagsAnalysis: TagsAnalysis with analyzed data """ # Filter successful submissions - processor = SubmissionProcessor(submissions) - successful_submissions = processor._filter_successful_submissions() + submission_collection = SubmissionCollection(submissions) + successful_submissions = submission_collection.filter_successful_submissions() if not successful_submissions: return TagsAnalysis( @@ -38,7 +38,7 @@ def analyze_tags(handle: str, submissions: List[Submission]) -> TagsAnalysis: ) # Remove duplicate problems (keep first solve) - unique_solves = processor._deduplicate_problems() + unique_solves = submission_collection.deduplicate_problems() # Group problems by tags and calculate statistics tags_data, overall_ratings = TagsService._analyze_tags(unique_solves) diff --git a/backend/tests/unit/base_metric_service/test_deduplicate_problems.py b/backend/tests/unit/base_metric_service/test_deduplicate_problems.py index 58c3dea..63e399a 100644 --- a/backend/tests/unit/base_metric_service/test_deduplicate_problems.py +++ b/backend/tests/unit/base_metric_service/test_deduplicate_problems.py @@ -1,4 +1,4 @@ -from backend.domain.services.base import SubmissionProcessor +from backend.domain.services.base import SubmissionCollection def test_deduplicate_problems_happy_path(mock_submission): @@ -20,13 +20,13 @@ def test_deduplicate_problems_happy_path(mock_submission): is_solved=True, ) - processor = SubmissionProcessor([submission1, submission2]) - result = processor._deduplicate_problems() + submission_collection = SubmissionCollection([submission1, submission2]) + result = submission_collection.deduplicate_problems() assert result == [submission1] def test_deduplicate_problems_empty_submissions_list(mock_submission): - processor = SubmissionProcessor([]) - result = processor._deduplicate_problems() + submission_collection = SubmissionCollection([]) + result = submission_collection.deduplicate_problems() assert result == [] def test_deduplicate_problems_single_submission(mock_submission): @@ -39,8 +39,8 @@ def test_deduplicate_problems_single_submission(mock_submission): is_solved=False, ) - processor = SubmissionProcessor([submission1]) - result = processor._deduplicate_problems() + submission_collection = SubmissionCollection([submission1]) + result = submission_collection.deduplicate_problems() assert result == [submission1] @@ -72,8 +72,8 @@ def test_deduplicate_problems_all_unique_submissions(mock_submission): is_solved=True, ) - processor = SubmissionProcessor([submission1, submission2, submission3]) - result = processor._deduplicate_problems() + submission_collection = SubmissionCollection([submission1, submission2, submission3]) + result = submission_collection.deduplicate_problems() assert result == [submission1, submission2, submission3] @@ -105,8 +105,8 @@ def test_deduplicate_problems_multiple_duplicates(mock_submission): is_solved=False, ) - processor = SubmissionProcessor([submission1, submission2, submission3]) - result = processor._deduplicate_problems() + submission_collection = SubmissionCollection([submission1, submission2, submission3]) + result = submission_collection.deduplicate_problems() assert result == [submission1] @@ -138,8 +138,8 @@ def test_deduplicate_problems_preserves_order(mock_submission): is_solved=False, ) - processor = SubmissionProcessor([submission1, submission2, submission3]) - result = processor._deduplicate_problems() + submission_collection = SubmissionCollection([submission1, submission2, submission3]) + result = submission_collection.deduplicate_problems() assert result == [submission1, submission2] @@ -162,8 +162,8 @@ def test_deduplicate_problems_same_index_different_contest_id(mock_submission): is_solved=True, ) - processor = SubmissionProcessor([submission1, submission2]) - result = processor._deduplicate_problems() + submission_collection = SubmissionCollection([submission1, submission2]) + result = submission_collection.deduplicate_problems() assert result == [submission1, submission2] def test_deduplicate_problems_different_index_same_contest_id(mock_submission): @@ -186,6 +186,6 @@ def test_deduplicate_problems_different_index_same_contest_id(mock_submission): is_solved=True, ) - processor = SubmissionProcessor([submission1, submission2]) - result = processor._deduplicate_problems() + submission_collection = SubmissionCollection([submission1, submission2]) + result = submission_collection.deduplicate_problems() assert result == [submission1, submission2] diff --git a/backend/tests/unit/base_metric_service/test_filter_successful_submissions.py b/backend/tests/unit/base_metric_service/test_filter_successful_submissions.py index 3d222c0..f374d4d 100644 --- a/backend/tests/unit/base_metric_service/test_filter_successful_submissions.py +++ b/backend/tests/unit/base_metric_service/test_filter_successful_submissions.py @@ -1,4 +1,4 @@ -from backend.domain.services.base import SubmissionProcessor +from backend.domain.services.base import SubmissionCollection def test_filter_successful_submissions_happy_path(mock_submission): @@ -29,14 +29,14 @@ def test_filter_successful_submissions_happy_path(mock_submission): is_solved=True, ) - processor = SubmissionProcessor([submission1, submission2, submission3]) - result = processor._filter_successful_submissions() + submission_collection = SubmissionCollection([submission1, submission2, submission3]) + result = submission_collection.filter_successful_submissions() assert result == [submission2, submission3] def test_filter_successful_submissions_no_submissions(mock_submission): - processor = SubmissionProcessor([]) - result = processor._filter_successful_submissions() + submission_collection = SubmissionCollection([]) + result = submission_collection.filter_successful_submissions() assert result == [] def test_filter_successful_submissions_only_not_solved_submissions(mock_submission): @@ -67,8 +67,8 @@ def test_filter_successful_submissions_only_not_solved_submissions(mock_submissi is_solved=False, ) - processor = SubmissionProcessor([submission1, submission2, submission3]) - result = processor._filter_successful_submissions() + submission_collection = SubmissionCollection([submission1, submission2, submission3]) + result = submission_collection.filter_successful_submissions() assert result == [] @@ -100,8 +100,8 @@ def test_filter_successful_submissions_only_solved_submissions(mock_submission): is_solved=True, ) - processor = SubmissionProcessor([submission1, submission2, submission3]) - result = processor._filter_successful_submissions() + submission_collection = SubmissionCollection([submission1, submission2, submission3]) + result = submission_collection.filter_successful_submissions() assert result == [submission1, submission2, submission3] @@ -133,8 +133,8 @@ def test_filter_successful_submissions_one_solved_submission_to_the_problem(mock is_solved=False, ) - processor = SubmissionProcessor([submission1, submission2, submission3]) - result = processor._filter_successful_submissions() + submission_collection = SubmissionCollection([submission1, submission2, submission3]) + result = submission_collection.filter_successful_submissions() assert result == [submission2] diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ede063e..0b9e7b0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -111,7 +111,7 @@ HTTP Request → Route Handler → Check Redis Cache ### Key Backend Features **Submission Deduplication:** -- `BaseMetricService._deduplicate_problems()` keeps only the first solve per problem +- `SubmissionCollection.deduplicate_problems()` keeps only the first solve per problem - Problems identified by `(contest_id, index)` tuple - Critical for accurate statistics since users may solve problems multiple times From f47662aabde3516a4984fea86983c6a5ab6b368d Mon Sep 17 00:00:00 2001 From: Colin Sonnenberg Date: Sat, 21 Feb 2026 07:22:21 +0900 Subject: [PATCH 6/6] Never overwrite self._submissions. Create copies of collection for modification within class methods. Updated method calls in associated backend services and testing files. --- backend/domain/services/base.py | 32 +++++++++---------- .../difficulty_distribution_service.py | 2 +- backend/domain/services/tags_service.py | 2 +- backend/package-lock.json | 6 ++++ 4 files changed, 23 insertions(+), 19 deletions(-) create mode 100644 backend/package-lock.json diff --git a/backend/domain/services/base.py b/backend/domain/services/base.py index d7753b8..156173d 100644 --- a/backend/domain/services/base.py +++ b/backend/domain/services/base.py @@ -1,5 +1,5 @@ +import copy from typing import List - from backend.domain.models.codeforces import Submission class SubmissionCollection: @@ -7,39 +7,36 @@ class SubmissionCollection: def __init__(self, submissions: List[Submission]): """ - Initialize an instance which contains original list of submissions and - a working copy to filter and/or deduplicate. + Initialize an instance which contains original list of submissions. Args: - submissions: List of submissions to perform operations on + submissions: List of user's submissions """ - self._original_submissions = submissions self._submissions = submissions - - def reset_submissions(self) -> None: - """ - Reset the working copy of submissions to the original list. - """ - self._submissions = self._original_submissions - def deduplicate_problems(self) -> List[Submission]: + def deduplicate_problems(self, submissions: List[Submission]=[]) -> List[Submission]: """ Keep only the first successful solve for each unique problem. + Args: + submissions: List of user's submissions to deduplicate + Returns: List of submissions with only first solve per problem """ seen_problems = set() unique_submissions = [] - for submission in self._submissions: + # Preference given to submissions argument + submissions_copy = submissions if submissions else copy.copy(self._submissions) + + for submission in submissions_copy: problem_key = submission.problem.problem_key if problem_key not in seen_problems: seen_problems.add(problem_key) unique_submissions.append(submission) - self._submissions = unique_submissions - return self._submissions + return unique_submissions def filter_successful_submissions(self) -> List[Submission]: """ @@ -48,5 +45,6 @@ def filter_successful_submissions(self) -> List[Submission]: Returns: List of only solved submissions """ - self._submissions = [s for s in self._submissions if s.is_solved] - return self._submissions \ No newline at end of file + submissions_copy = copy.copy(self._submissions) + successful_submissions = [s for s in submissions_copy if s.is_solved] + return successful_submissions \ No newline at end of file diff --git a/backend/domain/services/difficulty_distribution_service.py b/backend/domain/services/difficulty_distribution_service.py index 284f43a..e3eb0ca 100644 --- a/backend/domain/services/difficulty_distribution_service.py +++ b/backend/domain/services/difficulty_distribution_service.py @@ -72,7 +72,7 @@ def analyze_difficulty_distribution( ) # Remove duplicate problems (keep first solve) - unique_solves = submission_collection.deduplicate_problems() + unique_solves = submission_collection.deduplicate_problems(successful_submissions) # Group problems by rating bins bin_counts = DifficultyDistributionService._create_bin_distribution(unique_solves) diff --git a/backend/domain/services/tags_service.py b/backend/domain/services/tags_service.py index 8f16cd0..0b2fdf5 100644 --- a/backend/domain/services/tags_service.py +++ b/backend/domain/services/tags_service.py @@ -38,7 +38,7 @@ def analyze_tags(handle: str, submissions: List[Submission]) -> TagsAnalysis: ) # Remove duplicate problems (keep first solve) - unique_solves = submission_collection.deduplicate_problems() + unique_solves = submission_collection.deduplicate_problems(successful_submissions) # Group problems by tags and calculate statistics tags_data, overall_ratings = TagsService._analyze_tags(unique_solves) diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..dfb18f1 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "backend", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}