diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 1a3fca78..4bb70763 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -11,7 +11,6 @@ import "../../static/materials/repeating_queue.css"; import "../../static/materials/add_material.css"; import "../../static/notes/notes.css"; import "../../static/notes/add_note.css"; -import "../../static/reading_log/add_log_record.css"; import "../../static/cards/cards_list.css"; import "../../static/cards/add_card.css"; import "../../static/system/system.css"; diff --git a/frontend/src/pages/reading_log/AddReadingLogPage.tsx b/frontend/src/pages/reading_log/AddReadingLogPage.tsx index 287d5ee1..a973674d 100644 --- a/frontend/src/pages/reading_log/AddReadingLogPage.tsx +++ b/frontend/src/pages/reading_log/AddReadingLogPage.tsx @@ -1,24 +1,22 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useEffect, useRef, useState } from "react"; +import {useEffect, useMemo, useState} from "react"; import { useSearchParams, useLocation, useNavigate } from "react-router-dom"; import { apiFetch } from "../../api/readingLog"; +import { apiFetch as materialsApiFetch } from "../../api/materials.ts"; import { CelebrateButton } from "../../components/CelebrateButton"; -import { useAltchHotkeys } from "../../hooks/useAltchHotkeys"; import { GetMaterialCompletionInfoResponse, GetMaterialReadingNowResponse, - ListReadingMaterialsTitlesResponse + ListMaterialsTitlesResponse, } from "../../types.ts"; import {isUuid} from "../../utils/isUuid.ts"; +import {ComboboxInput, ComboboxList, ComboboxRoot} from "../../components/Combobox.tsx"; export function AddReadingLogPage() { const [searchParams] = useSearchParams(); const initialMaterial = searchParams.get("material_id"); - const contentRef = useRef(null); - useAltchHotkeys(contentRef); - const [materialId, setMaterialId] = useState(initialMaterial); const [date, setDate] = useState(""); const [count, setCount] = useState(""); @@ -30,20 +28,20 @@ export function AddReadingLogPage() { const from = location.state?.from || "/materials/reading"; const getMaterialReadingNow = useQuery({ - queryKey: ["material_reading_now"], + queryKey: ["materials", "reading_now"], queryFn: () => apiFetch("/material-reading-now"), }); const completionQ = useQuery({ - queryKey: ["material", materialId, "completion-info"], - queryFn: () => apiFetch(`/${materialId}/completion-info`), + queryKey: ["materials", materialId, "completion-info"], + queryFn: () => materialsApiFetch(`/${materialId}/completion-info`), enabled: !!materialId && isUuid(materialId), }); const readingMaterialsTitlesQ = useQuery({ - queryKey: ["reading_materials_titles"], - queryFn: () => apiFetch("/reading-materials-titles"), + queryKey: ["materials", "reading_titles"], + queryFn: () => materialsApiFetch("/reading-titles"), staleTime: 5 * 60 * 1000, }); @@ -86,6 +84,7 @@ export function AddReadingLogPage() { void qc.invalidateQueries({ queryKey: ["reading_logs", "list"] }); void qc.invalidateQueries({ queryKey: ["materials", "reading"] }); + void qc.invalidateQueries({ queryKey: ["materials", materialId, "completion-info"] }); navigate(from); }, onError: (e: Error) => { @@ -93,6 +92,13 @@ export function AddReadingLogPage() { }, }); + const titles = readingMaterialsTitles?.items ?? {}; + const materialOptions = useMemo(() => { + return Object.keys(titles).sort((a, b) => + (titles[a] ?? "").localeCompare(titles[b] ?? ""), + ); + }, [titles]); + if (getMaterialReadingNow.isLoading || completionQ.isLoading || readingMaterialsTitlesQ.isLoading) { return

Loading…

; } @@ -100,7 +106,7 @@ export function AddReadingLogPage() { if (hasError) { return (

- {(hasError as Error).message || "Не удалось загрузить данные"} + {(hasError as Error).message || "Failed to load data"}

); } @@ -117,27 +123,19 @@ export function AddReadingLogPage() { >
Add reading log - { - setMaterialId(e.target.value); - }} - /> - {/*todo: rewrite with combobox*/} - - {Object.entries(readingMaterialsTitles?.items ?? {}) - .sort((a, b) => a[1].localeCompare(b[1])) - .map(([id, t]) => ( - - ))} - + titles[id] || id} + value={materialId || ""} + onChange={setMaterialId} + > + + +

apiFetch("/materials-titles"), + queryKey: ["materials", "read_titles"], + queryFn: () => materialsApiFetch("/read-titles"), staleTime: 5 * 60 * 1000, }); const logsQ = useQuery({ queryKey: ["logs", { materialId }], queryFn: () => { - const params = materialId ? `?material_id=${materialId}` : ''; - return apiFetch(`/${params}`); + return apiFetch(`/${buildQuery({material_id: materialId || undefined})}`); }, }); const materialsTitles = materialsTitlesQ.data?.items ?? {}; const data = logsQ.data?.items ?? []; + const materialOptions = useMemo(() => { + return Object.keys(materialsTitles).sort((a, b) => + (materialsTitles[a] ?? "").localeCompare(materialsTitles[b] ?? ""), + ); + }, [materialsTitlesQ]); + if (logsQ.isLoading || materialsTitlesQ.isLoading) { - return

Загрузка...

; + return

Loading...

; } if (logsQ.error || materialsTitlesQ.error) { @@ -53,24 +60,24 @@ export function ListReadingLogsPage() { }} > - - {/*TODO: rewrite with combobox*/} - - {Object.entries(materialsTitles) - .sort((a, b) => a[1].localeCompare(b[1])) - .map(([id, title]) => ( - - ))} - + materialsTitles[id] || id} + value={materialId} + onChange={(e) => { + const next = new URLSearchParams(); + next.set("material_id", e); + setSearchParams(next); + }} + > + + + + -
- - - - - -{%- else -%} - {% import 'errors/not_found.html' as not_found %} - {% call not_found.not_found('reading materials') %} {% endcall %} -{% endif %} -{% endblock main %} diff --git a/templates/reading_log/reading_log.html b/templates/reading_log/reading_log.html deleted file mode 100644 index ad1cc8d7..00000000 --- a/templates/reading_log/reading_log.html +++ /dev/null @@ -1,51 +0,0 @@ -{% extends 'index.html' %} - -{% block style %} - -{% endblock style %} - -{% block header %} - {% from 'submenu.html' import submenu %} - {% call submenu(('Reading log', '/reading_log'), ('Add log record', '/reading_log/add-view')) %} - {% endcall %} -{% endblock header %} - -{% block main %} -{% if titles %} -
-
- - - {% for material_id, title in titles | dictsort(by='value') %} - - -
-
-{% endif %} - -{% if log %} - {% for info in log | sort(attribute="date", reverse=True) %} - {% set count = info.count %} - {% if count >= mean_materials_read_pages[info.material_id] %} -
- {% else %} -
- {% endif %} - -

