-
Notifications
You must be signed in to change notification settings - Fork 3
Лайки к комментариям #95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| """like_db | ||
|
|
||
| Revision ID: 7e40ff9486ce | ||
| Revises: 5cf69f1026d9 | ||
| Create Date: 2025-02-14 08:24:17.387577 | ||
|
|
||
| """ | ||
|
|
||
| import sqlalchemy as sa | ||
| from alembic import op | ||
|
|
||
|
|
||
| # revision identifiers, used by Alembic. | ||
| revision = '7e40ff9486ce' | ||
| down_revision = '5cf69f1026d9' | ||
| branch_labels = None | ||
| depends_on = None | ||
|
|
||
|
|
||
| def upgrade(): | ||
| # ### commands auto generated by Alembic - please adjust! ### | ||
| op.create_table( | ||
| 'comment_like', | ||
| sa.Column('id', sa.Integer(), nullable=False), | ||
| sa.Column('user_id', sa.Integer(), nullable=False), | ||
| sa.Column('comment_uuid', sa.UUID(), nullable=False), | ||
| sa.Column('create_ts', sa.DateTime(), nullable=False), | ||
| sa.Column('is_deleted', sa.Boolean(), nullable=False), | ||
| sa.ForeignKeyConstraint(['comment_uuid'], ['comment.uuid']), | ||
| sa.PrimaryKeyConstraint('id'), | ||
| ) | ||
| # ### end Alembic commands ### | ||
|
|
||
|
|
||
| def downgrade(): | ||
| # ### commands auto generated by Alembic - please adjust! ### | ||
| op.drop_table('likes') | ||
| # ### end Alembic commands ### |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,7 @@ | |
| from fastapi_sqlalchemy import db | ||
| from sqlalchemy import UUID, Boolean, DateTime | ||
| from sqlalchemy import Enum as DbEnum | ||
| from sqlalchemy import ForeignKey, Integer, String, UnaryExpression, and_, func, nulls_last, or_, true | ||
| from sqlalchemy import ForeignKey, Integer, String, UnaryExpression, and_, func, nulls_last, or_, select, true | ||
| from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property | ||
| from sqlalchemy.orm import Mapped, mapped_column, relationship | ||
| from sqlalchemy.orm.attributes import InstrumentedAttribute | ||
|
|
@@ -130,3 +130,11 @@ class LecturerUserComment(BaseDbModel): | |
| create_ts: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow, nullable=False) | ||
| update_ts: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow, nullable=False) | ||
| is_deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) | ||
|
|
||
|
|
||
| class CommentLike(BaseDbModel): | ||
| id: Mapped[int] = mapped_column(Integer, primary_key=True) | ||
| user_id: Mapped[int] = mapped_column(Integer, nullable=False) | ||
| comment_uuid: Mapped[uuid.UUID] = mapped_column(UUID, nullable=False) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. а мб отношение полноценное сделаем? |
||
| create_ts: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow, nullable=False) | ||
| is_deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| import datetime | ||
| import re | ||
| from typing import Literal, Union | ||
|
|
@@ -7,6 +7,9 @@ | |
| from auth_lib.fastapi import UnionAuth | ||
| from fastapi import APIRouter, Depends, Query | ||
| from fastapi_sqlalchemy import db | ||
| from sqlalchemy import func, select | ||
| from sqlalchemy.orm import aliased | ||
|
|
||
|
|
||
| from rating_api.exceptions import ( | ||
| CommentTooLong, | ||
|
|
@@ -17,7 +20,7 @@ | |
| TooManyCommentsToLecturer, | ||
| UpdateError, | ||
| ) | ||
| from rating_api.models import Comment, Lecturer, LecturerUserComment, ReviewStatus | ||
| from rating_api.models import Comment, CommentLike, Lecturer, LecturerUserComment, ReviewStatus | ||
| from rating_api.schemas.base import StatusResponseModel | ||
| from rating_api.schemas.models import ( | ||
| CommentGet, | ||
|
|
@@ -160,8 +163,10 @@ | |
| Возвращает комментарий по его UUID в базе данных RatingAPI | ||
| """ | ||
| comment: Comment = Comment.query(session=db.session).filter(Comment.uuid == uuid).one_or_none() | ||
| like_count = CommentLike.query(session=db.session).filter(CommentLike.comment_uuid == uuid).count() | ||
| if comment is None: | ||
| raise ObjectNotFound(Comment, uuid) | ||
| comment.like_count = like_count | ||
| return CommentGet.model_validate(comment) | ||
|
|
||
|
|
||
|
|
@@ -180,33 +185,33 @@ | |
|
|
||
| `limit` - максимальное количество возвращаемых комментариев | ||
|
|
||
| `offset` - смещение, определяющее, с какого по порядку комментария начинать выборку. | ||
| Если без смещения возвращается комментарий с условным номером N, | ||
| то при значении offset = X будет возвращаться комментарий с номером N + X | ||
| `offset` - смещение, определяющее, с какого по порядку комментария начинать выборку. | ||
|
|
||
| `order_by` - возможное значение `'create_ts'` - возвращается список комментариев отсортированных по времени создания | ||
| `order_by` - возможное значение `'create_ts'` - возвращается список комментариев, отсортированных по времени создания. | ||
|
|
||
| `lecturer_id` - вернет все комментарии для преподавателя с конкретным id, по дефолту возвращает вообще все аппрувнутые комментарии. | ||
| `lecturer_id` - вернет все комментарии для преподавателя с конкретным id. | ||
|
|
||
| `user_id` - вернет все комментарии пользователя с конкретным id | ||
| `user_id` - вернет все комментарии пользователя с конкретным id. | ||
|
|
||
| `unreviewed` - вернет все непроверенные комментарии, если True. По дефолту False. | ||
| """ | ||
| comments = Comment.query(session=db.session).all() | ||
| if not comments: | ||
| raise ObjectNotFound(Comment, 'all') | ||
| if "rating.comment.review" in [scope['name'] for scope in user.get('session_scopes')] or user.get('id') == user_id: | ||
| result = CommentGetAllWithStatus(limit=limit, offset=offset, total=len(comments)) | ||
| comment_validator = CommentGetWithStatus | ||
| else: | ||
| result = CommentGetAll(limit=limit, offset=offset, total=len(comments)) | ||
| comment_validator = CommentGet | ||
| result.comments = comments | ||
| if user_id is not None: | ||
| result.comments = [comment for comment in result.comments if comment.user_id == user_id] | ||
| # Subquery чтобы посчитать лайки | ||
| like_counts = ( | ||
| select(CommentLike.comment_uuid, func.count(CommentLike.id).label('like_count')) | ||
| .where(CommentLike.is_deleted == False) | ||
| .group_by(CommentLike.comment_uuid) | ||
| .alias("like_counts") | ||
| ) | ||
| # получаем комменты с лайками к ним | ||
| comments_query = select(Comment, func.coalesce(like_counts.c.like_count, 0).label('like_count')).outerjoin( | ||
| like_counts, Comment.uuid == like_counts.c.comment_uuid | ||
| ) | ||
|
Comment on lines
+199
to
+208
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. мб вынесем всю эту логику в гибрид-метод?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. это я так, на скорую руку писал, нужно будет переделать, да
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. мне кажется, я тогда с джоином не разобрался как его без селекта сделать |
||
|
|
||
| if lecturer_id is not None: | ||
| result.comments = [comment for comment in result.comments if comment.lecturer_id == lecturer_id] | ||
| comments_query = comments_query.where(Comment.lecturer_id == lecturer_id) | ||
|
|
||
| if user_id is not None: | ||
| comments_query = comments_query.where(Comment.user_id == user_id) | ||
|
|
||
| if unreviewed: | ||
| if not user: | ||
|
|
@@ -219,16 +224,31 @@ | |
| ] | ||
| else: | ||
| raise ForbiddenAction(Comment) | ||
| comments_query = comments_query.where(Comment.review_status == ReviewStatus.PENDING) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. можно через фильтр а не через where, чтобы стилистику кода сохранять |
||
| else: | ||
| result.comments = [comment for comment in result.comments if comment.review_status is ReviewStatus.APPROVED] | ||
|
|
||
| result.comments = result.comments[offset : limit + offset] | ||
| comments_query = comments_query.where(Comment.review_status == ReviewStatus.APPROVED) | ||
|
|
||
| if "create_ts" in order_by: | ||
| result.comments.sort(key=lambda comment: comment.create_ts, reverse=True) | ||
| result.total = len(result.comments) | ||
| result.comments = [comment_validator.model_validate(comment) for comment in result.comments] | ||
| result.comments.sort(key=lambda comment: comment.create_ts, reverse=True) | ||
| comments_query = comments_query.order_by(Comment.create_ts.desc()) | ||
|
|
||
| comments_query = comments_query.offset(offset).limit(limit) | ||
|
|
||
| comments_with_likes = db.session.execute(comments_query).all() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ну вот так не пишем, тоже не наш стиль бэкенда |
||
|
|
||
| if not comments_with_likes: | ||
| raise ObjectNotFound(Comment, 'all') | ||
| # добавляем лайки к комментам | ||
| comments = [] | ||
| for comment, like_count in comments_with_likes: | ||
| comment.like_count = like_count | ||
| comments.append(CommentGet.model_validate(comment)) | ||
|
|
||
| result = CommentGetAll( | ||
| limit=limit, | ||
| offset=offset, | ||
| total=len(comments), | ||
| comments=comments, | ||
| ) | ||
| return result | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import datetime | ||
| from typing import Literal | ||
| from uuid import UUID | ||
|
|
||
| import aiohttp | ||
| from auth_lib.fastapi import UnionAuth | ||
| from fastapi import APIRouter, Depends, Query | ||
| from fastapi_sqlalchemy import db | ||
|
|
||
| from rating_api.exceptions import AlreadyExists, ForbiddenAction, ObjectNotFound | ||
| from rating_api.models import Comment, CommentLike, Lecturer, LecturerUserComment, ReviewStatus | ||
| from rating_api.schemas.base import StatusResponseModel | ||
| from rating_api.schemas.models import LikeGet | ||
| from rating_api.settings import Settings, get_settings | ||
|
|
||
|
|
||
| settings: Settings = get_settings() | ||
| like = APIRouter(prefix="/like", tags=["Like"]) | ||
|
|
||
|
|
||
| @like.post("/{comment_uuid}", response_model=LikeGet) | ||
| async def create_like(comment_uuid, user=Depends(UnionAuth())) -> LikeGet: | ||
| """ | ||
| Создает лайк на коммент | ||
| """ | ||
| comment: Comment = Comment.query(session=db.session).filter(Comment.uuid == comment_uuid).one_or_none() | ||
| if comment is None: | ||
| raise ObjectNotFound(Comment, comment_uuid) | ||
|
|
||
| existing_like: CommentLike = ( | ||
| CommentLike.query(session=db.session) | ||
| .filter( | ||
| CommentLike.comment_uuid == comment_uuid, | ||
| CommentLike.user_id == user.get('id'), | ||
| CommentLike.is_deleted == False, | ||
| ) | ||
| .one_or_none() | ||
| ) | ||
| if existing_like: | ||
| raise AlreadyExists(CommentLike, comment_uuid) | ||
|
|
||
| new_like = CommentLike.create(session=db.session, user_id=user.get('id'), comment_uuid=comment_uuid) | ||
| return LikeGet.model_validate(new_like) | ||
|
|
||
|
|
||
| @like.delete("/{comment_uuid}", response_model=StatusResponseModel) | ||
| async def delete_like(comment_uuid: UUID, user=Depends(UnionAuth())): | ||
| """ | ||
| Удалить свой лайк на коммент | ||
| """ | ||
| like = ( | ||
| CommentLike.query(session=db.session) | ||
| .filter( | ||
| CommentLike.comment_uuid == comment_uuid, | ||
| CommentLike.user_id == user.get('id'), | ||
| CommentLike.is_deleted == False, | ||
| ) | ||
| .one_or_none() | ||
| ) | ||
| if not like: | ||
| raise ObjectNotFound(CommentLike, comment_uuid) | ||
| CommentLike.delete(session=db.session, id=like.id) | ||
| return StatusResponseModel( | ||
| status="Success", message="Like has been deleted", ru="Лайк на данный комментарий был удален" | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
зачем селект