Skip to content
Merged
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)
11 changes: 3 additions & 8 deletions modal_backend/routes/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@


@service.get("", response_model=list[ServiceGet])
async def get_services(
user=Depends(UnionAuth()),
) -> list[ServiceGet]:
async def get_services(user=Depends(UnionAuth())) -> list[ServiceGet]:
"""
Получить список всех сервисов.
"""
Expand All @@ -25,8 +23,7 @@ async def get_services(

@service.post("", response_model=ServiceGet)
async def create_service(
service_info: ServicePost,
user=Depends(UnionAuth(scopes=["modal.service.create"])),
service_info: ServicePost, user=Depends(UnionAuth(scopes=["modal.service.create"]))
) -> ServiceGet:
"""
Создает новый сервис.
Expand All @@ -39,9 +36,7 @@ async def create_service(

@service.patch("/{id}", response_model=ServiceGet)
async def update_service(
id: int,
service_info: ServicePost,
user=Depends(UnionAuth(scopes=["modal.service.update"])),
id: int, service_info: ServicePost, user=Depends(UnionAuth(scopes=["modal.service.update"]))
) -> ServiceGet:
"""
Обновляет сервис по `id`.
Expand Down
25 changes: 25 additions & 0 deletions modal_backend/routes/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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.
Comment thread
Aiz0r marked this conversation as resolved.
Если записи в 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")
Comment thread
Aiz0r marked this conversation as resolved.

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",
Comment thread
Aiz0r marked this conversation as resolved.
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",
Comment thread
Aiz0r marked this conversation as resolved.
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:
NoteView.update(
note_view.id,
session=db.session,
shown_count=note_view.shown_count + 1,
last_visit_number=visit_count,
last_shown_at=now,
)
else:
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,
)
51 changes: 51 additions & 0 deletions tests/test_routes/test_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import pytest
from starlette import status

from modal_backend.models.db import NoteView

url = "/user"


@pytest.fixture()
def note_view_cleanup(dbsession, authlib_user_data):
yield
dbsession.query(NoteView).filter(NoteView.user_id == authlib_user_data["id"]).delete()
dbsession.commit()


@pytest.mark.parametrize(
"note_index, view_count, expected_status, expected_shown_count",
[
pytest.param(0, 1, status.HTTP_200_OK, 1, id="first_view_creates_note_view"),
pytest.param(0, 2, status.HTTP_200_OK, 2, id="second_view_increments_shown_count"),
pytest.param(None, 1, status.HTTP_404_NOT_FOUND, None, id="nonexistent_note_returns_404"),
pytest.param(3, 1, status.HTTP_403_FORBIDDEN, None, id="archived_note_returns_403"),
],
)
def test_mark_note_view(
client,
dbsession,
notes,
services,
authlib_user_data,
note_view_cleanup,
note_index,
view_count,
expected_status,
expected_shown_count,
):
note_id = notes[note_index].id if note_index is not None else 999999
service_id = services[0].service_id

for _ in range(view_count):
response = client.post(f"{url}/{note_id}/view", params={"service_id": service_id})
assert response.status_code == expected_status

if expected_shown_count is not None:
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 == expected_shown_count
Loading