diff --git a/migrations/versions/1c001709fc55_advanced_sort.py b/migrations/versions/1c001709fc55_advanced_sort.py new file mode 100644 index 0000000..8238980 --- /dev/null +++ b/migrations/versions/1c001709fc55_advanced_sort.py @@ -0,0 +1,24 @@ +"""advanced_sort + +Revision ID: 1c001709fc55 +Revises: dd44854aa12a +Create Date: 2025-04-26 17:01:57.140143 + +""" + +import sqlalchemy as sa +from alembic import op + + +revision = '1c001709fc55' +down_revision = 'dd44854aa12a' +branch_labels = None +depends_on = None + + +def upgrade(): + op.alter_column('comment', 'approved_by', existing_type=sa.INTEGER(), nullable=True) + + +def downgrade(): + op.alter_column('comment', 'approved_by', existing_type=sa.INTEGER(), nullable=False) diff --git a/rating_api/models/db.py b/rating_api/models/db.py index 2f01068..28b75fa 100644 --- a/rating_api/models/db.py +++ b/rating_api/models/db.py @@ -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_, desc, func, nulls_last, or_, true from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.attributes import InstrumentedAttribute @@ -123,6 +123,28 @@ class Comment(BaseDbModel): def mark_general(self): return (self.mark_kindness + self.mark_freebie + self.mark_clarity) / 3 + @hybrid_method + def order_by_create_ts( + self, query: str, asc_order: bool + ) -> UnaryExpression[datetime.datetime] | InstrumentedAttribute: + return getattr(Comment, query) if asc_order else desc(getattr(Comment, query)) + + @hybrid_method + def order_by_mark(self, query: str, asc_order: bool) -> UnaryExpression[float] | InstrumentedAttribute: + return getattr(Comment, query) if asc_order else desc(getattr(Comment, query)) + + @hybrid_method + def search_by_lectorer_id(self, query: int) -> bool: + if not query: + return true() + return and_(Comment.review_status == ReviewStatus.APPROVED, Comment.lecturer_id == query) + + @hybrid_method + def search_by_user_id(self, query: int) -> bool: + if not query: + return true() + return Comment.user_id == query + class LecturerUserComment(BaseDbModel): id: Mapped[int] = mapped_column(Integer, primary_key=True) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index 137953d..b17ffa0 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -173,45 +173,60 @@ async def get_comments( offset: int = 0, lecturer_id: int | None = None, user_id: int | None = None, - order_by: list[Literal["create_ts"]] = Query(default=[]), + order_by: str = Query( + enum=["create_ts", "mark_kindness", "mark_freebie", "mark_clarity", "mark_general"], + default="create_ts", + ), unreviewed: bool = False, - user=Depends(UnionAuth(scopes=["rating.comment.review"], auto_error=False, allow_none=True)), + asc_order: bool = False, + user=Depends(UnionAuth(scopes=["rating.comment.review"], auto_error=False, allow_none=False)), ) -> CommentGetAll: """ - Scopes: `["rating.comment.review"]` + Scopes: `["rating.comment.review"]` + + `limit` - максимальное количество возвращаемых комментариев - `limit` - максимальное количество возвращаемых комментариев + `offset` - смещение, определяющее, с какого по порядку комментария начинать выборку. + Если без смещения возвращается комментарий с условным номером N, + то при значении offset = X будет возвращаться комментарий с номером N + X - `offset` - смещение, определяющее, с какого по порядку комментария начинать выборку. - Если без смещения возвращается комментарий с условным номером N, - то при значении offset = X будет возвращаться комментарий с номером N + X + `order_by` - возможные значения `"create_ts", "mark_kindness", "mark_freebie", "mark_clarity", "mark_general"`. + Если передано `'create_ts'` - возвращается список комментариев отсортированных по времени + Если передано `'mark_...'` - возвращается список комментариев отсортированных по конкретной оценке - `order_by` - возможное значение `'create_ts'` - возвращается список комментариев отсортированных по времени создания + `lecturer_id` - вернет все комментарии для преподавателя с конкретным id, по дефолту возвращает вообще все аппрувнутые комментарии. - `lecturer_id` - вернет все комментарии для преподавателя с конкретным id, по дефолту возвращает вообще все аппрувнутые комментарии. + `user_id` - вернет все комментарии пользователя с конкретным id - `user_id` - вернет все комментарии пользователя с конкретным id + `unreviewed` - вернет все непроверенные комментарии, если True. По дефолту False. - `unreviewed` - вернет все непроверенные комментарии, если True. По дефолту False. + `asc_order` -Если передано true, сортировать в порядке возрастания. Иначе - в порядке убывания """ - comments = Comment.query(session=db.session).all() + comments_query = ( + Comment.query(session=db.session) + .filter(Comment.search_by_lectorer_id(lecturer_id)) + .filter(Comment.search_by_user_id(user_id)) + .order_by( + Comment.order_by_mark(order_by, asc_order) + if "mark" in order_by + else Comment.order_by_create_ts(order_by, asc_order) + ) + ) + + comments = comments_query.limit(limit).offset(offset).all() if not comments: raise ObjectNotFound(Comment, 'all') - if "rating.comment.review" in [scope['name'] for scope in user.get('session_scopes')]: + if user and "rating.comment.review" in [scope['name'] for scope in user.get('session_scopes')]: result = CommentGetAllWithAllInfo(limit=limit, offset=offset, total=len(comments)) comment_validator = CommentGetWithAllInfo - elif user.get('id') == user_id: + elif user and 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] - if lecturer_id is not None: - result.comments = [comment for comment in result.comments if comment.lecturer_id == lecturer_id] + result.comments = comments if unreviewed: if not user: @@ -227,13 +242,9 @@ async def get_comments( else: result.comments = [comment for comment in result.comments if comment.review_status is ReviewStatus.APPROVED] - result.comments = result.comments[offset : limit + offset] - - 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) + return result diff --git a/tests/conftest.py b/tests/conftest.py index f5f6378..cfc960f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -46,6 +46,8 @@ def lecturer(dbsession): @pytest.fixture def comment(dbsession, lecturer): _comment = Comment( + user_id=0, + create_ts="2025-04-25T19:38:56.408Z", subject="test_subject", text="test_comment", mark_kindness=1, @@ -166,8 +168,9 @@ def lecturers_with_comments(dbsession, lecturers): (lecturers[2].id, 9992, 'test_subject12', ReviewStatus.APPROVED, 0, 0, 0), ] - comments = [ - Comment( + comments = [] + for lecturer_id, user_id, subject, review_status, mark_kindness, mark_clarity, mark_freebie in comments_data: + comment = Comment( subject=subject, text="test_comment", mark_kindness=mark_kindness, @@ -177,8 +180,12 @@ def lecturers_with_comments(dbsession, lecturers): user_id=user_id, review_status=review_status, ) - for lecturer_id, user_id, subject, review_status, mark_kindness, mark_clarity, mark_freebie in comments_data - ] + + # Set approved_by to -1 for approved or dismissed comments + if review_status in [ReviewStatus.APPROVED, ReviewStatus.DISMISSED]: + comment.approved_by = -1 + + comments.append(comment) dbsession.add_all(comments) dbsession.commit() diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index eaeaae4..79803ef 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -198,6 +198,7 @@ def test_create_comment(client, dbsession, lecturers, body, lecturer_n, response def test_get_comment(client, comment): response_comment = client.get(f'{url}/{comment.uuid}') + print("1") assert response_comment.status_code == status.HTTP_200_OK random_uuid = uuid.uuid4() response = client.get(f'{url}/{random_uuid}') @@ -205,7 +206,8 @@ def test_get_comment(client, comment): @pytest.mark.parametrize( - 'lecturer_n,response_status', [(0, status.HTTP_200_OK), (1, status.HTTP_200_OK), (3, status.HTTP_200_OK)] + 'lecturer_n,response_status', + [(0, status.HTTP_200_OK), (1, status.HTTP_200_OK), (2, status.HTTP_200_OK), (3, status.HTTP_404_NOT_FOUND)], ) def test_comments_by_lecturer_id(client, lecturers_with_comments, lecturer_n, response_status): lecturers, comments = lecturers_with_comments @@ -216,8 +218,10 @@ def test_comments_by_lecturer_id(client, lecturers_with_comments, lecturer_n, re assert len(json_response["comments"]) == len( [ comment - for comment in lecturers[lecturer_n].comments - if comment.review_status == ReviewStatus.APPROVED and not comment.is_deleted + for comment in comments + if comment.lecturer_id == lecturers[lecturer_n].id + and comment.review_status == ReviewStatus.APPROVED + and not comment.is_deleted ] ) @@ -227,7 +231,7 @@ def test_comments_by_lecturer_id(client, lecturers_with_comments, lecturer_n, re ) def test_comments_by_user_id(client, lecturers_with_comments, user_id, response_status): _, comments = lecturers_with_comments - response = response = client.get(f'{url}', params={"user_id": user_id}) + response = client.get(f'{url}', params={"user_id": 9990 + user_id}) assert response.status_code == response_status if response.status_code == status.HTTP_200_OK: json_response = response.json() @@ -235,7 +239,7 @@ def test_comments_by_user_id(client, lecturers_with_comments, user_id, response_ [ comment for comment in comments - if comment.user_id == user_id + if comment.user_id == 9990 + user_id and comment.review_status == ReviewStatus.APPROVED and not comment.is_deleted ]