diff --git a/backend/domain/services/__init__.py b/backend/domain/services/__init__.py index 7bc70fa..317c180 100644 --- a/backend/domain/services/__init__.py +++ b/backend/domain/services/__init__.py @@ -1,6 +1,4 @@ """Domain services package.""" - -from .base import BaseMetricService from .abandoned_problems_service import AbandonedProblemsService from .daily_activity_service import DailyActivityService from .difficulty_distribution_service import DifficultyDistributionService @@ -8,7 +6,6 @@ __all__ = [ "AbandonedProblemsService", - "BaseMetricService", "DailyActivityService", "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..156173d 100644 --- a/backend/domain/services/base.py +++ b/backend/domain/services/base.py @@ -1,18 +1,25 @@ +import copy from typing import List - from backend.domain.models.codeforces import Submission +class SubmissionCollection: + """Encapsulates submissions with filtering and deduplication operations.""" -class BaseMetricService: - """Base class for all metric services with shared functionality.""" - - @staticmethod - def _deduplicate_problems(submissions: List[Submission]) -> List[Submission]: + def __init__(self, submissions: List[Submission]): + """ + Initialize an instance which contains original list of submissions. + + Args: + submissions: List of user's submissions + """ + self._submissions = submissions + + def deduplicate_problems(self, submissions: List[Submission]=[]) -> List[Submission]: """ Keep only the first successful solve for each unique problem. Args: - submissions: List of submissions to deduplicate + submissions: List of user's submissions to deduplicate Returns: List of submissions with only first solve per problem @@ -20,7 +27,10 @@ def _deduplicate_problems(submissions: List[Submission]) -> List[Submission]: seen_problems = set() unique_submissions = [] - for submission in 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) @@ -28,15 +38,13 @@ def _deduplicate_problems(submissions: List[Submission]) -> List[Submission]: return unique_submissions - @staticmethod - def _filter_successful_submissions(submissions: List[Submission]) -> List[Submission]: + def filter_successful_submissions(self) -> 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] + 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/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 acacb28..e3eb0ca 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 SubmissionCollection -class DifficultyDistributionService(BaseMetricService): +class DifficultyDistributionService(): """Service for generating difficulty distribution analytics.""" # Standard rating bins for Codeforces (100-point intervals) @@ -61,9 +61,8 @@ def analyze_difficulty_distribution( DifficultyDistribution with analyzed data """ # Filter successful submissions - successful_submissions = DifficultyDistributionService._filter_successful_submissions( - submissions - ) + submission_collection = SubmissionCollection(submissions) + successful_submissions = submission_collection.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 = DifficultyDistributionService._deduplicate_problems(successful_submissions) + 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 d5ecda0..0b2fdf5 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 SubmissionCollection -class TagsService(BaseMetricService): +class TagsService(): """Service for generating tags analytics.""" @staticmethod @@ -25,7 +25,8 @@ def analyze_tags(handle: str, submissions: List[Submission]) -> TagsAnalysis: TagsAnalysis with analyzed data """ # Filter successful submissions - successful_submissions = TagsService._filter_successful_submissions(submissions) + submission_collection = SubmissionCollection(submissions) + successful_submissions = submission_collection.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 = TagsService._deduplicate_problems(successful_submissions) + 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": {} +} 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..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 import BaseMetricService +from backend.domain.services.base import SubmissionCollection def test_deduplicate_problems_happy_path(mock_submission): @@ -20,11 +20,13 @@ def test_deduplicate_problems_happy_path(mock_submission): is_solved=True, ) - result = BaseMetricService._deduplicate_problems([submission1, submission2]) + submission_collection = SubmissionCollection([submission1, submission2]) + result = submission_collection.deduplicate_problems() assert result == [submission1] def test_deduplicate_problems_empty_submissions_list(mock_submission): - result = BaseMetricService._deduplicate_problems([]) + submission_collection = SubmissionCollection([]) + result = submission_collection.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 = BaseMetricService._deduplicate_problems([submission1]) + submission_collection = SubmissionCollection([submission1]) + result = submission_collection.deduplicate_problems() assert result == [submission1] @@ -69,7 +72,8 @@ def test_deduplicate_problems_all_unique_submissions(mock_submission): is_solved=True, ) - result = BaseMetricService._deduplicate_problems([submission1, submission2, submission3]) + submission_collection = SubmissionCollection([submission1, submission2, submission3]) + result = submission_collection.deduplicate_problems() assert result == [submission1, submission2, submission3] @@ -101,7 +105,8 @@ def test_deduplicate_problems_multiple_duplicates(mock_submission): is_solved=False, ) - result = BaseMetricService._deduplicate_problems([submission1, submission2, submission3]) + submission_collection = SubmissionCollection([submission1, submission2, submission3]) + result = submission_collection.deduplicate_problems() assert result == [submission1] @@ -133,7 +138,8 @@ def test_deduplicate_problems_preserves_order(mock_submission): is_solved=False, ) - result = BaseMetricService._deduplicate_problems([submission1, submission2, submission3]) + submission_collection = SubmissionCollection([submission1, submission2, submission3]) + result = submission_collection.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 = BaseMetricService._deduplicate_problems([submission1, submission2]) + 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): @@ -179,5 +186,6 @@ def test_deduplicate_problems_different_index_same_contest_id(mock_submission): is_solved=True, ) - result = BaseMetricService._deduplicate_problems([submission1, submission2]) + 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 5b706e4..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 import BaseMetricService +from backend.domain.services.base import SubmissionCollection 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 = BaseMetricService._filter_successful_submissions([submission1, submission2, submission3]) + 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): - result = BaseMetricService._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): @@ -65,7 +67,8 @@ def test_filter_successful_submissions_only_not_solved_submissions(mock_submissi is_solved=False, ) - result = BaseMetricService._filter_successful_submissions([submission1, submission2, submission3]) + submission_collection = SubmissionCollection([submission1, submission2, submission3]) + result = submission_collection.filter_successful_submissions() assert result == [] @@ -97,7 +100,8 @@ def test_filter_successful_submissions_only_solved_submissions(mock_submission): is_solved=True, ) - result = BaseMetricService._filter_successful_submissions([submission1, submission2, submission3]) + submission_collection = SubmissionCollection([submission1, submission2, submission3]) + result = submission_collection.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 = BaseMetricService._filter_successful_submissions([submission1, submission2, submission3]) + 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