diff --git a/rating_api/exceptions.py b/rating_api/exceptions.py index f575f96..c0b57d4 100644 --- a/rating_api/exceptions.py +++ b/rating_api/exceptions.py @@ -1,4 +1,3 @@ -import datetime from typing import Type @@ -29,13 +28,28 @@ def __init__(self, obj: type, obj_id_or_name: int | str): class TooManyCommentRequests(RatingAPIError): - delay_time: datetime.timedelta + frequency: int + limit: int - def __init__(self, dtime: datetime.timedelta): - self.delay_time = dtime + def __init__(self, frequency: int, limit: int): + self.frequency = frequency + self.limit = limit super().__init__( - f'Too many comment requests. Delay: {dtime}', - f'Слишком много попыток оставить комментарий. Задержка: {dtime}', + f'Too many comment requests. Allowed: {limit} comments per {frequency} months.', + f'Слишком много попыток оставить комментарий. Разрешено: {limit} комментариев за {frequency} месяцев.', + ) + + +class TooManyCommentsToLecturer(RatingAPIError): + frequency: int + limit: int + + def __init__(self, frequency: int, limit: int): + self.frequency = frequency + self.limit = limit + super().__init__( + f"Too many comments to lecturer. Allowed: {limit} comments per {frequency} months.", + f"Превышен лимит комментариев лектору. Разрешено: {limit} комментариев за {frequency} месяцев.", ) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index 1962433..e931696 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, Query from fastapi_sqlalchemy import db -from rating_api.exceptions import ForbiddenAction, ObjectNotFound, TooManyCommentRequests +from rating_api.exceptions import ForbiddenAction, ObjectNotFound, TooManyCommentRequests, TooManyCommentsToLecturer from rating_api.models import Comment, Lecturer, LecturerUserComment, ReviewStatus from rating_api.schemas.base import StatusResponseModel from rating_api.schemas.models import CommentGet, CommentGetAll, CommentPost @@ -27,6 +27,8 @@ async def create_comment(lecturer_id: int, comment_info: CommentPost, user=Depen Для возможности создания комментария с указанием времени создания и изменения необходим скоуп ["rating.comment.import"] """ lecturer = Lecturer.get(session=db.session, id=lecturer_id) + now = datetime.datetime.now(tz=datetime.timezone.utc) + if not lecturer: raise ObjectNotFound(Lecturer, lecturer_id) @@ -35,23 +37,53 @@ async def create_comment(lecturer_id: int, comment_info: CommentPost, user=Depen raise ForbiddenAction(Comment) if not has_create_scope: - user_comments: list[LecturerUserComment] = ( - LecturerUserComment.query(session=db.session).filter(LecturerUserComment.user_id == user.get("id")).all() + # Определяем дату, до которой учитываем комментарии для проверки общего лимита. + date_count = datetime.datetime( + now.year + (now.month - settings.COMMENT_FREQUENCY_IN_MONTH) // 12, + (now.month - settings.COMMENT_FREQUENCY_IN_MONTH) % 12, + 1, + ) + user_comments_count = ( + LecturerUserComment.query(session=db.session) + .filter( + LecturerUserComment.user_id == user.get("id"), + LecturerUserComment.update_ts >= date_count, + ) + .count() + ) + if user_comments_count >= settings.COMMENT_LIMIT: + raise TooManyCommentRequests(settings.COMMENT_FREQUENCY_IN_MONTH, settings.COMMENT_LIMIT) + + # Дата, до которой учитываем комментарии для проверки лимита на комментарии конкретному лектору. + cutoff_date_lecturer = datetime.datetime( + now.year + (now.month - settings.COMMENT_LECTURER_FREQUENCE_IN_MONTH) // 12, + (now.month - settings.COMMENT_LECTURER_FREQUENCE_IN_MONTH) % 12, + 1, + ) + lecturer_comments_count = ( + LecturerUserComment.query(session=db.session) + .filter( + LecturerUserComment.user_id == user.get("id"), + LecturerUserComment.lecturer_id == lecturer_id, + LecturerUserComment.update_ts >= cutoff_date_lecturer, + ) + .count() ) - for user_comment in user_comments: - if datetime.datetime.utcnow() - user_comment.update_ts < datetime.timedelta( - minutes=settings.COMMENT_CREATE_FREQUENCY_IN_MINUTES - ): - raise TooManyCommentRequests( - dtime=user_comment.update_ts - + datetime.timedelta(minutes=settings.COMMENT_CREATE_FREQUENCY_IN_MINUTES) - - datetime.datetime.utcnow() - ) + if lecturer_comments_count >= settings.COMMENT_TO_LECTURER_LIMIT: + raise TooManyCommentsToLecturer( + settings.COMMENT_LECTURER_FREQUENCE_IN_MONTH, settings.COMMENT_TO_LECTURER_LIMIT + ) # Сначала добавляем с user_id, который мы получили при авторизации, # в LecturerUserComment, чтобы нельзя было слишком быстро добавлять комментарии - LecturerUserComment.create(session=db.session, lecturer_id=lecturer_id, user_id=user.get('id')) - + create_ts = datetime.datetime(now.year, now.month, 1) + LecturerUserComment.create( + session=db.session, + lecturer_id=lecturer_id, + user_id=user.get('id'), + create_ts=create_ts, + update_ts=create_ts, + ) # Обрабатываем анонимность комментария, и удаляем этот флаг чтобы добавить запись в БД user_id = None if comment_info.is_anonymous else user.get('id') diff --git a/rating_api/routes/exc_handlers.py b/rating_api/routes/exc_handlers.py index 3dcb940..a0f0d4b 100644 --- a/rating_api/routes/exc_handlers.py +++ b/rating_api/routes/exc_handlers.py @@ -1,7 +1,14 @@ import starlette.requests from starlette.responses import JSONResponse -from rating_api.exceptions import AlreadyExists, ForbiddenAction, ObjectNotFound, TooManyCommentRequests, WrongMark +from rating_api.exceptions import ( + AlreadyExists, + ForbiddenAction, + ObjectNotFound, + TooManyCommentRequests, + TooManyCommentsToLecturer, + WrongMark, +) from rating_api.schemas.base import StatusResponseModel from .base import app @@ -28,6 +35,13 @@ async def too_many_comment_handler(req: starlette.requests.Request, exc: Already ) +@app.exception_handler(TooManyCommentsToLecturer) +async def too_many_comment_handler(req: starlette.requests.Request, exc: AlreadyExists): + return JSONResponse( + content=StatusResponseModel(status="Error", message=exc.eng, ru=exc.ru).model_dump(), status_code=429 + ) + + @app.exception_handler(ForbiddenAction) async def forbidden_action_handler(req: starlette.requests.Request, exc: AlreadyExists): return JSONResponse( diff --git a/rating_api/settings.py b/rating_api/settings.py index ed09146..1f7612c 100644 --- a/rating_api/settings.py +++ b/rating_api/settings.py @@ -10,7 +10,10 @@ class Settings(BaseSettings): DB_DSN: PostgresDsn = 'postgresql://postgres@localhost:5432/postgres' ROOT_PATH: str = '/' + os.getenv("APP_NAME", "") - COMMENT_CREATE_FREQUENCY_IN_MINUTES: int = 1 + COMMENT_FREQUENCY_IN_MONTH: int = 10 + COMMENT_LECTURER_FREQUENCE_IN_MONTH: int = 6 + COMMENT_LIMIT: int = 20 + COMMENT_TO_LECTURER_LIMIT: int = 5 CORS_ALLOW_ORIGINS: list[str] = ['*'] CORS_ALLOW_CREDENTIALS: bool = True CORS_ALLOW_METHODS: list[str] = ['*']