-
Notifications
You must be signed in to change notification settings - Fork 0
Пользовательская ручка view #38 #40
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| from auth_lib.fastapi import UnionAuth | ||
| from fastapi import APIRouter, Depends | ||
| from fastapi_sqlalchemy import db | ||
|
|
||
| from modal_backend.schemas.base import StatusResponseModel | ||
| from modal_backend.settings import Settings, get_settings | ||
| from modal_backend.utils.user_logic import UserService | ||
|
|
||
| settings: Settings = get_settings() | ||
| user_router = APIRouter(prefix="/user", tags=["User"]) | ||
|
|
||
|
|
||
| @user_router.post("/{id}/view", response_model=StatusResponseModel) | ||
| async def mark_note_view( | ||
| id: int, | ||
| service_id: int, | ||
| user=Depends(UnionAuth()), | ||
| ) -> StatusResponseModel: | ||
| """ | ||
| Отмечает, что модалка реально была показана пользователю. | ||
|
|
||
| Увеличивает shown_count в таблице note_view и запоминает номер захода | ||
| (last_visit_number), от которого потом считается frequency. | ||
| Если записи в note_view ещё нет — создаёт. | ||
|
|
||
| Повторный вызов не ошибка | ||
| """ | ||
| await UserService.mark_view(db, note_id=id, user_id=user.get("id"), service_id=service_id) | ||
| return StatusResponseModel(status="success", message="View recorded", ru="Показ засчитан") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,7 +39,7 @@ async def get_notes_by_filters( | |
| notes = notes_query.limit(limit).offset(offset).all() | ||
|
|
||
| if not notes: | ||
| raise ObjectNotFound(Note, 'all') | ||
| raise ObjectNotFound(Note, "all") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. а в чем смысл замены?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. похоже что случайно форматнулось пока я пытался подгонать нужное форматирование для пуша. Поправлю обратно |
||
|
|
||
| return notes | ||
|
|
||
|
|
@@ -104,7 +104,9 @@ async def delete_service(cls, db: Session, id: int): | |
| Service.get(session=db.session, id=id) | ||
| Service.delete(session=db.session, id=id) | ||
| return StatusResponseModel( | ||
| status="Success", message="Service has been successfully deleted", ru="Сервис успешно удален" | ||
| status="Success", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. это линтинг исправил? если нет, вернуть |
||
| message="Service has been successfully deleted", | ||
| ru="Сервис успешно удален", | ||
| ) | ||
|
|
||
| @classmethod | ||
|
|
@@ -132,7 +134,9 @@ async def delete_group(cls, db: Session, id: int): | |
| Group.get(session=db.session, id=id) | ||
| Group.delete(session=db.session, id=id) | ||
| return StatusResponseModel( | ||
| status="Success", message="Group has been successfully deleted", ru="Группа успешно удалена" | ||
| status="Success", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. аналогично |
||
| message="Group has been successfully deleted", | ||
| ru="Группа успешно удалена", | ||
| ) | ||
|
|
||
| @classmethod | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| from datetime import datetime, timezone | ||
|
|
||
| from requests import Session | ||
|
|
||
| from modal_backend.exceptions import ForbiddenAction, ObjectNotFound | ||
| from modal_backend.models.db import ModalStatus, Note, NoteView, Service, UserVisit | ||
|
|
||
|
|
||
| class UserService: | ||
| """ | ||
| Пользовательский сервис для учёта показов модалок | ||
| """ | ||
|
|
||
| @classmethod | ||
| async def mark_view(cls, db: Session, note_id: int, user_id: int, service_id: int): | ||
| note = Note.get(session=db.session, id=note_id) | ||
| if note.status != ModalStatus.ACTIVE: | ||
| raise ForbiddenAction(Note) | ||
|
|
||
| now = datetime.now(timezone.utc).replace(tzinfo=None) | ||
| if note.is_always == False and now >= note.end_ts: | ||
| raise ForbiddenAction(Note) | ||
|
|
||
| service = Service.query(session=db.session).filter(Service.service_id == service_id).one_or_none() | ||
| if service is None: | ||
| raise ObjectNotFound(Service, service_id) | ||
|
|
||
| user_visit = ( | ||
| UserVisit.query(session=db.session) | ||
| .filter(UserVisit.user_id == user_id, UserVisit.service_id == service_id) | ||
| .one_or_none() | ||
| ) | ||
| visit_count = user_visit.visit_count if user_visit else 0 | ||
|
|
||
| note_view = ( | ||
| NoteView.query(session=db.session) | ||
| .filter(NoteView.note_id == note_id, NoteView.user_id == user_id) | ||
| .one_or_none() | ||
| ) | ||
| if note_view is None: | ||
| NoteView.create( | ||
| session=db.session, | ||
| note_id=note_id, | ||
| user_id=user_id, | ||
| shown_count=1, | ||
| last_visit_number=1, | ||
| first_shown_at=now, | ||
| last_shown_at=now, | ||
| ) | ||
| else: | ||
| NoteView.update( | ||
| note_view.id, | ||
| session=db.session, | ||
| shown_count=note_view.shown_count + 1, | ||
| last_visit_number=visit_count, | ||
| last_shown_at=now, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| from starlette import status | ||
|
|
||
| from modal_backend.models.db import NoteView | ||
|
|
||
| url = "/user" | ||
|
|
||
|
|
||
| def test_first_view_creates_note_view(client, dbsession, notes, authlib_user_data): | ||
| note = notes[0] | ||
|
|
||
| response = client.post(f"{url}/{note.id}/view", params={"service_id": 1}) | ||
| assert response.status_code == status.HTTP_200_OK | ||
|
|
||
| view = ( | ||
| dbsession.query(NoteView) | ||
| .filter(NoteView.note_id == note.id, NoteView.user_id == authlib_user_data["id"]) | ||
| .one_or_none() | ||
| ) | ||
| assert view is not None | ||
| assert view.shown_count == 1 | ||
|
|
||
| dbsession.delete(view) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. всю работу с бд выносим в фикстуры, там можно yield использовать для передачи, потом удаляем |
||
| dbsession.commit() | ||
|
|
||
|
|
||
| def test_second_view_increments_shown_count(client, dbsession, notes, authlib_user_data): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. плохо когда один и тот же тест разделен на 2, где разница - числовое значение |
||
| note = notes[0] | ||
|
|
||
| client.post(f"{url}/{note.id}/view", params={"service_id": 1}) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. почему есть привязка к сервису с айди 1? |
||
| response = client.post(f"{url}/{note.id}/view", params={"service_id": 1}) | ||
| assert response.status_code == status.HTTP_200_OK | ||
|
|
||
| view = ( | ||
| dbsession.query(NoteView) | ||
| .filter(NoteView.note_id == note.id, NoteView.user_id == authlib_user_data["id"]) | ||
| .one_or_none() | ||
| ) | ||
| assert view.shown_count == 2 | ||
|
|
||
| dbsession.delete(view) | ||
| dbsession.commit() | ||
|
|
||
|
|
||
| def test_nonexistent_note_returns_404(client): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. вообще надо один бы тест сделать, так как одна ручка, надо через mark.parametrize прописать тест-кейсы и на разные статусы ответов разная логика |
||
| response = client.post(f"{url}/999999/view", params={"service_id": 1}) | ||
| assert response.status_code == status.HTTP_404_NOT_FOUND | ||
|
|
||
|
|
||
| def test_archived_note_returns_403(client, notes): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. это все тоже в один тест общий можно сделать |
||
| archived_note = notes[3] | ||
|
|
||
| response = client.post(f"{url}/{archived_note.id}/view", params={"service_id": 1}) | ||
| assert response.status_code == status.HTTP_403_FORBIDDEN | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
эта строка должна быть по идее на прошлой, если линтинг сам не переносит строку то надо на одной написать (но там не должен переносить он ничего, 120 вроде символов ограничение)