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
2 changes: 2 additions & 0 deletions modal_backend/routes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from modal_backend.routes.groups import group
from modal_backend.routes.notes import note
from modal_backend.routes.services import service
from modal_backend.routes.user import user_router
from modal_backend.settings import get_settings

settings = get_settings()
Expand Down Expand Up @@ -37,3 +38,4 @@
app.include_router(note)
app.include_router(service)
app.include_router(group)
app.include_router(user_router)
29 changes: 29 additions & 0 deletions modal_backend/routes/user.py
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.

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.

эта строка должна быть по идее на прошлой, если линтинг сам не переносит строку то надо на одной написать (но там не должен переносить он ничего, 120 вроде символов ограничение)

Если записи в 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="Показ засчитан")
10 changes: 7 additions & 3 deletions modal_backend/utils/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

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.

а в чем смысл замены?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

похоже что случайно форматнулось пока я пытался подгонать нужное форматирование для пуша. Поправлю обратно


return notes

Expand Down Expand Up @@ -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",

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.

это линтинг исправил? если нет, вернуть

message="Service has been successfully deleted",
ru="Сервис успешно удален",
)

@classmethod
Expand Down Expand Up @@ -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",

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.

аналогично

message="Group has been successfully deleted",
ru="Группа успешно удалена",
)

@classmethod
Expand Down
57 changes: 57 additions & 0 deletions modal_backend/utils/user_logic.py
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,
)
53 changes: 53 additions & 0 deletions tests/test_routes/test_user.py
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)

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.

всю работу с бд выносим в фикстуры, там можно yield использовать для передачи, потом удаляем

dbsession.commit()


def test_second_view_increments_shown_count(client, dbsession, notes, authlib_user_data):

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.

плохо когда один и тот же тест разделен на 2, где разница - числовое значение

note = notes[0]

client.post(f"{url}/{note.id}/view", params={"service_id": 1})

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.

почему есть привязка к сервису с айди 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):

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.

вообще надо один бы тест сделать, так как одна ручка, надо через 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):

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.

это все тоже в один тест общий можно сделать

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
Loading