Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skill time #395

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
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
92 changes: 90 additions & 2 deletions api/endpoints/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
from secrets import token_urlsafe
from typing import Any, Iterable

from fastapi import APIRouter, Depends, Header, Query, Response
from fastapi import APIRouter, Depends, Header, Query, Request, Response

from api import models
from api.auth import public_auth, require_verified_email, user_auth
from api.auth import get_token, public_auth, require_verified_email, user_auth
from api.database import db, filter_by
from api.exceptions.auth import user_responses, verified_responses
from api.exceptions.course import (
Expand All @@ -19,10 +19,13 @@
NoCourseAccessException,
NotEnoughCoinsError,
)
from api.exceptions.view_time import DataFetchError
from api.redis import redis
from api.schemas.course import Course, CourseSummary, Lecture, NextUnseenResponse, UserCourse
from api.schemas.user import User
from api.schemas.view_time import TotalTime, ViewTime, ViewTimeLecture, ViewTimeSection, ViewTimeSubSkill
from api.services.auth import get_email
from api.services.challenges import challenge_subtasks
from api.services.courses import COURSES
from api.services.shop import has_premium, spend_coins
from api.settings import settings
Expand Down Expand Up @@ -305,3 +308,88 @@ async def buy_course(user: User = user_auth, course: Course = get_course) -> Any
await clear_cache("course_access")

return True


@router.get("/courses_viewtime", responses=responses(ViewTime))
async def get_course_viewtime(user: User = user_auth) -> Any:
"""
Return the total viewtime of all courses.

*Requirements:* **VERIFIED**
"""

completed_lectures: dict[str, set[str]] = {}

async for lecture in await db.stream(filter_by(models.LectureProgress, user_id=user.id)):
completed_lectures.setdefault(lecture.course_id, set()).add(lecture.lecture_id)

lecture_data = [
course.summary(None if completed_lectures is None else completed_lectures.get(course.id, set()))
for course in iter(COURSES.values())
]

sub_skill_reponses = []

for sub_skill in lecture_data:
total_time = 0
sections = []

for section in sub_skill.sections:
section_time = 0
lectures = []

for lecture in section.lectures:
if lecture.duration > 0 and lecture.completed:
lectures.append(ViewTimeLecture(lecture_name=lecture.title, time=lecture.duration))
section_time += lecture.duration

if section_time > 0:
sections.append(ViewTimeSection(section_name=section.title, total_time=section_time, lectures=lectures))
total_time += section_time

if total_time > 0:
sub_skill_reponses.append(
ViewTimeSubSkill(
sub_skill_id=sub_skill.id, sub_skill_name=sub_skill.title, total_time=total_time, sections=sections
)
)

return ViewTime(
total_time=sum([sub_skill.total_time for sub_skill in sub_skill_reponses]), sub_skills=sub_skill_reponses
)


@router.get("/tasks_viewtime", responses=responses(TotalTime, DataFetchError))
async def get_tasks_viewtime(request: Request, user: User = user_auth) -> Any:
"""
Return the total viewtime of all tasks.

*Requirements:* **VERIFIED**
"""
tasks_data = await challenge_subtasks(auth_token=get_token(request), solved=True)

if not isinstance(tasks_data, list):
return DataFetchError()

total_time = 0
for task in tasks_data:
task_type = task.type
if task_type == "MULTIPLE_CHOICE_QUESTION" or task_type == "MATCHING":
total_time += 60
elif task_type == "CODING_CHALLENGE":
total_time += 60 * 30

return TotalTime(total_time=total_time)


@router.get("/viewtime", responses=responses(TotalTime))
async def get_viewtime(request: Request, user: User = user_auth) -> Any:
"""
Return the total viewtime of all tasks.

*Requirements:* **VERIFIED**
"""
get_course_viewtime_response = await get_course_viewtime(user)
get_tasks_viewtime_response = await get_tasks_viewtime(request, user)

return TotalTime(total_time=get_course_viewtime_response.total_time + get_tasks_viewtime_response.total_time)
9 changes: 9 additions & 0 deletions api/exceptions/view_time.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from starlette import status

from api.exceptions.api_exception import APIException


class DataFetchError(APIException):
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
detail = "Failed to fetch task data"
description = "Failed to fetch task data from the database"
31 changes: 31 additions & 0 deletions api/schemas/subtasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from pydantic import BaseModel, Field

from api.utils.docs import example


class SubTask(BaseModel):
id: str = Field(description="ID of the sub task")
task_id: str = Field(description="Task ID of the task")
type: str = Field(description="Type of the sub task")
creator: str = Field(description="ID of the creator")
creation_timestamp: str = Field(description="Timestamp of the creation")
xp: int = Field(description="XP of the sub task")
coins: int = Field(description="Coins of the sub task")
solved: bool = Field(description="Whether the sub task is solved")
rated: bool = Field(description="Whether the sub task is rated")
enabled: bool = Field(description="Whether the sub task is enabled")
retired: bool = Field(description="Whether the sub task is retired")

Config = example(
id="3fa85f64-5717-4562-b3fc-2c963f66afa6",
task_id="3fa85f64-5717-4562-b3fc-2c963f66afa6",
type="CODING_CHALLENGE",
creator="3fa85f64-5717-4562-b3fc-2c963f66afa6",
creation_timestamp="2024-11-01T21:46:35.626Z",
xp=0,
coins=0,
solved=True,
rated=True,
enabled=True,
retired=True,
)
30 changes: 30 additions & 0 deletions api/schemas/view_time.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from typing import List

from pydantic import BaseModel


class ViewTimeLecture(BaseModel):
lecture_name: str
time: int


class ViewTimeSection(BaseModel):
section_name: str
total_time: int
lectures: List[ViewTimeLecture]


class ViewTimeSubSkill(BaseModel):
sub_skill_id: str
sub_skill_name: str
total_time: int
sections: List[ViewTimeSection]


class ViewTime(BaseModel):
total_time: int
sub_skills: List[ViewTimeSubSkill]


class TotalTime(BaseModel):
total_time: int
10 changes: 10 additions & 0 deletions api/services/challenges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from api.schemas.subtasks import SubTask
from api.services.internal import InternalService


async def challenge_subtasks(auth_token: str, solved: bool) -> list[SubTask]:
async with InternalService.CHALLENGES.client_external(auth_token) as client:
response = await client.get(f"/subtasks?solved={'true' if solved else 'false'}")
response.raise_for_status()
subtasks_data = response.json()
return [SubTask(**subtask) for subtask in subtasks_data]
8 changes: 8 additions & 0 deletions api/services/internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class InternalServiceError(Exception):
class InternalService(Enum):
AUTH = settings.auth_url
SHOP = settings.shop_url
CHALLENGES = settings.challenges_url

def _get_token(self) -> str:
return encode_jwt({"aud": self.name.lower()}, timedelta(seconds=settings.internal_jwt_ttl))
Expand All @@ -35,3 +36,10 @@ def client(self) -> AsyncClient:
headers={"Authorization": self._get_token()},
event_hooks={"response": [self._handle_error]},
)

def client_external(self, auth_token: str) -> AsyncClient:
return AsyncClient(
base_url=self.value,
headers={"Authorization": f"Bearer {auth_token}"},
event_hooks={"response": [self._handle_error]},
)
1 change: 1 addition & 0 deletions api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Settings(BaseSettings):

auth_url: str = ""
shop_url: str = ""
challenges_url: str = "https://localhost:8005" # TODO: change to real URL

lecture_xp: int = 10

Expand Down