diff --git a/alembic/versions/20d3addee5ba_notification_models.py b/alembic/versions/20d3addee5ba_notification_models.py new file mode 100644 index 0000000..90d30e3 --- /dev/null +++ b/alembic/versions/20d3addee5ba_notification_models.py @@ -0,0 +1,44 @@ +"""notification_models + +Revision ID: 20d3addee5ba +Revises: 981474c39706 +Create Date: 2024-07-30 20:18:41.017469 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '20d3addee5ba' +down_revision = '981474c39706' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('notification', schema=None) as batch_op: + batch_op.add_column(sa.Column('message', sa.Text(), nullable=False)) + batch_op.drop_column('description') + + with op.batch_alter_table('notification_user_association', schema=None) as batch_op: + batch_op.add_column(sa.Column('viewed', sa.Boolean(), nullable=True)) + batch_op.add_column(sa.Column('date', sa.DateTime(timezone=True), nullable=True)) + batch_op.drop_constraint('constraint_notification_user', type_='unique') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('notification_user_association', schema=None) as batch_op: + batch_op.create_unique_constraint('constraint_notification_user', ['notification_id', 'user_id']) + batch_op.drop_column('date') + batch_op.drop_column('viewed') + + with op.batch_alter_table('notification', schema=None) as batch_op: + batch_op.add_column(sa.Column('description', sa.TEXT(), nullable=True)) + batch_op.drop_column('message') + + # ### end Alembic commands ### diff --git a/app/api/endpoints/notification.py b/app/api/endpoints/notification.py index 0639bef..e6b7a94 100644 --- a/app/api/endpoints/notification.py +++ b/app/api/endpoints/notification.py @@ -1,28 +1,156 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Query, Response from sqlalchemy.ext.asyncio import AsyncSession +from starlette import status +from app.api.validators import check_obj_duplicate, check_obj_exists from app.core.db import get_async_session from app.core.user import current_superuser, current_user -from app.crud import notification_crud -from app.schemas.notification import NotificationCreate, NotificationRead +from app.crud import notification_crud, user_crud +from app.models import User +from app.schemas.notification import ( + AddNotificationForUser, NotificationCreate, NotificationRead, + ReadNotificationForUser, +) +from app.services.utils import ( + Pagination, add_response_headers, get_pagination_params, paginated, +) router = APIRouter() -@router.post('/', dependencies=[Depends(current_superuser)]) +@router.post( + '/', + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(current_superuser)], +) async def create_notification( notification: NotificationCreate, - session: AsyncSession = Depends(get_async_session) + session: AsyncSession = Depends(get_async_session) ): - return await notification_crud.create(notification, session) + """Создаст уведомление.""" + obj = await notification_crud.get_by_attr( + attr_name='name', + attr_value=notification.name, + session=session, + ) + await check_obj_duplicate(obj=obj) + await notification_crud.create(obj_in=notification, session=session) @router.get( '/', response_model=list[NotificationRead], - dependencies=[Depends(current_user)] + dependencies=[Depends(current_superuser)], ) async def get_all_notifications( - session: AsyncSession = Depends(get_async_session) + session: AsyncSession = Depends(get_async_session), +): + """Вернет список всех уведомлений.""" + return await notification_crud.get_multi(session=session) + + +@router.delete( + '/{notification_id}', + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(current_superuser)], +) +async def delete_notification( + notification_id: int, + session: AsyncSession = Depends(get_async_session) +): + """Удалит уведомление.""" + obj = await notification_crud.get_by_attr( + attr_name='id', + attr_value=notification_id, + session=session, + ) + await check_obj_exists(obj=obj) + await notification_crud.remove( + db_obj=obj, + session=session, + ) + + +@router.post( + '/add-for-user', + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(current_superuser)], +) +async def add_notification_for_user( + notification: AddNotificationForUser, + session: AsyncSession = Depends(get_async_session) +): + """Создаст связь пользователя и уведомления.""" + db_user = await user_crud.get_by_attr( + attr_name='id', + attr_value=notification.user_id, + session=session, + ) + await check_obj_exists(db_user) + db_notification = await notification_crud.get_by_attr( + attr_name='id', + attr_value=notification.notification_id, + session=session, + ) + await check_obj_exists(db_notification) + + await notification_crud.add_notification_for_user( + obj_in=notification, + session=session, + ) + + +@router.get( + '/get-for-user', + response_model=list[ReadNotificationForUser], + response_model_exclude_none=True, + dependencies=[Depends(current_user)], +) +async def get_notifications_for_user( + response: Response, + pagination: Pagination = Depends(get_pagination_params), + user: User = Depends(current_user), + viewed: bool = Query(default=False, alias="viewed"), + bell: bool = Query(default=False, alias="bell"), + session: AsyncSession = Depends(get_async_session), +): + """ + Вернет список уведомлений для пользователя. + + Параметры: + - **viewed**: (bool) Если True, вернет прочитанные уведомления. + По умолчанию False. + - **bell**: (bool) Если True, вернет непрочитанные уведомления без поля + message. Дополнительно указывать параметр viewed не требуется. + По умолчанию False. + """ + notifications = await notification_crud.get_user_notifications( + user_id=user.id, + viewed=viewed, + bell=bell, + session=session, + ) + add_response_headers(response, notifications, pagination) + return paginated(notifications, pagination) + + +@router.patch( + '/mark-as-viewed/{user_notification_id}', + dependencies=[Depends(current_user)], +) +async def mark_as_viewed( + user_notification_id: int, + user: User = Depends(current_user), + session: AsyncSession = Depends(get_async_session) ): - return await notification_crud.get_multi(session) + """Пометит уведомление как прочитанное.""" + obj = await notification_crud.get_user_notification_by_id( + obj_id=user_notification_id, + user_id=user.id, + session=session, + ) + await check_obj_exists(obj=obj) + await notification_crud.mark_user_notification_as_viewed( + obj_id=user_notification_id, + session=session, + ) diff --git a/app/api/routers.py b/app/api/routers.py index 2f24dcc..b66e6a4 100644 --- a/app/api/routers.py +++ b/app/api/routers.py @@ -2,7 +2,8 @@ from app.api.endpoints import ( achievement_router, course_router, examination_router, group_router, - locale_router, profile_router, tariff_router, task_router, user_router, + locale_router, notification_router, profile_router, tariff_router, + task_router, user_router, ) main_router = APIRouter() @@ -24,3 +25,6 @@ main_router.include_router( locale_router, prefix='/locales', tags=['Locales'] ) +main_router.include_router( + notification_router, prefix='/notifications', tags=['Notifications'] +) diff --git a/app/crud/notification.py b/app/crud/notification.py index f80600e..4de7273 100644 --- a/app/crud/notification.py +++ b/app/crud/notification.py @@ -1,9 +1,78 @@ +from sqlalchemy import and_, select, update +from sqlalchemy.ext.asyncio import AsyncSession + from app.crud.base import CRUDBase from app.models import Notification +from app.models.notification import notification_user_association class CRUDNotification(CRUDBase): - pass + async def add_notification_for_user( + self, + obj_in, + session: AsyncSession, + ): + stmt = notification_user_association.insert().values( + user_id=obj_in.user_id, + notification_id=obj_in.notification_id, + ) + await session.execute(stmt) + await session.commit() + + async def get_user_notifications( + self, + user_id: int, + viewed: bool, + bell: bool, + session: AsyncSession, + ): + stmt = select( + self.model.name, + self.model.message if not bell else None, + notification_user_association.c.id, + notification_user_association.c.date, + ).join( + notification_user_association, + ).where(and_( + notification_user_association.c.user_id == user_id, + notification_user_association.c.viewed == ( + viewed if not bell else False + ), + )) + db_objs = await session.execute(stmt) + return db_objs.mappings().all() + + async def get_user_notification_by_id( + self, + obj_id: int, + user_id: int, + session: AsyncSession, + ): + stmt = select( + notification_user_association, + ).where(and_( + notification_user_association.c.id == obj_id, + notification_user_association.c.user_id == user_id, + )) + db_obj = await session.execute(stmt) + return db_obj.mappings().all() + + async def mark_user_notification_as_viewed(self, obj_id, session): + stmt = update( + notification_user_association, + ).where( + notification_user_association.c.id == obj_id, + ).values(viewed=True) + await session.execute(stmt) + await session.commit() + + async def remove(self, db_obj: Notification, session: AsyncSession): + stmt = notification_user_association.delete().where( + notification_user_association.c.notification_id == db_obj.id + ) + await session.execute(stmt) + await session.delete(db_obj) + await session.commit() notification_crud = CRUDNotification(Notification) diff --git a/app/models/notification.py b/app/models/notification.py index 443806f..d44e9a4 100644 --- a/app/models/notification.py +++ b/app/models/notification.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING from sqlalchemy import ( - Column, ForeignKey, Integer, String, Table, Text, UniqueConstraint, + Boolean, Column, DateTime, ForeignKey, Integer, String, Table, Text, func, ) from sqlalchemy.orm import Mapped, relationship @@ -13,26 +13,27 @@ if TYPE_CHECKING: from app.models import User - notification_user_association = Table( 'notification_user_association', Base.metadata, Column('id', Integer, primary_key=True), Column('notification_id', ForeignKey('notification.id')), Column('user_id', ForeignKey('user.id')), - UniqueConstraint( - 'notification_id', 'user_id', name='constraint_notification_user' - ), + Column('viewed', Boolean, default=False), + Column('date', DateTime, default=func.now()), ) class Notification(Base): name: str = Column( - String(length=settings.max_length_string), unique=True, nullable=False + String(length=settings.max_length_string), + unique=True, + nullable=False, ) - description: str = Column(Text) + message: str = Column(Text, nullable=False) users: Mapped[list[User]] = relationship( - secondary=notification_user_association, back_populates='notifications' + secondary=notification_user_association, + back_populates='notifications', ) def __repr__(self): diff --git a/app/schemas/notification.py b/app/schemas/notification.py index 8ead90f..120caf1 100644 --- a/app/schemas/notification.py +++ b/app/schemas/notification.py @@ -1,17 +1,29 @@ -from typing import Optional +from datetime import datetime from pydantic import BaseModel class NotificationCreate(BaseModel): name: str - description: Optional[str] + message: str class NotificationRead(BaseModel): id: int - name: Optional[str] - description: Optional[str] + name: str + message: str + + +class AddNotificationForUser(BaseModel): + user_id: int + notification_id: int + + +class ReadNotificationForUser(BaseModel): + id: int + name: str + message: str = None + date: datetime class Config: from_attributes = True