From 5cb2d73872c10034587e4faf43232bd4253acdb4 Mon Sep 17 00:00:00 2001 From: default Date: Wed, 23 Oct 2024 19:18:42 +0000 Subject: [PATCH 1/6] soft deletes initial commit --- rating_api/models/db.py | 3 +++ rating_api/routes/comment.py | 13 ++++--------- rating_api/routes/lecturer.py | 6 +++--- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/rating_api/models/db.py b/rating_api/models/db.py index c39495d..753e1da 100644 --- a/rating_api/models/db.py +++ b/rating_api/models/db.py @@ -34,6 +34,7 @@ class Lecturer(BaseDbModel): avatar_link: Mapped[str] = mapped_column(String, nullable=True) timetable_id: Mapped[int] = mapped_column(Integer, unique=True, nullable=False) comments: Mapped[list[Comment]] = relationship("Comment", back_populates="lecturer") + is_deleted: Mapped[bool] = mapped_column(Boolean, default=False) class Comment(BaseDbModel): @@ -48,6 +49,7 @@ class Comment(BaseDbModel): lecturer_id: Mapped[int] = mapped_column(Integer, ForeignKey("lecturer.id")) lecturer: Mapped[Lecturer] = relationship("Lecturer", back_populates="comments") review_status: Mapped[ReviewStatus] = mapped_column(DbEnum(ReviewStatus, native_enum=False), nullable=False) + is_deleted: Mapped[bool] = mapped_column(Boolean, default=False) class LecturerUserComment(BaseDbModel): @@ -56,3 +58,4 @@ class LecturerUserComment(BaseDbModel): user_id: Mapped[int] = mapped_column(Integer, nullable=False) 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, default=False) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index 237b64b..b8d3acb 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -6,14 +6,10 @@ from fastapi import APIRouter, Depends, Query from fastapi_sqlalchemy import db -from rating_api.models import Comment, Lecturer, LecturerUserComment, ReviewStatus from rating_api.exceptions import ForbiddenAction, ObjectNotFound, TooManyCommentRequests +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, -) +from rating_api.schemas.models import CommentGet, CommentGetAll, CommentPost from rating_api.settings import Settings, get_settings @@ -86,7 +82,7 @@ async def get_comments( `unreviewed` - вернет все непроверенные комментарии, если True. По дефолту False. """ - comments = Comment.query(session=db.session).all() + comments = Comment.query(session=db.session).filter(Comment.is_deleted == False).all() if not comments: raise ObjectNotFound(Comment, 'all') result = CommentGetAll(limit=limit, offset=offset, total=len(comments)) @@ -142,8 +138,7 @@ async def delete_comment( check_comment = Comment.get(session=db.session, id=uuid) if check_comment is None: raise ObjectNotFound(Comment, uuid) - Comment.delete(session=db.session, id=uuid) - + Comment.update(session=db.session, id=uuid, is_deleted=True) return StatusResponseModel( status="Success", message="Comment has been deleted", ru="Комментарий удален из RatingAPI" ) diff --git a/rating_api/routes/lecturer.py b/rating_api/routes/lecturer.py index e370225..bfad8bd 100644 --- a/rating_api/routes/lecturer.py +++ b/rating_api/routes/lecturer.py @@ -5,8 +5,8 @@ from fastapi_sqlalchemy import db from sqlalchemy import and_ -from rating_api.models import Comment, Lecturer, LecturerUserComment, ReviewStatus from rating_api.exceptions import AlreadyExists, ObjectNotFound +from rating_api.models import Comment, Lecturer, LecturerUserComment, ReviewStatus from rating_api.schemas.base import StatusResponseModel from rating_api.schemas.models import CommentGet, LecturerGet, LecturerGetAll, LecturerPatch, LecturerPost @@ -101,7 +101,7 @@ async def get_lecturers( Если передано `subject` - возвращает всех преподавателей, для которых переданное значение совпадает с их предметом преподавания. Также возвращает всех преподавателей, у которых есть комментарий с совпадающим с данным subject. """ - lecturers = Lecturer.query(session=db.session).all() + lecturers = Lecturer.query(session=db.session).filter(Lecturer.is_deleted == False).all() if not lecturers: raise ObjectNotFound(Lecturer, 'all') result = LecturerGetAll(limit=limit, offset=offset, total=len(lecturers)) @@ -188,7 +188,7 @@ async def delete_lecturer( for lecturer_user_comment in lecturer_user_comments: LecturerUserComment.delete(lecturer_user_comment.id, session=db.session) - Lecturer.delete(session=db.session, id=id) + Lecturer.update(session=db.session, id=id, is_deleted=True) return StatusResponseModel( status="Success", message="Lecturer has been deleted", ru="Преподаватель удален из RatingAPI" ) From 23a3ec1e6f4323ddd888d38166756afde0b6daf4 Mon Sep 17 00:00:00 2001 From: default Date: Thu, 24 Oct 2024 07:07:37 +0000 Subject: [PATCH 2/6] fix --- .../0c117913717b_adding_is_deleted_fields.py | 31 +++++++++++++++++++ rating_api/routes/comment.py | 4 +-- rating_api/routes/lecturer.py | 4 +-- 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 migrations/versions/0c117913717b_adding_is_deleted_fields.py diff --git a/migrations/versions/0c117913717b_adding_is_deleted_fields.py b/migrations/versions/0c117913717b_adding_is_deleted_fields.py new file mode 100644 index 0000000..0e4ed05 --- /dev/null +++ b/migrations/versions/0c117913717b_adding_is_deleted_fields.py @@ -0,0 +1,31 @@ +"""adding_is_deleted_fields + +Revision ID: 0c117913717b +Revises: 656228b2d6e0 +Create Date: 2024-10-24 06:59:27.285029 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0c117913717b' +down_revision = '656228b2d6e0' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('comment', sa.Column('is_deleted', sa.Boolean(), nullable=False, server_default=sa.false)) + op.add_column('lecturer', sa.Column('is_deleted', sa.Boolean(), nullable=False, server_default=sa.false)) + op.add_column( + 'lecturer_user_comment', sa.Column('is_deleted', sa.Boolean(), nullable=False, server_default=sa.false) + ) + + +def downgrade(): + op.drop_column('lecturer_user_comment', 'is_deleted') + op.drop_column('lecturer', 'is_deleted') + op.drop_column('comment', 'is_deleted') diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index b8d3acb..a6be194 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -82,7 +82,7 @@ async def get_comments( `unreviewed` - вернет все непроверенные комментарии, если True. По дефолту False. """ - comments = Comment.query(session=db.session).filter(Comment.is_deleted == False).all() + comments = Comment.query(session=db.session).all() if not comments: raise ObjectNotFound(Comment, 'all') result = CommentGetAll(limit=limit, offset=offset, total=len(comments)) @@ -138,7 +138,7 @@ async def delete_comment( check_comment = Comment.get(session=db.session, id=uuid) if check_comment is None: raise ObjectNotFound(Comment, uuid) - Comment.update(session=db.session, id=uuid, is_deleted=True) + Comment.delete(session=db.session, id=uuid) return StatusResponseModel( status="Success", message="Comment has been deleted", ru="Комментарий удален из RatingAPI" ) diff --git a/rating_api/routes/lecturer.py b/rating_api/routes/lecturer.py index bfad8bd..20aabe9 100644 --- a/rating_api/routes/lecturer.py +++ b/rating_api/routes/lecturer.py @@ -101,7 +101,7 @@ async def get_lecturers( Если передано `subject` - возвращает всех преподавателей, для которых переданное значение совпадает с их предметом преподавания. Также возвращает всех преподавателей, у которых есть комментарий с совпадающим с данным subject. """ - lecturers = Lecturer.query(session=db.session).filter(Lecturer.is_deleted == False).all() + lecturers = Lecturer.query(session=db.session).all() if not lecturers: raise ObjectNotFound(Lecturer, 'all') result = LecturerGetAll(limit=limit, offset=offset, total=len(lecturers)) @@ -188,7 +188,7 @@ async def delete_lecturer( for lecturer_user_comment in lecturer_user_comments: LecturerUserComment.delete(lecturer_user_comment.id, session=db.session) - Lecturer.update(session=db.session, id=id, is_deleted=True) + Lecturer.delete(session=db.session, id=id) return StatusResponseModel( status="Success", message="Lecturer has been deleted", ru="Преподаватель удален из RatingAPI" ) From 770f528b608e547e69b5f9861d7b1564d4f81b4a Mon Sep 17 00:00:00 2001 From: default Date: Thu, 24 Oct 2024 07:07:52 +0000 Subject: [PATCH 3/6] fix --- .../versions/0c117913717b_adding_is_deleted_fields.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/migrations/versions/0c117913717b_adding_is_deleted_fields.py b/migrations/versions/0c117913717b_adding_is_deleted_fields.py index 0e4ed05..cf026a1 100644 --- a/migrations/versions/0c117913717b_adding_is_deleted_fields.py +++ b/migrations/versions/0c117913717b_adding_is_deleted_fields.py @@ -6,8 +6,8 @@ """ -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. @@ -18,10 +18,10 @@ def upgrade(): - op.add_column('comment', sa.Column('is_deleted', sa.Boolean(), nullable=False, server_default=sa.false)) - op.add_column('lecturer', sa.Column('is_deleted', sa.Boolean(), nullable=False, server_default=sa.false)) + op.add_column('comment', sa.Column('is_deleted', sa.Boolean(), nullable=False, server_default=sa.false())) + op.add_column('lecturer', sa.Column('is_deleted', sa.Boolean(), nullable=False, server_default=sa.false())) op.add_column( - 'lecturer_user_comment', sa.Column('is_deleted', sa.Boolean(), nullable=False, server_default=sa.false) + 'lecturer_user_comment', sa.Column('is_deleted', sa.Boolean(), nullable=False, server_default=sa.false()) ) From 041d8d18b12af741d13931f2e9e63b6f7fd17cfd Mon Sep 17 00:00:00 2001 From: default Date: Fri, 25 Oct 2024 15:07:02 +0000 Subject: [PATCH 4/6] relationship --- rating_api/models/db.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/rating_api/models/db.py b/rating_api/models/db.py index 753e1da..213934b 100644 --- a/rating_api/models/db.py +++ b/rating_api/models/db.py @@ -46,7 +46,11 @@ class Comment(BaseDbModel): mark_kindness: Mapped[int] = mapped_column(Integer, nullable=False) mark_freebie: Mapped[int] = mapped_column(Integer, nullable=False) mark_clarity: Mapped[int] = mapped_column(Integer, nullable=False) - lecturer_id: Mapped[int] = mapped_column(Integer, ForeignKey("lecturer.id")) + lecturer_id: Mapped[int] = mapped_column( + Integer, + ForeignKey("lecturer.id"), + primary_join="and_(Comment.lecturer_id==Lecturer.id),not_(Lecturer.is_deleted))", + ) lecturer: Mapped[Lecturer] = relationship("Lecturer", back_populates="comments") review_status: Mapped[ReviewStatus] = mapped_column(DbEnum(ReviewStatus, native_enum=False), nullable=False) is_deleted: Mapped[bool] = mapped_column(Boolean, default=False) @@ -54,7 +58,11 @@ class Comment(BaseDbModel): class LecturerUserComment(BaseDbModel): id: Mapped[int] = mapped_column(Integer, primary_key=True) - lecturer_id: Mapped[int] = mapped_column(Integer, ForeignKey("lecturer.id")) + lecturer_id: Mapped[int] = mapped_column( + Integer, + ForeignKey("lecturer.id"), + primary_join="and_(LecturerUserComment.lecturer_id==Lecturer.id),not_(Lecturer.is_deleted))", + ) user_id: Mapped[int] = mapped_column(Integer, nullable=False) 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) From 8dadb9f288c739bb3b990c56378f027535ee81c9 Mon Sep 17 00:00:00 2001 From: default Date: Sat, 26 Oct 2024 09:51:21 +0000 Subject: [PATCH 5/6] relationships --- rating_api/models/db.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rating_api/models/db.py b/rating_api/models/db.py index 213934b..0db151b 100644 --- a/rating_api/models/db.py +++ b/rating_api/models/db.py @@ -33,7 +33,11 @@ class Lecturer(BaseDbModel): subject: Mapped[str] = mapped_column(String, nullable=True) avatar_link: Mapped[str] = mapped_column(String, nullable=True) timetable_id: Mapped[int] = mapped_column(Integer, unique=True, nullable=False) - comments: Mapped[list[Comment]] = relationship("Comment", back_populates="lecturer") + comments: Mapped[list[Comment]] = relationship( + "Comment", + back_populates="lecturer", + primaryjoin="and_(Lecturer.id == Comment.lecturer_id, not_(Comment.is_deleted))", + ) is_deleted: Mapped[bool] = mapped_column(Boolean, default=False) @@ -49,7 +53,7 @@ class Comment(BaseDbModel): lecturer_id: Mapped[int] = mapped_column( Integer, ForeignKey("lecturer.id"), - primary_join="and_(Comment.lecturer_id==Lecturer.id),not_(Lecturer.is_deleted))", + primary_join="and_(Comment.lecturer_id==Lecturer.id, not_(Lecturer.is_deleted))", ) lecturer: Mapped[Lecturer] = relationship("Lecturer", back_populates="comments") review_status: Mapped[ReviewStatus] = mapped_column(DbEnum(ReviewStatus, native_enum=False), nullable=False) From 788260760fb85e4f24a1c748e3eb12e6f1240159 Mon Sep 17 00:00:00 2001 From: default Date: Sat, 9 Nov 2024 14:42:33 +0000 Subject: [PATCH 6/6] tests fix up to soft deletes --- rating_api/models/base.py | 4 ++-- tests/test_routes/test_comment.py | 6 ++++-- tests/test_routes/test_lecturer.py | 21 ++++++++++++++++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/rating_api/models/base.py b/rating_api/models/base.py index 678078a..8ffc3b1 100644 --- a/rating_api/models/base.py +++ b/rating_api/models/base.py @@ -2,9 +2,9 @@ import re -from sqlalchemy import Integer, not_ +from sqlalchemy import not_ from sqlalchemy.exc import NoResultFound -from sqlalchemy.orm import Mapped, Query, Session, as_declarative, declared_attr, mapped_column +from sqlalchemy.orm import Query, Session, as_declarative, declared_attr from rating_api.exceptions import ObjectNotFound diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 8361450..780457a 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -105,7 +105,9 @@ def test_delete_comment(client, dbsession): random_uuid = uuid.uuid4() response = client.delete(f'{url}/{random_uuid}') assert response.status_code == status.HTTP_404_NOT_FOUND - comment = Comment.query(session=dbsession).filter(Comment.uuid == comment.uuid).one_or_none() - assert comment is None + comment1 = Comment.query(session=dbsession).filter(Comment.uuid == comment.uuid).one_or_none() + assert comment1 == None + comment.is_deleted = True + dbsession.delete(comment) dbsession.delete(lecturer) dbsession.commit() diff --git a/tests/test_routes/test_lecturer.py b/tests/test_routes/test_lecturer.py index 85146b8..22066d2 100644 --- a/tests/test_routes/test_lecturer.py +++ b/tests/test_routes/test_lecturer.py @@ -101,9 +101,18 @@ def test_get_lecturer_with_comments(client, dbsession): assert json_response["mark_general"] == 0.5 assert json_response["subject"] == "Физика" assert len(json_response["comments"]) != 0 + dbsession.delete(comment1) + dbsession.delete(comment2) + dbsession.delete(comment3) + dbsession.delete(lecturer) + dbsession.commit() def test_update_lecturer(client, dbsession): + body = {"first_name": 'Иван', "last_name": 'Иванов', "middle_name": 'Иванович', "timetable_id": 0} + lecturer: Lecturer = Lecturer(**body) + dbsession.add(lecturer) + dbsession.commit() body = { "first_name": 'Алексей', "last_name": 'Алексеев', @@ -128,12 +137,22 @@ def test_update_lecturer(client, dbsession): assert json_response["first_name"] == 'Иван' assert json_response["last_name"] == 'Иванов' assert json_response["middle_name"] == "Иванович" + db_lecturer: Lecturer = Lecturer.query(session=dbsession).filter(Lecturer.timetable_id == 0).one_or_none() + dbsession.delete(db_lecturer) + dbsession.commit() def test_delete_lecturer(client, dbsession): + body = {"first_name": 'Иван', "last_name": 'Иванов', "middle_name": 'Иванович', "timetable_id": 0} + lecturer: Lecturer = Lecturer(**body) + dbsession.add(lecturer) + dbsession.commit() lecturer = dbsession.query(Lecturer).filter(Lecturer.timetable_id == 0).one_or_none() - assert lecturer is not None + assert not lecturer is None response = client.delete(f"{url}/{lecturer.id}") assert response.status_code == status.HTTP_200_OK response = client.delete(f"{url}/{lecturer.id}") assert response.status_code == status.HTTP_404_NOT_FOUND + lecturer.is_deleted = True + dbsession.delete(lecturer) + dbsession.commit()