Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions quizzes/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,3 +633,26 @@ def validate_folder_id(self, value):
class RecordAnswerSerializer(serializers.Serializer):
question_id = serializers.UUIDField()
selected_answers = serializers.ListField(allow_empty=False)


class QuestionStatsSerializer(serializers.Serializer):
"""Serializer for per-question statistics within a quiz stats response."""

question_id = serializers.UUIDField()
attempts = serializers.IntegerField()
correct_attempts = serializers.IntegerField()
last_answered_at = serializers.DateTimeField(allow_null=True)


class QuizStatsSerializer(serializers.Serializer):
"""Serializer for aggregated quiz statistics for the current user."""

quiz_id = serializers.UUIDField()
total_answers = serializers.IntegerField()
correct_answers = serializers.IntegerField()
wrong_answers = serializers.IntegerField()
accuracy = serializers.FloatField()
study_time_seconds = serializers.IntegerField()
sessions_count = serializers.IntegerField()
last_activity_at = serializers.DateTimeField(allow_null=True)
per_question = QuestionStatsSerializer(many=True, required=False)
88 changes: 88 additions & 0 deletions quizzes/services/stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from django.db.models import Count, Max, Q

from quizzes.models import AnswerRecord, QuizSession


def get_quiz_stats(quiz, user, *, include_per_question: bool = False) -> dict:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

potrzebujemy rozdzielić i zwracać i ogólne i per user, spójrz tutaj co powinno być zwracane

Solvro/pm-testownik#28 (comment)

"""
Compute aggregated quiz statistics for a specific user.

Aggregates answer data across all sessions (active + archived) for the given quiz/user pair.
Study time is taken from the active session only.

Args:
quiz: Quiz instance to compute stats for.
user: User instance to compute stats for.
include_per_question: If True, includes per-question breakdown in the result.

Returns:
A dict with aggregated stats ready to be passed to QuizStatsSerializer.
"""
sessions = QuizSession.objects.filter(quiz=quiz, user=user)

# Aggregate basic session stats in a single query to avoid extra round-trips.
session_aggregates = sessions.aggregate(
sessions_count=Count("id"),
last_activity_at=Max("updated_at"),
)
sessions_count = session_aggregates["sessions_count"] or 0
last_activity_at = session_aggregates["last_activity_at"]

# Aggregate answer data directly from AnswerRecord to avoid cross-join row duplication.
answer_aggregates = AnswerRecord.objects.filter(session__in=sessions).aggregate(
total_answers=Count("id"),
correct_answers=Count("id", filter=Q(was_correct=True)),
)

total_answers = answer_aggregates["total_answers"] or 0
correct_answers = answer_aggregates["correct_answers"] or 0
wrong_answers = total_answers - correct_answers

accuracy = round(correct_answers / total_answers * 100, 2) if total_answers > 0 else 0.0

active_study_time = sessions.filter(is_active=True).values_list("study_time", flat=True).first()
study_time_seconds = int(active_study_time.total_seconds()) if active_study_time else 0

result = {
"quiz_id": quiz.id,
"total_answers": total_answers,
"correct_answers": correct_answers,
"wrong_answers": wrong_answers,
"accuracy": accuracy,
"study_time_seconds": study_time_seconds,
"sessions_count": sessions_count,
"last_activity_at": last_activity_at,
}

if include_per_question:
result["per_question"] = _get_per_question_stats(sessions)

return result


def _get_per_question_stats(sessions) -> list[dict]:
"""
Compute per-question statistics for the given sessions queryset.

Returns a list of dicts with question_id, attempts, correct_attempts, and last_answered_at.
"""
per_question = (
AnswerRecord.objects.filter(session__in=sessions)
.values("question_id")
.annotate(
attempts=Count("id"),
correct_attempts=Count("id", filter=Q(was_correct=True)),
last_answered_at=Max("answered_at"),
)
.order_by("question_id")
)

return [
{
"question_id": row["question_id"],
"attempts": row["attempts"],
"correct_attempts": row["correct_attempts"],
"last_answered_at": row["last_answered_at"],
}
for row in per_question
]
Loading
Loading