Skip to content

Commit

Permalink
CU-8693pytr0_Results (#48)
Browse files Browse the repository at this point in the history
* Install matplotlib

* Try to show chart in message

* Fix related name

* Method for creating charts

* Test method for sending charts

* Show percentage of complited tasks

* Move the command to the subject

* Add histogram

* CU-8695xdy5k | Create one method

* Fix x_sticks

* Refactor code after review

* Freeze packages

* Refactor code

* Rename the method
  • Loading branch information
mezgoodle authored Sep 26, 2024
1 parent 6249fbe commit 3b28a0e
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 1 deletion.
Binary file modified requirements.txt
Binary file not shown.
1 change: 1 addition & 0 deletions tgbot/handlers/teacher.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ async def get_subjects(
{"text": "Invite students", "name": "add_subject"},
{"text": "Add task", "name": "add_task"},
{"text": "See tasks", "name": "see_tasks"},
{"text": "Show stats", "name": "subject_stats"},
],
)
await message.answer("Here are your subjects:")
Expand Down
75 changes: 75 additions & 0 deletions tgbot/misc/charts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import io
import logging
import os
import tempfile
from enum import Enum

import pandas as pd
import seaborn as sns
from aiogram.types import FSInputFile, Message
from matplotlib import pyplot as plt


class ChartType(Enum):
BAR = "bar"
HIST = "hist"


async def send_chart(
message: Message,
data: dict,
chart_type: ChartType,
x_legend: str | None = None,
y_legend: str | None = None,
title: str | None = None,
) -> Message:
if chart_type == ChartType.BAR:
ax = prepare_bar_chart(data, x_legend, y_legend, title)
elif chart_type == ChartType.HIST:
ax = prepare_hist_chart(data, x_legend, title)
else:
raise ValueError(f"Unknown chart type: {chart_type}")
try:
buffer = io.BytesIO()
fig = ax.get_figure()
fig.savefig(buffer, format="png")
buffer.seek(0)
plt.close(fig)
temp_file = tempfile.NamedTemporaryFile(
suffix=".png", delete=False
).name
with open(temp_file, "wb") as f:
f.write(buffer.read())
file = FSInputFile(temp_file)
except ValueError as ve:
logging.error(ve)
return await message.answer("Error creating chart. Try again later.")
except Exception as e:
logging.error(e)
return await message.answer("Unexpected error. Try again later.")
try:
return await message.answer_photo(file)
finally:
os.remove(temp_file)


def prepare_hist_chart(data: dict, x_legend: str, title: str):
sample_data = pd.DataFrame({x_legend: tuple(data[x_legend])})
_, ax = plt.subplots()
ax.set_xticks([1, 2, 3, 4, 5])
sns.histplot(x=x_legend, data=sample_data, ax=ax)
ax.set_title(title)
return ax


def prepare_bar_chart(data: dict, x_legend: str, y_legend: str, title: str):
sample_data = pd.DataFrame(
{
x_legend: tuple(data[x_legend]),
y_legend: tuple(data[y_legend]),
}
)
_, ax = plt.subplots()
sns.barplot(x=x_legend, y=y_legend, data=sample_data, ax=ax)
ax.set_title(title)
return ax
20 changes: 20 additions & 0 deletions tgbot/misc/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,26 @@ async def get_solutions_for_task(
.prefetch_related("student")
)

async def get_percentage_solutions_by_subject(
self, subject: Subject
) -> dict[str, int]:
students = await self.student.filter(subjects=subject).count()
tasks = await self.subjecttask.filter(subject=subject).all()
stats = {}
if students:
for task in tasks:
solutions = await self.solution.filter(
subject_task=task
).count()
stats[task.name] = solutions / students
return stats

async def get_grades_by_subject(self, subject: Subject) -> list[int]:
solutions = await self.solution.filter(
subject_task__subject=subject
).all()
return [solution.grade for solution in solutions]

async def get_student_solution(
self, student_id: int, subject_task_id: int
) -> Solution | None:
Expand Down
61 changes: 61 additions & 0 deletions tgbot/misc/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from loader import bot
from tgbot.keyboards.inline.support_keyboard import support_keyboard
from tgbot.keyboards.inline.task_keyboard import task_keyboard
from tgbot.misc.charts import ChartType, send_chart
from tgbot.misc.database import Database
from tgbot.models.models import Student, Subject, SubjectTask
from tgbot.states.states import Task
Expand Down Expand Up @@ -156,10 +157,70 @@ async def ask_teacher(
)


async def get_subject_statistics(
db: Database, subject_id: int
) -> tuple[Subject | None, dict | None, dict | None]:
subject = await db.get_subject(subject_id)
if not subject:
logging.error(f"Subject with id {subject_id} not found")
return None, None, None

try:
subject_stats = await db.get_percentage_solutions_by_subject(subject)
grades = await db.get_grades_by_subject(subject)
return subject, subject_stats, grades
except Exception as e:
logging.error(f"Error fetching subject statistics: {e}")
return subject, None, None


async def prepare_chart_data(subject_stats, grades):
if not subject_stats or not grades:
raise ValueError("Subject stats or grades not found.")
stats = {
"Tasks names": subject_stats.keys(),
"Solutions": subject_stats.values(),
}
grades_data = {"Grades": grades}
return stats, grades_data


async def subject_stats(
message: Message, payload: dict, db: Database, *args, **kwargs
) -> str:
subject, subject_stats, grades = await get_subject_statistics(
db, payload.get("id")
)
if not subject:
return await message.answer("Subject not found.")
if not grades:
return await message.answer(
f"Stats for subject {hbold(subject.name)}: No data"
)
stats, grades_data = await prepare_chart_data(subject_stats, grades)
await message.answer(f"Stats for subject {hbold(subject.name)}:")
await send_chart(
message=message,
data=grades_data,
x_legend="Grades",
title="Grades",
chart_type=ChartType.HIST,
)
return await send_chart(
message=message,
data=stats,
x_legend="Tasks names",
y_legend="Solutions",
title="Number of solutions for tasks",
chart_type=ChartType.BAR,
)


utils = {
"add_subject": add_student_to_subject,
"quit_subject": quit_student_to_subject,
"add_task": add_task,
"see_tasks": see_tasks,
"ask_teacher": ask_teacher,
"subject_stats": subject_stats,
}
2 changes: 1 addition & 1 deletion tgbot/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class Solution(TimedBaseModel):
subject_task: fields.ForeignKeyRelation[SubjectTask] = (
fields.ForeignKeyField(
"models.SubjectTask",
related_name="students",
related_name="solutions",
description="Task subject",
on_delete=fields.OnDelete.CASCADE,
)
Expand Down

0 comments on commit 3b28a0e

Please sign in to comment.