diff --git a/CHANGELOG.md b/CHANGELOG.md index 99a89d78..2db60b2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +#### [0.4.12] - 2025-02-12 +- `delete_reaction` detects comments and uses `deleted_at` update + #### [0.4.11] - 2025-02-12 - `create_draft` resolver requires draft_id fixed - `create_draft` resolver defaults body and title fields to empty string diff --git a/requirements.dev.txt b/requirements.dev.txt new file mode 100644 index 00000000..fe95b9fb --- /dev/null +++ b/requirements.dev.txt @@ -0,0 +1,6 @@ +fakeredis +pytest +pytest-asyncio +pytest-cov +mypy +ruff diff --git a/requirements.txt b/requirements.txt index 56b09175..daa6dfb9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,11 +4,8 @@ authlib passlib google-analytics-data -dogpile-cache -opensearch-py colorlog psycopg2-binary -dogpile-cache httpx redis[hiredis] sentry-sdk[starlette,sqlalchemy] @@ -17,10 +14,4 @@ gql ariadne granian -pydantic -fakeredis -pytest -pytest-asyncio -pytest-cov -mypy -ruff +pydantic \ No newline at end of file diff --git a/resolvers/draft.py b/resolvers/draft.py index 50c8b758..4424ff3e 100644 --- a/resolvers/draft.py +++ b/resolvers/draft.py @@ -1,5 +1,5 @@ -from operator import or_ import time +from operator import or_ from sqlalchemy.sql import and_ @@ -56,9 +56,11 @@ async def load_drafts(_, info): return {"error": "User ID and author ID are required"} with local_session() as session: - drafts = session.query(Draft).filter(or_( - Draft.authors.any(Author.id == author_id), - Draft.created_by == author_id)).all() + drafts = ( + session.query(Draft) + .filter(or_(Draft.authors.any(Author.id == author_id), Draft.created_by == author_id)) + .all() + ) return {"drafts": drafts} @@ -99,7 +101,7 @@ async def create_draft(_, info, draft_input): # Проверяем обязательные поля if "body" not in draft_input or not draft_input["body"]: draft_input["body"] = "" # Пустая строка вместо NULL - + if "title" not in draft_input or not draft_input["title"]: draft_input["title"] = "" # Пустая строка вместо NULL @@ -123,23 +125,29 @@ async def create_draft(_, info, draft_input): @mutation.field("update_draft") @login_required -async def update_draft(_, info, draft_input): +async def update_draft(_, info, draft_id: int, draft_input): + """Обновляет черновик публикации. + + Args: + draft_id: ID черновика для обновления + draft_input: Данные для обновления черновика + + Returns: + dict: Обновленный черновик или сообщение об ошибке + """ user_id = info.context.get("user_id") author_dict = info.context.get("author", {}) author_id = author_dict.get("id") - draft_id = draft_input.get("id") - if not draft_id: - return {"error": "Draft ID is required"} + if not user_id or not author_id: return {"error": "Author ID are required"} with local_session() as session: draft = session.query(Draft).filter(Draft.id == draft_id).first() - del draft_input["id"] - Draft.update(draft, {**draft_input}) if not draft: return {"error": "Draft not found"} + Draft.update(draft, draft_input) draft.updated_at = int(time.time()) session.commit() return {"draft": draft} diff --git a/resolvers/reaction.py b/resolvers/reaction.py index 3f448b96..f4334065 100644 --- a/resolvers/reaction.py +++ b/resolvers/reaction.py @@ -133,21 +133,18 @@ def check_to_feature(session, approver_id, reaction) -> bool: return False -def check_to_unfeature(session, rejecter_id, reaction) -> bool: +def check_to_unfeature(session, reaction) -> bool: """ Unfeature a shout if 20% of reactions are negative. :param session: Database session. - :param rejecter_id: Rejecter author ID. :param reaction: Reaction object. :return: True if shout should be unfeatured, else False. """ if not reaction.reply_to and is_negative(reaction.kind): total_reactions = ( session.query(Reaction) - .filter( - Reaction.shout == reaction.shout, Reaction.kind.in_(RATING_REACTIONS), Reaction.deleted_at.is_(None) - ) + .filter(Reaction.shout == reaction.shout, Reaction.reply_to.is_(None), Reaction.kind.in_(RATING_REACTIONS)) .count() ) @@ -217,7 +214,7 @@ async def _create_reaction(session, shout_id: int, is_author: bool, author_id: i # Handle rating if r.kind in RATING_REACTIONS: - if check_to_unfeature(session, author_id, r): + if check_to_unfeature(session, r): set_unfeatured(session, shout_id) elif check_to_feature(session, author_id, r): await set_featured(session, shout_id) @@ -354,7 +351,7 @@ async def update_reaction(_, info, reaction): result = session.execute(reaction_query).unique().first() if result: - r, author, shout, commented_stat, rating_stat = result + r, author, _shout, commented_stat, rating_stat = result if not r or not author: return {"error": "Invalid reaction ID or unauthorized"} @@ -406,15 +403,24 @@ async def delete_reaction(_, info, reaction_id: int): if r.created_by != author_id and "editor" not in roles: return {"error": "Access denied"} - logger.debug(f"{user_id} user removing his #{reaction_id} reaction") - reaction_dict = r.dict() - session.delete(r) - session.commit() - - # Update author stat if r.kind == ReactionKind.COMMENT.value: + r.deleted_at = int(time.time()) update_author_stat(author.id) + session.add(r) + session.commit() + elif r.kind == ReactionKind.PROPOSE.value: + r.deleted_at = int(time.time()) + session.add(r) + session.commit() + # TODO: add more reaction types here + else: + logger.debug(f"{user_id} user removing his #{reaction_id} reaction") + session.delete(r) + session.commit() + if check_to_unfeature(session, r): + set_unfeatured(session, r.shout) + reaction_dict = r.dict() await notify_reaction(reaction_dict, "delete") return {"error": None, "reaction": reaction_dict}