Skip to content
Open
3 changes: 0 additions & 3 deletions backend/domain/services/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
"""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
from .tags_service import TagsService

__all__ = [
"AbandonedProblemsService",
"BaseMetricService",
"DailyActivityService",
"DifficultyDistributionService",
"TagsService",
Expand Down
3 changes: 1 addition & 2 deletions backend/domain/services/abandoned_problems_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 22 additions & 14 deletions backend/domain/services/base.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,50 @@
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
"""
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)
unique_submissions.append(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
3 changes: 1 addition & 2 deletions backend/domain/services/daily_activity_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
11 changes: 5 additions & 6 deletions backend/domain/services/difficulty_distribution_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Expand Down
9 changes: 5 additions & 4 deletions backend/domain/services/tags_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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):
Expand All @@ -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]

Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -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]

Expand All @@ -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):
Expand All @@ -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]
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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 == []

Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -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]

2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down