{{ loop.index }} / {{ loop.length }}

-

Date: {{ info.date.strftime(DATE_FORMAT) }}

-

Title: «{{ info.material_title }}»

-

Count: {{ info.count }}

-
- {% endfor %} - - {% from 'arrows.html' import arrows %} - {% call arrows() %} - {% endcall %} -{%- else -%} - {% import 'errors/not_found.html' as not_found %} - {% call not_found.not_found('log records') %} {% endcall %} -{% endif %} -{% endblock main %} diff --git a/tests/test_reading_log.py b/tests/test_reading_log.py index cd526074..0872856e 100644 --- a/tests/test_reading_log.py +++ b/tests/test_reading_log.py @@ -24,35 +24,10 @@ def test_safe_list_get(lst, index, default, value): assert db._safe_list_get(lst, index, default) == value -@pytest.mark.skip -async def test_get_mean_materials_read_pages(): - # TODO: zero material - stat = await db.get_mean_materials_read_pages() - - stmt = sa.select(models.ReadingLog.c.material_id, models.ReadingLog.c.count) - - async with database.session() as ses: - result = (await ses.execute(stmt)).all() - - expected_stat = {} - for material_id, count in result: - count = Decimal(count) - expected_stat[material_id] = [*expected_stat.get(material_id, []), count] - - expected_result = { - material_id: round(statistics.mean(counts), 2) - for material_id, counts in expected_stat.items() - } - - # not zeros - assert all(stat.values()) - assert expected_result == stat - - async def test_get_log_records(): stmt = sa.select(sa.func.count(1)).select_from(models.ReadingLog) - log_records = await db.get_log_records() + log_records = await db.list_log_records() async with database.session() as ses: expected_res_count = await ses.scalar(stmt) diff --git a/tests/test_statistics.py b/tests/test_statistics.py index c7384c92..fa862352 100644 --- a/tests/test_statistics.py +++ b/tests/test_statistics.py @@ -33,7 +33,7 @@ def mean(coll: Sequence[int | float | Decimal]) -> int | float | Decimal: ], ) async def test_calculate_materials_stat(material_id): - records = await db.get_log_records() + records = await db.list_log_records() stats = await st.calculate_materials_stat({material_id}) stat = stats[material_id] @@ -53,21 +53,21 @@ async def test_calculate_materials_stat(material_id): async def test_get_start_date(): - records = await db.get_log_records() + records = await db.list_log_records() start_date = st._calc_started_at(records) assert min(records, key=lambda record: record.date).date == start_date async def test_get_last_date(): - records = await db.get_log_records() + records = await db.list_log_records() last_date = st._calc_finished_at(records) assert max(records, key=lambda record: record.date).date == last_date async def test_get_log_duration(): - records = await db.get_log_records() + records = await db.list_log_records() start_date = st._calc_started_at(records) last_date = st._calc_finished_at(records) duration = st._calc_log_duration(start_date, last_date) @@ -82,13 +82,13 @@ async def test_get_log_duration(): async def test_get_total_read_pages(): total = await st._get_read_pages() - records = await db.get_log_records() + records = await db.list_log_records() assert sum(total.values()) == sum(record.count for record in records) async def test_get_lost_days(): - records = await db.get_log_records() + records = await db.list_log_records() start_date = st._calc_started_at(records) last_date = st._calc_finished_at(records) duration = st._calc_log_duration(start_date, last_date) @@ -111,7 +111,7 @@ async def test_get_lost_days(): async def test_get_means(): # TODO: zero material means = await st.get_means() - records = await db.get_log_records() + records = await db.list_log_records() assert records @@ -137,7 +137,7 @@ async def test_get_means(): async def test_get_median_pages_read_per_day(): - records = await db.get_log_records() + records = await db.list_log_records() median = st._calc_median_pages_read_per_day(records) counts = sorted(record.count for record in records) @@ -150,7 +150,7 @@ async def test_get_median_pages_read_per_day(): async def test_contains(): - records = await db.get_log_records() + records = await db.list_log_records() assert all( [ @@ -171,7 +171,7 @@ async def test_contains(): ) async def test_get_min_record(material_id): min_record = await st._get_min_record(material_id=material_id) - records = await db.get_log_records() + records = await db.list_log_records() material = await materials_db.get_material(material_id=material_id) if material_id: @@ -196,7 +196,7 @@ async def test_get_min_record(material_id): ) async def test_get_max_record(material_id): max_record = await st._get_max_record(material_id=material_id) - records = await db.get_log_records() + records = await db.list_log_records() material = await materials_db.get_material(material_id=material_id) if material_id: @@ -223,7 +223,7 @@ async def test_get_max_record_nof_found(): async def test_would_be_total(): - logs = await db.get_log_records() + logs = await db.list_log_records() started_at = st._calc_started_at(logs) finished_at = st._calc_finished_at(logs) duration = st._calc_log_duration(started_at, finished_at) diff --git a/tests/test_system.py b/tests/test_system.py index 7fc95447..0b539a81 100644 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -24,7 +24,7 @@ async def test_get_graphic_data_clear_reading(material_id, last_days): material_id = UUID(material_id) result = await db._get_graphic_data(material_id=material_id, last_days=last_days) - logs = await logs_db.get_log_records() + logs = await logs_db.list_log_records() material_logs = [log for log in logs if log.material_id == material_id] material_logs = material_logs[:last_days] @@ -51,7 +51,7 @@ async def test_get_graphic_data_unclear_reading(material_id, last_days): material_id = UUID(material_id) result = await db._get_graphic_data(material_id=material_id, last_days=last_days) - logs = await logs_db.get_log_records() + logs = await logs_db.list_log_records() material_logs = [log for log in logs if log.material_id == material_id] material_logs = material_logs[:last_days] diff --git a/tracker/api/v1/materials.py b/tracker/api/v1/materials.py index 9bd4cd8c..5f467d54 100644 --- a/tracker/api/v1/materials.py +++ b/tracker/api/v1/materials.py @@ -73,11 +73,40 @@ async def get_tags(): } -@router.get("/titles", response_model=schemas.ListMaterialTitlesResponse) +@router.get( + "/titles", + response_model=schemas.ListMaterialTitlesResponse, + description="List all materials titles", +) async def list_titles(): titles = await db.get_titles() + return { + "items": titles, + } + + +@router.get( + "/read-titles", + response_model=schemas.ListMaterialTitlesResponse, + description="List all started materials titles", +) +async def list_read_materials_titles(): + items = await db.get_read_material_titles() + return { + "items": items, + } - return {"items": titles} + +@router.get( + "/reading-titles", + response_model=schemas.ListMaterialTitlesResponse, + description="List titles of materials reading now", +) +async def list_reading_materials_titles(): + items = await db.get_reading_material_titles() + return { + "items": items, + } @router.get("/queue/start", response_model=schemas.GetQueueEdgeResponse) @@ -230,6 +259,24 @@ async def repeat_material(material_id: UUID): await db.repeat_material(material_id=material_id) +@router.get( + "/{material_id}/completion-info", + response_model=schemas.GetMaterialCompletionInfoResponse, +) +async def get_material_completion_info(material_id: UUID): + from tracker.reading_log import db as logs_db + + material = await db.get_material_api(material_id=material_id) + reading_logs = await logs_db.list_log_records(material_id=material_id) + + return { + "material_pages": material.pages, + "material_type": material.material_type, + "pages_read": sum(record.count for record in reading_logs), + "read_days": len(reading_logs), + } + + @router.post("/queue/swap-order", status_code=204) async def swap_order(body: schemas.SwapOrderRequest): await db.swap_order(body.material_id, body.index) diff --git a/tracker/api/v1/reading_logs.py b/tracker/api/v1/reading_logs.py index b9f746bb..8c5eb7b0 100644 --- a/tracker/api/v1/reading_logs.py +++ b/tracker/api/v1/reading_logs.py @@ -18,8 +18,14 @@ async def list_reading_logs(material_id: UUID | None = None): @router.post("/", status_code=201, response_model=schemas.CreateReadingLogsResponse) async def create_log_record(log: schemas.CreateReadingLogsRequest): - if not await db.is_record_correct(**log.model_dump()): - raise HTTPException(status_code=400, detail="Invalid record") + try: + await db.check_record_correct( + material_id=log.material_id, + date=log.date, + count=log.count, + ) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) from None log_id = await db.insert_log_record( material_id=log.material_id, @@ -30,35 +36,6 @@ async def create_log_record(log: schemas.CreateReadingLogsRequest): return {"log_id": log_id} -@router.get("/materials-titles", response_model=schemas.ListMaterialsTitles) -async def list_materials_titles(): - items = await db.get_titles() - return { - "items": items, - } - - -@router.get("/reading-materials-titles", response_model=schemas.ListMaterialsTitles) -async def list_reading_materials_titles(): - items = await db.get_reading_material_titles() - return { - "items": items, - } - - -@router.get( - "/{material_id}/completion-info", - response_model=schemas.GetMaterialCompletionInfoResponse, -) -async def get_material_completion_info(material_id: UUID): - from tracker.reading_log.routes import completion_info as get_completion_info - - if not (completion_info := await get_completion_info(material_id)): - raise HTTPException(status_code=404, detail="Material not found") - - return completion_info - - @router.get("/material-reading-now", response_model=schemas.GetMaterialReadingNowResponse) async def get_material_reading_now(): if material_id := await db.get_material_reading_now(): diff --git a/tracker/main.py b/tracker/main.py index 36fbd13d..e6659384 100644 --- a/tracker/main.py +++ b/tracker/main.py @@ -24,7 +24,6 @@ router as notes_html_router, ) from tracker.notes.spa import router as notes_spa_router -from tracker.reading_log.routes import router as reading_log_router from tracker.reading_log.spa import router as reading_logs_spa_router from tracker.system.routes import router as system_router from tracker.system.spa import router as system_spa_router @@ -89,7 +88,6 @@ async def lifespan(_: FastAPI): app.include_router(system_spa_router) else: app.include_router(notes_html_router) -app.include_router(reading_log_router) app.include_router(cards_router) app.include_router(system_router) diff --git a/tracker/materials/db.py b/tracker/materials/db.py index 76c39080..d2b53d34 100644 --- a/tracker/materials/db.py +++ b/tracker/materials/db.py @@ -622,11 +622,11 @@ async def is_completion_request_valid( material_id: UUID, completed_at: datetime.date | None = None, ) -> None: - from tracker.reading_log.db import get_log_records + from tracker.reading_log.db import list_log_records async with asyncio.TaskGroup() as tg: get_status_task = tg.create_task(_get_status(material_id=material_id)) - get_logs_task = tg.create_task(get_log_records(material_id=material_id)) + get_logs_task = tg.create_task(list_log_records(material_id=material_id)) get_material_task = tg.create_task(get_material(material_id=material_id)) completed_at = completed_at or database.utcnow().date() @@ -1103,3 +1103,41 @@ async def parse_youtube(video_id: str, *, http_timeout: int = 5) -> YoutubeVideo parsed_duration = _parse_duration(duration) return YoutubeVideo(title=title, authors=authors, duration=parsed_duration) + + +async def get_read_material_titles() -> dict[UUID, str]: + """Get titles for materials even been read.""" + logger.debug("Getting reading material titles") + + stmt = sa.select(models.Materials.c.material_id, models.Materials.c.title).join( + models.Statuses, + models.Materials.c.material_id == models.Statuses.c.material_id, + ) + + async with database.session() as ses: + titles = { # noqa: C416 + material_id: title for material_id, title in (await ses.execute(stmt)).all() + } + + logger.debug("%s materials titles got", len(titles)) + return titles + + +async def get_reading_material_titles() -> dict[UUID, str]: + """Get titles for materials reading now.""" + logger.debug("Getting reading material titles") + + stmt = ( + sa.select(models.Materials.c.material_id, models.Materials.c.title) + .join( + models.Statuses, + models.Materials.c.material_id == models.Statuses.c.material_id, + ) + .where(models.Statuses.c.completed_at == None) + ) + + async with database.session() as ses: + titles: dict[UUID, str] = dict((await ses.execute(stmt)).all()) # type: ignore[arg-type] + + logger.debug("%s reading materials titles got", len(titles)) + return titles diff --git a/tracker/materials/schemas.py b/tracker/materials/schemas.py index eda110ac..0a0f2103 100644 --- a/tracker/materials/schemas.py +++ b/tracker/materials/schemas.py @@ -184,3 +184,10 @@ class ParseLinkRequest(CustomBaseModel): class ListMaterialTitlesResponse(CustomBaseModel): items: dict[UUID, str] + + +class GetMaterialCompletionInfoResponse(CustomBaseModel): + material_pages: NonNegativeInt + material_type: enums.MaterialTypesEnum + pages_read: NonNegativeInt + read_days: NonNegativeInt diff --git a/tracker/reading_log/db.py b/tracker/reading_log/db.py index b53e77d9..ad51a99b 100644 --- a/tracker/reading_log/db.py +++ b/tracker/reading_log/db.py @@ -2,7 +2,6 @@ import datetime from collections import defaultdict from collections.abc import AsyncGenerator -from decimal import Decimal from typing import cast from uuid import UUID @@ -30,49 +29,6 @@ def _safe_list_get[T](lst: list[T], index: int, default: T | None = None) -> T | return default -async def get_mean_materials_read_pages() -> dict[UUID, Decimal]: - logger.debug("Getting mean reading read pages count of materials") - - stmt = sa.select( - models.ReadingLog.c.material_id, - sa.func.avg(models.ReadingLog.c.count).label("mean"), - ).group_by(models.ReadingLog.c.material_id) - - async with database.session() as ses: - mean = { - material_id: round(mean, 2) - for material_id, mean in (await ses.execute(stmt)).all() - } - - logger.debug("Mean material reading got") - return mean - - -# TODO: deprecated, remove -async def get_log_records(*, material_id: str | UUID | None = None) -> list[LogRecord]: - logger.debug("Getting all log records") - - stmt = sa.select( - models.ReadingLog, - models.Materials.c.title.label("material_title"), - ).join( - models.Materials, - models.ReadingLog.c.material_id == models.Materials.c.material_id, - ) - - if material_id: - stmt = stmt.where(models.Materials.c.material_id == str(material_id)) - - async with database.session() as ses: - records = [ - LogRecord.model_validate(row, from_attributes=True) - for row in (await ses.execute(stmt)).all() - ] - - logger.debug("%s log records got", len(records)) - return records - - async def list_log_records( *, material_id: UUID | None = None, @@ -115,43 +71,6 @@ async def get_log_record(*, log_id: UUID) -> LogRecord: raise database.NotFoundException(msg) -async def get_reading_material_titles() -> dict[UUID, str]: - logger.debug("Getting reading material titles") - - stmt = ( - sa.select(models.Materials.c.material_id, models.Materials.c.title) - .join( - models.Statuses, - models.Materials.c.material_id == models.Statuses.c.material_id, - ) - .where(models.Statuses.c.completed_at == None) - ) - - async with database.session() as ses: - titles: dict[UUID, str] = dict((await ses.execute(stmt)).all()) # type: ignore[arg-type] - - logger.debug("%s reading materials titles got", len(titles)) - return titles - - -async def get_titles() -> dict[UUID, str]: - """Get titles for materials even been read.""" - logger.debug("Getting reading material titles") - - stmt = sa.select(models.Materials.c.material_id, models.Materials.c.title).join( - models.Statuses, - models.Materials.c.material_id == models.Statuses.c.material_id, - ) - - async with database.session() as ses: - titles = { # noqa: C416 - material_id: title for material_id, title in (await ses.execute(stmt)).all() - } - - logger.debug("%s materials titles got", len(titles)) - return titles - - async def get_completion_dates() -> dict[UUID | None, datetime.datetime]: logger.debug("Getting completion dates") @@ -186,7 +105,7 @@ async def data( """ logger.debug("Getting logging data") - if not (log_records := log_records or await get_log_records()): + if not (log_records := log_records or await list_log_records()): return log_records_dict: defaultdict[datetime.date, list[LogRecord]] = defaultdict(list) @@ -307,18 +226,20 @@ async def insert_log_record( return cast("UUID", log_id) -async def is_record_correct( +async def check_record_correct( *, material_id: UUID, date: datetime.date, count: int, -) -> bool: - if date > database.utcnow().date() or count <= 0: - return False +) -> None: + if date > database.utcnow().date(): + raise ValueError("Date could not be in the future") + if count <= 0: + raise ValueError("Count must be greater than 0") async with asyncio.TaskGroup() as tg: reading_materials_task = tg.create_task(materials_db.get_reading_materials()) - log_records_task = tg.create_task(get_log_records(material_id=str(material_id))) + log_records_task = tg.create_task(list_log_records(material_id=material_id)) materials = [ material @@ -326,26 +247,23 @@ async def is_record_correct( if material.material_id == material_id ] if not materials: - logger.warning("No reading material id=%s found", material_id) - return False + raise ValueError(f"No reading material id={material_id} found") material = materials[0] st = material.status if date < st.started_at.date() or (st.completed_at and date > st.completed_at.date()): - logger.warning( - "Date is not inside the range %s not in [%s; %s]", - date, - st.started_at, - st.completed_at, + raise ValueError( + "Date is not inside the range. " + f"{date} not in [{st.started_at}; {st.completed_at}]", ) - return False total_pages_read = sum(record.count for record in log_records_task.result()) if total_pages_read + count > material.material.pages: - logger.warning("There are more pages than the material has: ") - return False - - return True + raise ValueError( + "There are more pages than the material remains: " + f"total pages read {total_pages_read}, " + f"material pages {material.material.pages}", + ) async def records_sum( diff --git a/tracker/reading_log/routes.py b/tracker/reading_log/routes.py deleted file mode 100644 index 736bf136..00000000 --- a/tracker/reading_log/routes.py +++ /dev/null @@ -1,114 +0,0 @@ -import asyncio -from typing import Annotated, Any, cast -from uuid import UUID - -from fastapi import APIRouter, Form, HTTPException, Request -from fastapi.responses import HTMLResponse, RedirectResponse -from fastapi.templating import Jinja2Templates - -from tracker.common import database, settings -from tracker.materials import db as materials_db -from tracker.reading_log import db, schemas - - -router = APIRouter( - prefix="/reading_log", - tags=["reading log"], - default_response_class=HTMLResponse, - deprecated=True, -) -templates = Jinja2Templates(directory="templates") - - -@router.get("/") -async def get_reading_log(request: Request, material_id: str | None = None): - async with asyncio.TaskGroup() as tg: - get_reading_logs = tg.create_task(db.get_log_records(material_id=material_id)) - get_mean_materials_read_pages = tg.create_task(db.get_mean_materials_read_pages()) - get_titles_task = tg.create_task(db.get_titles()) - - context = { - "request": request, - "log": get_reading_logs.result(), - "mean_materials_read_pages": get_mean_materials_read_pages.result(), - "titles": get_titles_task.result(), - "material_id": material_id or "", - "DATE_FORMAT": settings.DATE_FORMAT, - } - return templates.TemplateResponse(request, "reading_log/reading_log.html", context) - - -@router.get("/add-view") -async def add_log_record_view(request: Request, material_id: UUID | None = None): - async with asyncio.TaskGroup() as tg: - get_titles = tg.create_task(db.get_reading_material_titles()) - if material_id: - is_material_reading = tg.create_task( - materials_db.is_reading(material_id=material_id), - ) - get_reading_material_id = tg.create_task(db.get_material_reading_now()) - - log_material_id = material_id - if not (material_id and is_material_reading.result()): - log_material_id = get_reading_material_id.result() - - completion_info_ = await completion_info(log_material_id) - - context: dict[str, Any] = { - "request": request, - "material_id": log_material_id, - "titles": get_titles.result(), - "date": database.utcnow(), - } - - if completion_info_: - completion_info_ = cast("schemas.CompletionInfoSchema", completion_info_) - context["pages_read"] = completion_info_.pages_read - context["material_pages"] = completion_info_.material_pages - - return templates.TemplateResponse(request, "reading_log/add_log_record.html", context) - - -@router.post("/add") -async def add_log_record(record: Annotated[schemas.CreateReadingLogsRequest, Form()]): - if not await db.is_record_correct(**record.model_dump()): - raise HTTPException(status_code=400, detail="Invalid record") - - await db.insert_log_record( - material_id=str(record.material_id), - count=record.count, - date=record.date, - ) - - redirect_url = router.url_path_for(add_log_record_view.__name__) - return RedirectResponse(redirect_url, status_code=302) - - -@router.get("/completion-info", response_model=schemas.CompletionInfoSchema) -async def get_completion_info(material_id: UUID): - if completion_info_ := await completion_info(material_id): - return completion_info_ - - raise HTTPException(status_code=404, detail="Material not found") - - -async def completion_info( - material_id: UUID | None, -) -> schemas.CompletionInfoSchema | None: - if not material_id: - return None - - async with asyncio.TaskGroup() as tg: - material_task = tg.create_task(materials_db.get_material(material_id=material_id)) - reading_logs_task = tg.create_task(db.get_log_records(material_id=material_id)) - - if not (material := material_task.result()): - return None - reading_logs = reading_logs_task.result() - - return schemas.CompletionInfoSchema( - material_pages=material.pages, - material_type=material.material_type, - pages_read=sum(record.count for record in reading_logs), - read_days=len(reading_logs), - ) diff --git a/tracker/reading_log/schemas.py b/tracker/reading_log/schemas.py index d5e4c461..eee5e8d7 100644 --- a/tracker/reading_log/schemas.py +++ b/tracker/reading_log/schemas.py @@ -4,17 +4,9 @@ from pydantic import NonNegativeInt, PositiveInt from tracker.common.schemas import CustomBaseModel -from tracker.models import enums -class CompletionInfoSchema(CustomBaseModel): - material_pages: NonNegativeInt - material_type: enums.MaterialTypesEnum - pages_read: NonNegativeInt - read_days: NonNegativeInt - - -class _GetLogRecordItem(CustomBaseModel): +class _ListLogRecordItem(CustomBaseModel): log_id: UUID material_id: UUID # because of 'without material' notes @@ -23,7 +15,7 @@ class _GetLogRecordItem(CustomBaseModel): class ListReadingLogsResponse(CustomBaseModel): - items: list[_GetLogRecordItem] + items: list[_ListLogRecordItem] class CreateReadingLogsResponse(CustomBaseModel): @@ -37,19 +29,8 @@ class CreateReadingLogsRequest(CustomBaseModel): class GetReadingLogResponse(CustomBaseModel): - reading_log: _GetLogRecordItem - - -class ListMaterialsTitles(CustomBaseModel): - items: dict[UUID, str] + reading_log: _ListLogRecordItem class GetMaterialReadingNowResponse(CustomBaseModel): material_id: UUID - - -class GetMaterialCompletionInfoResponse(CustomBaseModel): - material_pages: NonNegativeInt - material_type: enums.MaterialTypesEnum - pages_read: NonNegativeInt - read_days: NonNegativeInt diff --git a/tracker/reading_log/statistics.py b/tracker/reading_log/statistics.py index aca52c29..73c727d3 100644 --- a/tracker/reading_log/statistics.py +++ b/tracker/reading_log/statistics.py @@ -281,7 +281,7 @@ def _tracker_mean(means: enums.MEANS) -> float: async def get_tracker_statistics() -> TrackerStatistics: async with asyncio.TaskGroup() as tg: - log_records_task = tg.create_task(db.get_log_records()) + log_records_task = tg.create_task(db.list_log_records()) mean_task = tg.create_task(get_means()) read_pages_task = tg.create_task(_get_read_pages()) min_log_record_task = tg.create_task(_get_min_record())