diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 55606cb2..738261fe 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -69,7 +69,7 @@ jobs: - uses: isort/isort-action@master with: requirementsFiles: "requirements.txt requirements.dev.txt" - - uses: psf/black@stable + - uses: psf/black@23.11.0 - name: Comment if linting failed if: failure() uses: thollander/actions-comment-pull-request@v2 diff --git a/calendar_backend/models/__init__.py b/calendar_backend/models/__init__.py index 19f25799..b9c35e30 100644 --- a/calendar_backend/models/__init__.py +++ b/calendar_backend/models/__init__.py @@ -8,6 +8,8 @@ EventsGroups, EventsLecturers, EventsRooms, + EventUser, + EventUserStatus, Group, Lecturer, Room, @@ -27,4 +29,6 @@ "EventsRooms", "ApproveStatuses", "EventsGroups", + "EventUser", + "EventUserStatus", ] diff --git a/calendar_backend/models/db.py b/calendar_backend/models/db.py index 47f4ceac..62f8318c 100644 --- a/calendar_backend/models/db.py +++ b/calendar_backend/models/db.py @@ -14,6 +14,13 @@ from .base import ApproveStatuses, BaseDbModel +class EventUserStatus(str, Enum): + NO_STATUS: str = "no_status" + GOING: str = "going" + NOT_GOING: str = "not_going" + ATTENDED: str = "attended" + + class Credentials(BaseDbModel): """User credentials""" @@ -211,3 +218,14 @@ class CommentEvent(BaseDbModel): foreign_keys="CommentEvent.event_id", primaryjoin="and_(Event.id==CommentEvent.event_id, not_(Event.is_deleted))", ) + + +class EventUser(BaseDbModel): + id: Mapped[int] = mapped_column(Integer, primary_key=True) + event_id: Mapped[int] = mapped_column(Integer, ForeignKey("event.id"), nullable=False) + user_id: Mapped[int] = mapped_column(Integer, nullable=False) + status: Mapped[EventUserStatus] = mapped_column(DbEnum(EventUserStatus, native_enum=False), nullable=False) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow + ) + is_deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) diff --git a/calendar_backend/routes/base.py b/calendar_backend/routes/base.py index 5cdd7cdf..0706363d 100644 --- a/calendar_backend/routes/base.py +++ b/calendar_backend/routes/base.py @@ -20,6 +20,7 @@ from .event.comment import router as event_comment_router from .event.comment_review import router as event_comment_review_router from .event.event import router as event_router +from .event.user_event import router as user_event_router from .group.group import router as group_router from .lecturer.comment import router as lecturer_comment_router from .lecturer.comment_review import router as lecturer_comment_review_router @@ -128,3 +129,4 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) - app.include_router(event_router) app.include_router(event_comment_router) app.include_router(event_comment_review_router) +app.include_router(user_event_router) diff --git a/calendar_backend/routes/event/event.py b/calendar_backend/routes/event/event.py index 181cae1d..1af85121 100644 --- a/calendar_backend/routes/event/event.py +++ b/calendar_backend/routes/event/event.py @@ -1,5 +1,5 @@ import logging -from datetime import date, datetime, timedelta +from datetime import date, timedelta from typing import Literal from auth_lib.fastapi import UnionAuth diff --git a/calendar_backend/routes/event/user_event.py b/calendar_backend/routes/event/user_event.py new file mode 100644 index 00000000..b0b17fc3 --- /dev/null +++ b/calendar_backend/routes/event/user_event.py @@ -0,0 +1,42 @@ +from auth_lib.fastapi import UnionAuth +from fastapi import APIRouter, Depends, Query +from fastapi_sqlalchemy import db + +from calendar_backend.models import Event, EventUser +from calendar_backend.routes.models.visit import VisitResponse + + +router = APIRouter(prefix="/event", tags=["Event: Visit"]) + + +@router.post("/{event_id}/visit", response_model=VisitResponse) +async def set_event_visit_status( + event_id: int, + auth: dict = Depends(UnionAuth()), + visit: str = Query(enum=["no_status", "going", "not_going", "attended"], default="no_status"), +) -> VisitResponse: + """ + Отметить посещение мероприятия для текущего пользователя. + """ + user_id = auth.get('id') + + Event.get(event_id, with_deleted=False, session=db.session) + + existing = ( + EventUser.get_all(session=db.session) + .filter(EventUser.event_id == event_id, EventUser.user_id == user_id) + .first() + ) + + if existing: + result = EventUser.update(existing.id, session=db.session, status=visit) + else: + result = EventUser.create( + session=db.session, + event_id=event_id, + user_id=user_id, + status=visit, + ) + + db.session.commit() + return VisitResponse.model_validate(result) diff --git a/calendar_backend/routes/models/__init__.py b/calendar_backend/routes/models/__init__.py index bd80e638..1f4f587b 100644 --- a/calendar_backend/routes/models/__init__.py +++ b/calendar_backend/routes/models/__init__.py @@ -14,6 +14,7 @@ Photo, ) from .room import GetListRoom, RoomEvents, RoomPatch, RoomPost +from .visit import VisitRequest, VisitResponse __all__ = ( @@ -46,4 +47,6 @@ "RoomEvents", "RoomPatch", "RoomPost", + "VisitRequest", + "VisitResponse", ) diff --git a/calendar_backend/routes/models/visit.py b/calendar_backend/routes/models/visit.py new file mode 100644 index 00000000..6bd8f978 --- /dev/null +++ b/calendar_backend/routes/models/visit.py @@ -0,0 +1,19 @@ +import datetime + +from pydantic import BaseModel + +from calendar_backend.models import EventUserStatus + + +class VisitRequest(BaseModel): + status: EventUserStatus + + +class VisitResponse(BaseModel): + id: int + event_id: int + user_id: int + status: EventUserStatus + updated_at: datetime.datetime + + model_config = {"from_attributes": True} diff --git a/migrations/versions/b060027b11b3_eventuser_building.py b/migrations/versions/b060027b11b3_eventuser_building.py new file mode 100644 index 00000000..f013c1f3 --- /dev/null +++ b/migrations/versions/b060027b11b3_eventuser_building.py @@ -0,0 +1,50 @@ +"""EventUser building + +Revision ID: b060027b11b3 +Revises: 55a049fde8f4 +Create Date: 2026-04-20 17:56:39.185374 + +""" + +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision = 'b060027b11b3' +down_revision = '55a049fde8f4' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + 'event_user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('event_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column( + 'status', + sa.Enum('NO_STATUS', 'GOING', 'NOT_GOING', 'ATTENDED', name='eventuserstatus', native_enum=False), + nullable=False, + ), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('is_deleted', sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint( + ['event_id'], + ['event.id'], + ), + sa.PrimaryKeyConstraint('id'), + ) + op.drop_constraint(op.f('lesson_group_id_fkey'), 'event', type_='foreignkey') + op.drop_column('event', 'group_id') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('event', sa.Column('group_id', sa.INTEGER(), autoincrement=False, nullable=True)) + op.create_foreign_key(op.f('lesson_group_id_fkey'), 'event', 'group', ['group_id'], ['id']) + op.drop_table('event_user') + # ### end Alembic commands ### diff --git a/requirements.dev.txt b/requirements.dev.txt index 15533c0f..b36fa3d1 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -2,6 +2,6 @@ pytest pytest-cov requests pytest-mock -black +black==23.11.0 isort autoflake