From 2115821ffa869e967725395616bdb65112c459b0 Mon Sep 17 00:00:00 2001 From: gitfresnel <151745312+gitfresnel@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:59:46 +0300 Subject: [PATCH 01/12] f --- ...93_add_is_deleted_field_to_unionmember_.py | 37 +++++++++++++++++++ print_service/exceptions.py | 4 ++ print_service/models/__init__.py | 1 + print_service/routes/exc_handlers.py | 11 ++++++ print_service/routes/file.py | 16 ++++++++ print_service/routes/user.py | 13 ++++++- print_service/utils/__init__.py | 1 + tests/test_routes/conftest.py | 7 ++++ tests/test_routes/test_file.py | 33 +++++++++++++++++ tests/test_routes/test_user.py | 24 ++++++++++++ 10 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 migrations/versions/dddf1cc0c593_add_is_deleted_field_to_unionmember_.py diff --git a/migrations/versions/dddf1cc0c593_add_is_deleted_field_to_unionmember_.py b/migrations/versions/dddf1cc0c593_add_is_deleted_field_to_unionmember_.py new file mode 100644 index 0000000..d49e64d --- /dev/null +++ b/migrations/versions/dddf1cc0c593_add_is_deleted_field_to_unionmember_.py @@ -0,0 +1,37 @@ +"""Add is_deleted field to UnionMember table + +Revision ID: dddf1cc0c593 +Revises: a68c6bb2972c +Create Date: 2024-10-19 19:49:20.361627 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'dddf1cc0c593' +down_revision = 'a68c6bb2972c' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('file', 'source', + existing_type=sa.VARCHAR(), + nullable=False) + op.add_column('union_member', sa.Column('is_deleted', sa.Boolean(), nullable=True)) + op.execute(f'UPDATE "union_member" SET is_deleted = False') + op.alter_column('union_member', sa.Column('is_deleted', sa.Boolean(), nullable=False)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('union_member', 'is_deleted') + op.alter_column('file', 'source', + existing_type=sa.VARCHAR(), + nullable=True) + # ### end Alembic commands ### diff --git a/print_service/exceptions.py b/print_service/exceptions.py index 99ed16a..259690f 100644 --- a/print_service/exceptions.py +++ b/print_service/exceptions.py @@ -12,6 +12,10 @@ class TerminalTokenNotFound(ObjectNotFound): pass +class UserIsDeleted(Exception): + pass + + class TerminalQRNotFound(ObjectNotFound): pass diff --git a/print_service/models/__init__.py b/print_service/models/__init__.py index d1ffcfe..b577f42 100644 --- a/print_service/models/__init__.py +++ b/print_service/models/__init__.py @@ -22,6 +22,7 @@ class UnionMember(Model): surname: Mapped[str] = mapped_column(String, nullable=False) union_number: Mapped[str] = mapped_column(String, nullable=True) student_number: Mapped[str] = mapped_column(String, nullable=True) + is_deleted: Mapped[bool] = mapped_column(Boolean, nullable=True, default=False) files: Mapped[list[File]] = relationship('File', back_populates='owner') print_facts: Mapped[list[PrintFact]] = relationship('PrintFact', back_populates='owner') diff --git a/print_service/routes/exc_handlers.py b/print_service/routes/exc_handlers.py index ecb8aeb..cb868e3 100644 --- a/print_service/routes/exc_handlers.py +++ b/print_service/routes/exc_handlers.py @@ -21,6 +21,7 @@ UnionStudentDuplicate, UnprocessableFileInstance, UserNotFound, + UserIsDeleted, ) from print_service.routes.base import app from print_service.settings import get_settings @@ -75,6 +76,16 @@ async def terminal_not_found_by_qr(req: starlette.requests.Request, exc: Termina ) +@app.exception_handler(UserIsDeleted) +async def user_is_deleted(req: starlette.requests.Request, exc: TerminalTokenNotFound): + return JSONResponse( + content=StatusResponseModel( + status="Error", message="User is deleted", ru="Пользователь удалён из базы" + ).model_dump(), + status_code=410 + ) + + @app.exception_handler(TerminalTokenNotFound) async def terminal_not_found_by_token(req: starlette.requests.Request, exc: TerminalTokenNotFound): return JSONResponse( diff --git a/print_service/routes/file.py b/print_service/routes/file.py index 5205e0f..9b99651 100644 --- a/print_service/routes/file.py +++ b/print_service/routes/file.py @@ -25,6 +25,7 @@ TooManyPages, UnprocessableFileInstance, UserNotFound, + UserIsDeleted, ) from print_service.models import File as FileModel from print_service.models import UnionMember @@ -104,6 +105,7 @@ class ReceiveOutput(BaseModel): responses={ 403: {'model': StatusResponseModel, 'detail': 'User error'}, 500: {'model': StatusResponseModel, 'detail': 'PIN generate error'}, + 410: {'model': StatusResponseModel, 'detail': 'User is deleted'}, }, response_model=SendOutput, ) @@ -122,6 +124,9 @@ async def send(inp: SendInput, settings: Settings = Depends(get_settings)): ), func.upper(UnionMember.surname) == inp.surname.upper(), ).one_or_none() + if user: + if user.is_deleted: + raise UserIsDeleted() if not user: raise NotInUnion() try: @@ -155,6 +160,7 @@ async def send(inp: SendInput, settings: Settings = Depends(get_settings)): 415: {'model': StatusResponseModel, 'detail': 'File error'}, 413: {'model': StatusResponseModel, 'detail': 'Too large file'}, 416: {'model': StatusResponseModel, 'detail': 'Invalid page request'}, + 410: {'model': StatusResponseModel, 'detail': 'User is deleted'}, }, response_model=SendOutput, ) @@ -175,6 +181,10 @@ async def upload_file( .order_by(FileModel.created_at.desc()) .one_or_none() ) + if file_model: + if file_model.owner.is_deleted: + raise UserIsDeleted() + if not file_model: await file.close() raise PINNotFound(pin) @@ -225,6 +235,7 @@ async def upload_file( 404: {'model': StatusResponseModel, 'detail': 'Pin not found'}, 413: {'model': StatusResponseModel, 'detail': 'Too many pages'}, 416: {'model': StatusResponseModel, 'detail': 'Invalid page request'}, + 410: {'model': StatusResponseModel, 'detail': 'User is deleted'}, }, response_model=SendOutput, ) @@ -242,6 +253,11 @@ async def update_file_options( .order_by(FileModel.created_at.desc()) .one_or_none() ) + + if file_model: + if file_model.owner.is_deleted: + raise UserIsDeleted() + print(options) if not file_model: raise PINNotFound(pin) diff --git a/print_service/routes/user.py b/print_service/routes/user.py index 7cd7743..7007ef0 100644 --- a/print_service/routes/user.py +++ b/print_service/routes/user.py @@ -9,7 +9,7 @@ from sqlalchemy import and_, func, or_ from print_service import __version__ -from print_service.exceptions import UnionStudentDuplicate, UserNotFound +from print_service.exceptions import UnionStudentDuplicate, UserNotFound, UserIsDeleted from print_service.models import UnionMember from print_service.schema import BaseModel from print_service.settings import get_settings @@ -42,6 +42,7 @@ class UpdateUserList(BaseModel): status_code=202, responses={ 404: {'detail': 'User not found'}, + 410: {'detail': 'User is deleted'} }, ) async def check_union_member( @@ -61,7 +62,9 @@ async def check_union_member( ), func.upper(UnionMember.surname) == surname, ).one_or_none() - + if user: + if user.is_deleted: + raise UserIsDeleted() if v == '1': return bool(user) @@ -110,16 +113,22 @@ def update_list( .one_or_none() ) + if db_user: + if db_user.is_deleted: + raise UserIsDeleted() + if db_user: db_user.surname = user.username db_user.union_number = user.union_number db_user.student_number = user.student_number + db_user.is_deleted = False else: db.session.add( UnionMember( surname=user.username, union_number=user.union_number, student_number=user.student_number, + is_deleted=False, ) ) db.session.flush() diff --git a/print_service/utils/__init__.py b/print_service/utils/__init__.py index 1c44b33..863cf2d 100644 --- a/print_service/utils/__init__.py +++ b/print_service/utils/__init__.py @@ -62,6 +62,7 @@ def get_file(dbsession, pin: str or list[str]): .order_by(FileModel.created_at.desc()) .all() ) + if len(pin) != len(files): raise FileNotFound(len(pin) - len(files)) diff --git a/tests/test_routes/conftest.py b/tests/test_routes/conftest.py index 8eb798b..2575c66 100644 --- a/tests/test_routes/conftest.py +++ b/tests/test_routes/conftest.py @@ -23,6 +23,13 @@ def union_member_user(dbsession): dbsession.commit() +@pytest.fixture(scope='function') +def add_is_deleted_flag(dbsession): + db_user = dbsession.query(UnionMember).filter(UnionMember.id == 42).one_or_none() + db_user.is_deleted = True + dbsession.commit() + + @pytest.fixture(scope='function') def uploaded_file_db(dbsession, union_member_user, client): body = { diff --git a/tests/test_routes/test_file.py b/tests/test_routes/test_file.py index 864e245..76e5b5c 100644 --- a/tests/test_routes/test_file.py +++ b/tests/test_routes/test_file.py @@ -44,6 +44,18 @@ def test_post_success(union_member_user, client, dbsession): dbsession.commit() +def test_post_is_deleted(client, union_member_user, add_is_deleted_flag): + body = { + "surname": union_member_user['surname'], + "number": union_member_user['union_number'], + "filename": "filename.pdf", + "source": "webapp", + "options": {"pages": "", "copies": 1, "two_sided": False}, + } + res = client.post(url, data=json.dumps(body)) + assert res.status_code == status.HTTP_410_GONE + + def test_post_unauthorized_user(client): body = { "surname": 'surname', @@ -108,6 +120,27 @@ def test_file_check(): assert checking_for_pdf(open("tests/test_routes/test_files/correct.pdf", "rb").read()) == (True, 2) +def test_upload_is_deleted(pin_pdf, client, add_is_deleted_flag): + pin = pin_pdf + fileName = 'tests/test_routes/test_files/correct.pdf' + files = {'file': (f"{fileName}", open(f"{fileName}", 'rb'), "application/pdf")} + res = client.post(f"{url}/{pin}", files=files) + assert res.status_code == status.HTTP_410_GONE + + +def test_patch_is_deleted(pin_pdf, client, add_is_deleted_flag): + pin = pin_pdf + body = { + "options": { + "pages": "", + "copies": 2, + "two_sided": False + } + } + res = client.patch(f"{url}/{pin}", json=body) + assert res.status_code == status.HTTP_410_GONE + + def test_upload_and_print_correct_pdf(pin_pdf, client): pin = pin_pdf fileName = 'tests/test_routes/test_files/correct.pdf' diff --git a/tests/test_routes/test_user.py b/tests/test_routes/test_user.py index 97fdb44..a1ed9fd 100644 --- a/tests/test_routes/test_user.py +++ b/tests/test_routes/test_user.py @@ -29,6 +29,15 @@ def test_get_not_found(client): assert res.status_code == status.HTTP_404_NOT_FOUND +def test_get_is_deleted(client, union_member_user, add_is_deleted_flag): + params = { + 'surname': 'test', + 'number': '6666667', + } + res = client.get(url, params=params) + assert res.status_code == status.HTTP_410_GONE + + def test_post_success(client, dbsession): body = { 'users': [ @@ -49,6 +58,20 @@ def test_post_success(client, dbsession): dbsession.commit() +def test_post_is_deleted(client, union_member_user, add_is_deleted_flag): + body = { + 'users': [ + { + 'username': 'new_test', + 'union_number': '6666667', + 'student_number': '13033224', + } + ], + } + res = client.post(url, data=json.dumps(body)) + assert res.status_code == status.HTTP_410_GONE + + @pytest.mark.parametrize( 'users', [ @@ -88,3 +111,4 @@ def test_post_list_duplicates(users, client): body = {'users': users} res = client.post(url, json=body) assert res.status_code == status.HTTP_400_BAD_REQUEST, res.json() + From 2a6326b6b42445e8b40c80a232bd49fa7e6f8412 Mon Sep 17 00:00:00 2001 From: gitfresnel <151745312+gitfresnel@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:09:33 +0300 Subject: [PATCH 02/12] f --- ...df1cc0c593_add_is_deleted_field_to_unionmember_.py | 11 ++++------- print_service/routes/exc_handlers.py | 4 ++-- print_service/routes/file.py | 2 +- print_service/routes/user.py | 7 ++----- tests/test_routes/test_file.py | 8 +------- tests/test_routes/test_user.py | 1 - 6 files changed, 10 insertions(+), 23 deletions(-) diff --git a/migrations/versions/dddf1cc0c593_add_is_deleted_field_to_unionmember_.py b/migrations/versions/dddf1cc0c593_add_is_deleted_field_to_unionmember_.py index d49e64d..9f38bf7 100644 --- a/migrations/versions/dddf1cc0c593_add_is_deleted_field_to_unionmember_.py +++ b/migrations/versions/dddf1cc0c593_add_is_deleted_field_to_unionmember_.py @@ -5,8 +5,9 @@ Create Date: 2024-10-19 19:49:20.361627 """ -from alembic import op + import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. @@ -18,9 +19,7 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('file', 'source', - existing_type=sa.VARCHAR(), - nullable=False) + op.alter_column('file', 'source', existing_type=sa.VARCHAR(), nullable=False) op.add_column('union_member', sa.Column('is_deleted', sa.Boolean(), nullable=True)) op.execute(f'UPDATE "union_member" SET is_deleted = False') op.alter_column('union_member', sa.Column('is_deleted', sa.Boolean(), nullable=False)) @@ -31,7 +30,5 @@ def upgrade(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### op.drop_column('union_member', 'is_deleted') - op.alter_column('file', 'source', - existing_type=sa.VARCHAR(), - nullable=True) + op.alter_column('file', 'source', existing_type=sa.VARCHAR(), nullable=True) # ### end Alembic commands ### diff --git a/print_service/routes/exc_handlers.py b/print_service/routes/exc_handlers.py index cb868e3..8661f8f 100644 --- a/print_service/routes/exc_handlers.py +++ b/print_service/routes/exc_handlers.py @@ -20,8 +20,8 @@ TooManyPages, UnionStudentDuplicate, UnprocessableFileInstance, - UserNotFound, UserIsDeleted, + UserNotFound, ) from print_service.routes.base import app from print_service.settings import get_settings @@ -82,7 +82,7 @@ async def user_is_deleted(req: starlette.requests.Request, exc: TerminalTokenNot content=StatusResponseModel( status="Error", message="User is deleted", ru="Пользователь удалён из базы" ).model_dump(), - status_code=410 + status_code=410, ) diff --git a/print_service/routes/file.py b/print_service/routes/file.py index 9b99651..472799f 100644 --- a/print_service/routes/file.py +++ b/print_service/routes/file.py @@ -24,8 +24,8 @@ TooLargeSize, TooManyPages, UnprocessableFileInstance, - UserNotFound, UserIsDeleted, + UserNotFound, ) from print_service.models import File as FileModel from print_service.models import UnionMember diff --git a/print_service/routes/user.py b/print_service/routes/user.py index 7007ef0..5f0dd15 100644 --- a/print_service/routes/user.py +++ b/print_service/routes/user.py @@ -9,7 +9,7 @@ from sqlalchemy import and_, func, or_ from print_service import __version__ -from print_service.exceptions import UnionStudentDuplicate, UserNotFound, UserIsDeleted +from print_service.exceptions import UnionStudentDuplicate, UserIsDeleted, UserNotFound from print_service.models import UnionMember from print_service.schema import BaseModel from print_service.settings import get_settings @@ -40,10 +40,7 @@ class UpdateUserList(BaseModel): @router.get( '/is_union_member', status_code=202, - responses={ - 404: {'detail': 'User not found'}, - 410: {'detail': 'User is deleted'} - }, + responses={404: {'detail': 'User not found'}, 410: {'detail': 'User is deleted'}}, ) async def check_union_member( surname: constr(strip_whitespace=True, to_upper=True, min_length=1), diff --git a/tests/test_routes/test_file.py b/tests/test_routes/test_file.py index 76e5b5c..d9fdbf1 100644 --- a/tests/test_routes/test_file.py +++ b/tests/test_routes/test_file.py @@ -130,13 +130,7 @@ def test_upload_is_deleted(pin_pdf, client, add_is_deleted_flag): def test_patch_is_deleted(pin_pdf, client, add_is_deleted_flag): pin = pin_pdf - body = { - "options": { - "pages": "", - "copies": 2, - "two_sided": False - } - } + body = {"options": {"pages": "", "copies": 2, "two_sided": False}} res = client.patch(f"{url}/{pin}", json=body) assert res.status_code == status.HTTP_410_GONE diff --git a/tests/test_routes/test_user.py b/tests/test_routes/test_user.py index a1ed9fd..9f9fcff 100644 --- a/tests/test_routes/test_user.py +++ b/tests/test_routes/test_user.py @@ -111,4 +111,3 @@ def test_post_list_duplicates(users, client): body = {'users': users} res = client.post(url, json=body) assert res.status_code == status.HTTP_400_BAD_REQUEST, res.json() - From 5495e19b062985191661bf690ceb266fc3c9f36a Mon Sep 17 00:00:00 2001 From: gitfresnel <151745312+gitfresnel@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:49:13 +0300 Subject: [PATCH 03/12] Delete migrations/versions/dddf1cc0c593_add_is_deleted_field_to_unionmember_.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Удаление старого файла миграции --- ...93_add_is_deleted_field_to_unionmember_.py | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 migrations/versions/dddf1cc0c593_add_is_deleted_field_to_unionmember_.py diff --git a/migrations/versions/dddf1cc0c593_add_is_deleted_field_to_unionmember_.py b/migrations/versions/dddf1cc0c593_add_is_deleted_field_to_unionmember_.py deleted file mode 100644 index 9f38bf7..0000000 --- a/migrations/versions/dddf1cc0c593_add_is_deleted_field_to_unionmember_.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Add is_deleted field to UnionMember table - -Revision ID: dddf1cc0c593 -Revises: a68c6bb2972c -Create Date: 2024-10-19 19:49:20.361627 - -""" - -import sqlalchemy as sa -from alembic import op - - -# revision identifiers, used by Alembic. -revision = 'dddf1cc0c593' -down_revision = 'a68c6bb2972c' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('file', 'source', existing_type=sa.VARCHAR(), nullable=False) - op.add_column('union_member', sa.Column('is_deleted', sa.Boolean(), nullable=True)) - op.execute(f'UPDATE "union_member" SET is_deleted = False') - op.alter_column('union_member', sa.Column('is_deleted', sa.Boolean(), nullable=False)) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('union_member', 'is_deleted') - op.alter_column('file', 'source', existing_type=sa.VARCHAR(), nullable=True) - # ### end Alembic commands ### From dae002894dab0d22153349b9295b24c656d518f1 Mon Sep 17 00:00:00 2001 From: gitfresnel <151745312+gitfresnel@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:50:05 +0300 Subject: [PATCH 04/12] f --- ...10249_add_is_deleted_field_to_unionmember_.py} | 7 +++---- print_service/models/__init__.py | 12 ++++++++++-- print_service/routes/file.py | 9 --------- tests/test_routes/test_file.py | 15 --------------- 4 files changed, 13 insertions(+), 30 deletions(-) rename migrations/versions/{dddf1cc0c593_add_is_deleted_field_to_unionmember_.py => 120e06710249_add_is_deleted_field_to_unionmember_.py} (90%) diff --git a/migrations/versions/dddf1cc0c593_add_is_deleted_field_to_unionmember_.py b/migrations/versions/120e06710249_add_is_deleted_field_to_unionmember_.py similarity index 90% rename from migrations/versions/dddf1cc0c593_add_is_deleted_field_to_unionmember_.py rename to migrations/versions/120e06710249_add_is_deleted_field_to_unionmember_.py index 9f38bf7..808eda9 100644 --- a/migrations/versions/dddf1cc0c593_add_is_deleted_field_to_unionmember_.py +++ b/migrations/versions/120e06710249_add_is_deleted_field_to_unionmember_.py @@ -1,8 +1,8 @@ """Add is_deleted field to UnionMember table -Revision ID: dddf1cc0c593 +Revision ID: 120e06710249 Revises: a68c6bb2972c -Create Date: 2024-10-19 19:49:20.361627 +Create Date: 2024-10-25 16:40:35.109586 """ @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. -revision = 'dddf1cc0c593' +revision = '120e06710249' down_revision = 'a68c6bb2972c' branch_labels = None depends_on = None @@ -23,7 +23,6 @@ def upgrade(): op.add_column('union_member', sa.Column('is_deleted', sa.Boolean(), nullable=True)) op.execute(f'UPDATE "union_member" SET is_deleted = False') op.alter_column('union_member', sa.Column('is_deleted', sa.Boolean(), nullable=False)) - # ### end Alembic commands ### diff --git a/print_service/models/__init__.py b/print_service/models/__init__.py index b577f42..9573083 100644 --- a/print_service/models/__init__.py +++ b/print_service/models/__init__.py @@ -45,7 +45,11 @@ class File(Model): number_of_pages: Mapped[int] = Column(Integer) source: Mapped[str] = Column(String, default='unknown', nullable=False) - owner: Mapped[UnionMember] = relationship('UnionMember', back_populates='files') + owner: Mapped[UnionMember] = relationship( + 'UnionMember', + primaryjoin="and_(File.owner_id==UnionMember.id, not_(UnionMember.is_deleted))", + back_populates='files', + ) print_facts: Mapped[list[PrintFact]] = relationship('PrintFact', back_populates='file') @property @@ -88,6 +92,10 @@ class PrintFact(Model): owner_id: Mapped[int] = Column(Integer, ForeignKey('union_member.id'), nullable=False) created_at: Mapped[datetime] = Column(DateTime, nullable=False, default=datetime.utcnow) - owner: Mapped[UnionMember] = relationship('UnionMember', back_populates='print_facts') + owner: Mapped[UnionMember] = relationship( + 'UnionMember', + primaryjoin="and_(PrintFact.owner_id == UnionMember.id, not_(UnionMember.is_deleted))", + back_populates='print_facts', + ) file: Mapped[File] = relationship('File', back_populates='print_facts') sheets_used: Mapped[int] = Column(Integer) diff --git a/print_service/routes/file.py b/print_service/routes/file.py index 8cc5ab4..4878ca7 100644 --- a/print_service/routes/file.py +++ b/print_service/routes/file.py @@ -161,7 +161,6 @@ async def send(inp: SendInput, settings: Settings = Depends(get_settings)): 415: {'model': StatusResponseModel, 'detail': 'File error'}, 413: {'model': StatusResponseModel, 'detail': 'Too large file'}, 416: {'model': StatusResponseModel, 'detail': 'Invalid page request'}, - 410: {'model': StatusResponseModel, 'detail': 'User is deleted'}, }, response_model=SendOutput, ) @@ -185,9 +184,6 @@ async def upload_file( .order_by(FileModel.created_at.desc()) .one_or_none() ) - if file_model: - if file_model.owner.is_deleted: - raise UserIsDeleted() if not file_model: await file.close() @@ -239,7 +235,6 @@ async def upload_file( 404: {'model': StatusResponseModel, 'detail': 'Pin not found'}, 413: {'model': StatusResponseModel, 'detail': 'Too many pages'}, 416: {'model': StatusResponseModel, 'detail': 'Invalid page request'}, - 410: {'model': StatusResponseModel, 'detail': 'User is deleted'}, }, response_model=SendOutput, ) @@ -261,10 +256,6 @@ async def update_file_options( .one_or_none() ) - if file_model: - if file_model.owner.is_deleted: - raise UserIsDeleted() - print(options) if not file_model: raise PINNotFound(pin) diff --git a/tests/test_routes/test_file.py b/tests/test_routes/test_file.py index d9fdbf1..667b1f1 100644 --- a/tests/test_routes/test_file.py +++ b/tests/test_routes/test_file.py @@ -120,21 +120,6 @@ def test_file_check(): assert checking_for_pdf(open("tests/test_routes/test_files/correct.pdf", "rb").read()) == (True, 2) -def test_upload_is_deleted(pin_pdf, client, add_is_deleted_flag): - pin = pin_pdf - fileName = 'tests/test_routes/test_files/correct.pdf' - files = {'file': (f"{fileName}", open(f"{fileName}", 'rb'), "application/pdf")} - res = client.post(f"{url}/{pin}", files=files) - assert res.status_code == status.HTTP_410_GONE - - -def test_patch_is_deleted(pin_pdf, client, add_is_deleted_flag): - pin = pin_pdf - body = {"options": {"pages": "", "copies": 2, "two_sided": False}} - res = client.patch(f"{url}/{pin}", json=body) - assert res.status_code == status.HTTP_410_GONE - - def test_upload_and_print_correct_pdf(pin_pdf, client): pin = pin_pdf fileName = 'tests/test_routes/test_files/correct.pdf' From 64679c131a260110092b6d297a375fb6b86c64e0 Mon Sep 17 00:00:00 2001 From: gitfresnel <151745312+gitfresnel@users.noreply.github.com> Date: Wed, 30 Oct 2024 20:09:46 +0300 Subject: [PATCH 05/12] f --- migrations/env.py | 4 +-- ...4_add_is_deleted_field_to_unionmember_.py} | 8 ++--- print_service/exceptions.py | 4 --- print_service/models/__init__.py | 17 +++++------ print_service/models/base.py | 29 +++++++++++++++++++ print_service/routes/exc_handlers.py | 11 ------- print_service/routes/file.py | 12 +++----- print_service/routes/user.py | 24 +++++++-------- print_service/utils/__init__.py | 4 +-- tests/test_routes/conftest.py | 28 +++++++++++------- tests/test_routes/test_file.py | 6 ++-- tests/test_routes/test_user.py | 6 ++-- 12 files changed, 80 insertions(+), 73 deletions(-) rename migrations/versions/{120e06710249_add_is_deleted_field_to_unionmember_.py => 2b86076bf074_add_is_deleted_field_to_unionmember_.py} (75%) create mode 100644 print_service/models/base.py diff --git a/migrations/env.py b/migrations/env.py index 3b98615..65e0bc2 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -3,7 +3,7 @@ from alembic import context from sqlalchemy import engine_from_config, pool -from print_service.models import Model +from print_service.models.base import Base from print_service.settings import get_settings @@ -20,7 +20,7 @@ # for 'autogenerate' support # from myapp import mymodel # target_metadata = mymodel.Base.metadata -target_metadata = Model.metadata +target_metadata = Base.metadata # other values from the config, defined by the needs of env.py, # can be acquired: diff --git a/migrations/versions/120e06710249_add_is_deleted_field_to_unionmember_.py b/migrations/versions/2b86076bf074_add_is_deleted_field_to_unionmember_.py similarity index 75% rename from migrations/versions/120e06710249_add_is_deleted_field_to_unionmember_.py rename to migrations/versions/2b86076bf074_add_is_deleted_field_to_unionmember_.py index 808eda9..0c7f5a5 100644 --- a/migrations/versions/120e06710249_add_is_deleted_field_to_unionmember_.py +++ b/migrations/versions/2b86076bf074_add_is_deleted_field_to_unionmember_.py @@ -1,8 +1,8 @@ """Add is_deleted field to UnionMember table -Revision ID: 120e06710249 +Revision ID: 2b86076bf074 Revises: a68c6bb2972c -Create Date: 2024-10-25 16:40:35.109586 +Create Date: 2024-10-30 19:08:15.473750 """ @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. -revision = '120e06710249' +revision = '2b86076bf074' down_revision = 'a68c6bb2972c' branch_labels = None depends_on = None @@ -21,8 +21,6 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### op.alter_column('file', 'source', existing_type=sa.VARCHAR(), nullable=False) op.add_column('union_member', sa.Column('is_deleted', sa.Boolean(), nullable=True)) - op.execute(f'UPDATE "union_member" SET is_deleted = False') - op.alter_column('union_member', sa.Column('is_deleted', sa.Boolean(), nullable=False)) # ### end Alembic commands ### diff --git a/print_service/exceptions.py b/print_service/exceptions.py index 259690f..99ed16a 100644 --- a/print_service/exceptions.py +++ b/print_service/exceptions.py @@ -12,10 +12,6 @@ class TerminalTokenNotFound(ObjectNotFound): pass -class UserIsDeleted(Exception): - pass - - class TerminalQRNotFound(ObjectNotFound): pass diff --git a/print_service/models/__init__.py b/print_service/models/__init__.py index 9573083..50049b9 100644 --- a/print_service/models/__init__.py +++ b/print_service/models/__init__.py @@ -9,14 +9,11 @@ from sqlalchemy.sql.schema import ForeignKey from sqlalchemy.sql.sqltypes import Boolean +from print_service.models.base import BaseDbModel -@as_declarative() -class Model: - pass - -class UnionMember(Model): - __tablename__ = 'union_member' +class UnionMember(BaseDbModel): + # __tablename__ = 'union_member' id: Mapped[int] = mapped_column(Integer, primary_key=True) surname: Mapped[str] = mapped_column(String, nullable=False) @@ -28,8 +25,8 @@ class UnionMember(Model): print_facts: Mapped[list[PrintFact]] = relationship('PrintFact', back_populates='owner') -class File(Model): - __tablename__ = 'file' +class File(BaseDbModel): + # __tablename__ = 'file' id: Mapped[int] = Column(Integer, primary_key=True) pin: Mapped[str] = Column(String, nullable=False) @@ -84,8 +81,8 @@ def sheets_count(self) -> int | None: return len(self.flatten_pages) * self.option_copies -class PrintFact(Model): - __tablename__ = 'print_fact' +class PrintFact(BaseDbModel): + # __tablename__ = 'print_fact' id: Mapped[int] = Column(Integer, primary_key=True) file_id: Mapped[int] = Column(Integer, ForeignKey('file.id'), nullable=False) diff --git a/print_service/models/base.py b/print_service/models/base.py new file mode 100644 index 0000000..d663434 --- /dev/null +++ b/print_service/models/base.py @@ -0,0 +1,29 @@ +import re + +import sqlalchemy +from sqlalchemy import Integer, not_ +from sqlalchemy.orm import Mapped, Query, Session, as_declarative, declared_attr, mapped_column + + +@as_declarative() +class Base: + """Base class for all database entities""" + + @declared_attr + def __tablename__(cls) -> str: # pylint: disable=no-self-argument + """Generate database table name automatically. + Convert CamelCase class name to snake_case db table name. + """ + return re.sub(r"(? Query: + objs = session.query(cls) + if not with_deleted and hasattr(cls, "is_deleted"): + objs = objs.filter(not_(cls.is_deleted)) + return objs diff --git a/print_service/routes/exc_handlers.py b/print_service/routes/exc_handlers.py index 8661f8f..ecb8aeb 100644 --- a/print_service/routes/exc_handlers.py +++ b/print_service/routes/exc_handlers.py @@ -20,7 +20,6 @@ TooManyPages, UnionStudentDuplicate, UnprocessableFileInstance, - UserIsDeleted, UserNotFound, ) from print_service.routes.base import app @@ -76,16 +75,6 @@ async def terminal_not_found_by_qr(req: starlette.requests.Request, exc: Termina ) -@app.exception_handler(UserIsDeleted) -async def user_is_deleted(req: starlette.requests.Request, exc: TerminalTokenNotFound): - return JSONResponse( - content=StatusResponseModel( - status="Error", message="User is deleted", ru="Пользователь удалён из базы" - ).model_dump(), - status_code=410, - ) - - @app.exception_handler(TerminalTokenNotFound) async def terminal_not_found_by_token(req: starlette.requests.Request, exc: TerminalTokenNotFound): return JSONResponse( diff --git a/print_service/routes/file.py b/print_service/routes/file.py index 4878ca7..47bbf4e 100644 --- a/print_service/routes/file.py +++ b/print_service/routes/file.py @@ -25,7 +25,6 @@ TooLargeSize, TooManyPages, UnprocessableFileInstance, - UserIsDeleted, UserNotFound, ) from print_service.models import File as FileModel @@ -106,7 +105,6 @@ class ReceiveOutput(BaseModel): responses={ 403: {'model': StatusResponseModel, 'detail': 'User error'}, 500: {'model': StatusResponseModel, 'detail': 'PIN generate error'}, - 410: {'model': StatusResponseModel, 'detail': 'User is deleted'}, }, response_model=SendOutput, ) @@ -115,7 +113,7 @@ async def send(inp: SendInput, settings: Settings = Depends(get_settings)): Полученный пин-код можно использовать в методах POST и GET `/file/{pin}`. """ - user = db.session.query(UnionMember) + user = UnionMember.query(session=db.session) if not settings.ALLOW_STUDENT_NUMBER: user = user.filter(UnionMember.union_number != None) user = user.filter( @@ -125,9 +123,7 @@ async def send(inp: SendInput, settings: Settings = Depends(get_settings)): ), func.upper(UnionMember.surname) == inp.surname.upper(), ).one_or_none() - if user: - if user.is_deleted: - raise UserIsDeleted() + if not user: raise NotInUnion() try: @@ -179,7 +175,7 @@ async def upload_file( if file == ...: raise FileIsNotReceived() file_model = ( - db.session.query(FileModel) + FileModel.query(session=db.session) .filter(func.upper(FileModel.pin) == pin.upper()) .order_by(FileModel.created_at.desc()) .one_or_none() @@ -250,7 +246,7 @@ async def update_file_options( можно бесконечное количество раз. Можно изменять настройки по одной.""" options = inp.options.model_dump(exclude_unset=True) file_model = ( - db.session.query(FileModel) + FileModel.query(session=db.session) .filter(func.upper(FileModel.pin) == pin.upper()) .order_by(FileModel.created_at.desc()) .one_or_none() diff --git a/print_service/routes/user.py b/print_service/routes/user.py index 5f0dd15..57b305a 100644 --- a/print_service/routes/user.py +++ b/print_service/routes/user.py @@ -9,7 +9,7 @@ from sqlalchemy import and_, func, or_ from print_service import __version__ -from print_service.exceptions import UnionStudentDuplicate, UserIsDeleted, UserNotFound +from print_service.exceptions import UnionStudentDuplicate, UserNotFound from print_service.models import UnionMember from print_service.schema import BaseModel from print_service.settings import get_settings @@ -40,7 +40,7 @@ class UpdateUserList(BaseModel): @router.get( '/is_union_member', status_code=202, - responses={404: {'detail': 'User not found'}, 410: {'detail': 'User is deleted'}}, + responses={404: {'detail': 'User not found'}}, ) async def check_union_member( surname: constr(strip_whitespace=True, to_upper=True, min_length=1), @@ -49,7 +49,7 @@ async def check_union_member( ): """Проверяет наличие пользователя в списке.""" surname = surname.upper() - user = db.session.query(UnionMember) + user = UnionMember.query(session=db.session) if not settings.ALLOW_STUDENT_NUMBER: user = user.filter(UnionMember.union_number != None) user: UnionMember = user.filter( @@ -59,9 +59,7 @@ async def check_union_member( ), func.upper(UnionMember.surname) == surname, ).one_or_none() - if user: - if user.is_deleted: - raise UserIsDeleted() + if v == '1': return bool(user) @@ -94,7 +92,7 @@ def update_list( for user in input.users: db_user: UnionMember = ( - db.session.query(UnionMember) + UnionMember.query(session=db.session, with_deleted=True) .filter( or_( and_( @@ -112,13 +110,11 @@ def update_list( if db_user: if db_user.is_deleted: - raise UserIsDeleted() - - if db_user: - db_user.surname = user.username - db_user.union_number = user.union_number - db_user.student_number = user.student_number - db_user.is_deleted = False + raise UserNotFound + else: + db_user.surname = user.username + db_user.union_number = user.union_number + db_user.student_number = user.student_number else: db.session.add( UnionMember( diff --git a/print_service/utils/__init__.py b/print_service/utils/__init__.py index 863cf2d..60a160a 100644 --- a/print_service/utils/__init__.py +++ b/print_service/utils/__init__.py @@ -31,7 +31,7 @@ def generate_pin(session: Session): for i in range(15): pin = ''.join(random.choice(settings.PIN_SYMBOLS) for _ in range(settings.PIN_LENGTH)) cnt = ( - session.query(File) + File.query(session=session) .filter( File.pin == pin, File.created_at + timedelta(hours=settings.STORAGE_TIME) >= datetime.utcnow(), @@ -57,7 +57,7 @@ def generate_filename(original_filename: str): def get_file(dbsession, pin: str or list[str]): pin = [pin.upper()] if isinstance(pin, str) else tuple(p.upper() for p in pin) files: list[FileModel] = ( - dbsession.query(FileModel) + FileModel.query(session=dbsession) .filter(func.upper(FileModel.pin).in_(pin)) .order_by(FileModel.created_at.desc()) .all() diff --git a/tests/test_routes/conftest.py b/tests/test_routes/conftest.py index 2575c66..7c4327a 100644 --- a/tests/test_routes/conftest.py +++ b/tests/test_routes/conftest.py @@ -16,16 +16,22 @@ def union_member_user(dbsession): dbsession.add(UnionMember(**union_member)) dbsession.commit() yield union_member - db_user = dbsession.query(UnionMember).filter(UnionMember.id == union_member['id']).one_or_none() + db_user = ( + UnionMember.query(session=dbsession, with_deleted=True) + .filter(UnionMember.id == union_member['id']) + .one_or_none() + ) assert db_user is not None - dbsession.query(PrintFact).filter(PrintFact.owner_id == union_member['id']).delete() - dbsession.query(UnionMember).filter(UnionMember.id == union_member['id']).delete() + PrintFact.query(session=dbsession).filter(PrintFact.owner_id == union_member['id']).delete() + UnionMember.query(session=dbsession, with_deleted=True).filter( + UnionMember.id == union_member['id'] + ).delete() dbsession.commit() @pytest.fixture(scope='function') def add_is_deleted_flag(dbsession): - db_user = dbsession.query(UnionMember).filter(UnionMember.id == 42).one_or_none() + db_user = UnionMember.query(session=dbsession).filter(UnionMember.id == 42).one_or_none() db_user.is_deleted = True dbsession.commit() @@ -39,12 +45,12 @@ def uploaded_file_db(dbsession, union_member_user, client): "options": {"pages": "", "copies": 1, "two_sided": False}, } res = client.post('/file', json=body) - db_file = dbsession.query(File).filter(File.pin == res.json()['pin']).one_or_none() + db_file = File.query(session=dbsession).filter(File.pin == res.json()['pin']).one_or_none() yield db_file - file = dbsession.query(File).filter(File.pin == res.json()['pin']).one_or_none() + file = File.query(session=dbsession).filter(File.pin == res.json()['pin']).one_or_none() assert file is not None - dbsession.query(PrintFact).filter(PrintFact.file_id == file.id).delete() - dbsession.query(File).filter(File.pin == res.json()['pin']).delete() + PrintFact.query(session=dbsession).filter(PrintFact.file_id == file.id).delete() + File.query(session=dbsession).filter(File.pin == res.json()['pin']).delete() dbsession.commit() @@ -67,8 +73,8 @@ def pin_pdf(dbsession, union_member_user, client): res = client.post('/file', json=body) pin = res.json()['pin'] yield pin - file = dbsession.query(File).filter(File.pin == res.json()['pin']).one_or_none() + file = File.query(session=dbsession).filter(File.pin == res.json()['pin']).one_or_none() assert file is not None - dbsession.query(PrintFact).filter(PrintFact.file_id == file.id).delete() - dbsession.query(File).filter(File.pin == res.json()['pin']).delete() + PrintFact.query(session=dbsession).filter(PrintFact.file_id == file.id).delete() + File.query(session=dbsession).filter(File.pin == res.json()['pin']).delete() dbsession.commit() diff --git a/tests/test_routes/test_file.py b/tests/test_routes/test_file.py index 667b1f1..d72f8b5 100644 --- a/tests/test_routes/test_file.py +++ b/tests/test_routes/test_file.py @@ -25,7 +25,7 @@ def test_post_success(union_member_user, client, dbsession): } res = client.post(url, data=json.dumps(body)) assert res.status_code == status.HTTP_200_OK - db_file = dbsession.query(File).filter(File.pin == res.json()['pin']).one_or_none() + db_file = File.query(session=dbsession).filter(File.pin == res.json()['pin']).one_or_none() assert db_file is not None assert db_file.source == 'webapp' body2 = { @@ -36,7 +36,7 @@ def test_post_success(union_member_user, client, dbsession): } res2 = client.post(url, data=json.dumps(body2)) assert res2.status_code == status.HTTP_200_OK - db_file2 = dbsession.query(File).filter(File.pin == res2.json()['pin']).one_or_none() + db_file2 = File.query(session=dbsession).filter(File.pin == res2.json()['pin']).one_or_none() assert db_file2 is not None assert db_file2.source == 'unknown' dbsession.delete(db_file) @@ -53,7 +53,7 @@ def test_post_is_deleted(client, union_member_user, add_is_deleted_flag): "options": {"pages": "", "copies": 1, "two_sided": False}, } res = client.post(url, data=json.dumps(body)) - assert res.status_code == status.HTTP_410_GONE + assert res.status_code == status.HTTP_403_FORBIDDEN def test_post_unauthorized_user(client): diff --git a/tests/test_routes/test_user.py b/tests/test_routes/test_user.py index 9f9fcff..1b69579 100644 --- a/tests/test_routes/test_user.py +++ b/tests/test_routes/test_user.py @@ -35,7 +35,7 @@ def test_get_is_deleted(client, union_member_user, add_is_deleted_flag): 'number': '6666667', } res = client.get(url, params=params) - assert res.status_code == status.HTTP_410_GONE + assert res.status_code == status.HTTP_404_NOT_FOUND def test_post_success(client, dbsession): @@ -50,7 +50,7 @@ def test_post_success(client, dbsession): } res = client.post(url, data=json.dumps(body)) assert res.status_code == status.HTTP_200_OK - dbsession.query(UnionMember).filter( + UnionMember.query(session=dbsession).filter( UnionMember.surname == body['users'][0]['username'], UnionMember.union_number == body['users'][0]['union_number'], UnionMember.student_number == body['users'][0]['student_number'], @@ -69,7 +69,7 @@ def test_post_is_deleted(client, union_member_user, add_is_deleted_flag): ], } res = client.post(url, data=json.dumps(body)) - assert res.status_code == status.HTTP_410_GONE + assert res.status_code == status.HTTP_404_NOT_FOUND @pytest.mark.parametrize( From 6becd9e1ae22b31b8d685c8bdb104c4a414d69af Mon Sep 17 00:00:00 2001 From: gitfresnel <151745312+gitfresnel@users.noreply.github.com> Date: Wed, 6 Nov 2024 00:12:08 +0300 Subject: [PATCH 06/12] f --- ...74_add_is_deleted_field_to_unionmember_.py | 3 +- tests/test_routes/test_user.py | 57 +++++++++++++++++-- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/migrations/versions/2b86076bf074_add_is_deleted_field_to_unionmember_.py b/migrations/versions/2b86076bf074_add_is_deleted_field_to_unionmember_.py index 0c7f5a5..c0ac071 100644 --- a/migrations/versions/2b86076bf074_add_is_deleted_field_to_unionmember_.py +++ b/migrations/versions/2b86076bf074_add_is_deleted_field_to_unionmember_.py @@ -18,9 +18,10 @@ def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### op.alter_column('file', 'source', existing_type=sa.VARCHAR(), nullable=False) op.add_column('union_member', sa.Column('is_deleted', sa.Boolean(), nullable=True)) + op.execute(f'UPDATE "union_member" SET is_deleted = False') + op.alter_column('union_member', 'is_deleted', nullable=False) # ### end Alembic commands ### diff --git a/tests/test_routes/test_user.py b/tests/test_routes/test_user.py index 1b69579..5dfe97a 100644 --- a/tests/test_routes/test_user.py +++ b/tests/test_routes/test_user.py @@ -5,6 +5,7 @@ from print_service.models import UnionMember from print_service.settings import get_settings +from sqlalchemy import and_, func url: str = '/is_union_member' @@ -46,14 +47,14 @@ def test_post_success(client, dbsession): 'union_number': '1966', 'student_number': '1967', } - ], + ] } res = client.post(url, data=json.dumps(body)) assert res.status_code == status.HTTP_200_OK - UnionMember.query(session=dbsession).filter( - UnionMember.surname == body['users'][0]['username'], - UnionMember.union_number == body['users'][0]['union_number'], - UnionMember.student_number == body['users'][0]['student_number'], + UnionMember.query(session=dbsession).filter(and_( + UnionMember.surname == func.upper(body['users'][0]['username']), + UnionMember.union_number == func.upper(body['users'][0]['union_number']), + UnionMember.student_number == func.upper(body['users'][0]['student_number'])) ).delete() dbsession.commit() @@ -66,12 +67,56 @@ def test_post_is_deleted(client, union_member_user, add_is_deleted_flag): 'union_number': '6666667', 'student_number': '13033224', } - ], + ] } res = client.post(url, data=json.dumps(body)) assert res.status_code == status.HTTP_404_NOT_FOUND +def test_restore_is_deleted(client, dbsession): + user = UnionMember(id=5, + surname='test_user', + union_number='123', + student_number='56', + is_deleted=False) + dbsession.add(user) + dbsession.commit() + + body = { + 'users': [ + { + 'username': 'test_user', + 'union_number': '123', + 'student_number': '56', + 'is_deleted': True + } + ] + } + _ = client.post(url, data=json.dumps(body)) + res = UnionMember.query(session=dbsession, with_deleted=True).filter(UnionMember.id == 5).one_or_none() + assert res.is_deleted is False + user.is_deleted = True + dbsession.commit() + body = { + 'users': [ + { + 'username': 'test_user', + 'union_number': '123', + 'student_number': '56', + 'is_deleted': False + } + ] + } + res = client.post(url, data=json.dumps(body)) + assert res.status_code == status.HTTP_404_NOT_FOUND + UnionMember.query(session=dbsession, with_deleted=True).filter(and_( + UnionMember.surname == func.upper(body['users'][0]['username']), + UnionMember.union_number == func.upper(body['users'][0]['union_number']), + UnionMember.student_number == func.upper(body['users'][0]['student_number']) + )).delete() + dbsession.commit() + + @pytest.mark.parametrize( 'users', [ From 897ad477b29e406d640b574ab6ca5e501a39ae25 Mon Sep 17 00:00:00 2001 From: gitfresnel <151745312+gitfresnel@users.noreply.github.com> Date: Wed, 6 Nov 2024 00:13:46 +0300 Subject: [PATCH 07/12] f --- tests/test_routes/test_user.py | 50 +++++++++++++++------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/tests/test_routes/test_user.py b/tests/test_routes/test_user.py index 5dfe97a..52bca80 100644 --- a/tests/test_routes/test_user.py +++ b/tests/test_routes/test_user.py @@ -1,11 +1,11 @@ import json import pytest +from sqlalchemy import and_, func from starlette import status from print_service.models import UnionMember from print_service.settings import get_settings -from sqlalchemy import and_, func url: str = '/is_union_member' @@ -51,10 +51,12 @@ def test_post_success(client, dbsession): } res = client.post(url, data=json.dumps(body)) assert res.status_code == status.HTTP_200_OK - UnionMember.query(session=dbsession).filter(and_( - UnionMember.surname == func.upper(body['users'][0]['username']), - UnionMember.union_number == func.upper(body['users'][0]['union_number']), - UnionMember.student_number == func.upper(body['users'][0]['student_number'])) + UnionMember.query(session=dbsession).filter( + and_( + UnionMember.surname == func.upper(body['users'][0]['username']), + UnionMember.union_number == func.upper(body['users'][0]['union_number']), + UnionMember.student_number == func.upper(body['users'][0]['student_number']), + ) ).delete() dbsession.commit() @@ -74,46 +76,38 @@ def test_post_is_deleted(client, union_member_user, add_is_deleted_flag): def test_restore_is_deleted(client, dbsession): - user = UnionMember(id=5, - surname='test_user', - union_number='123', - student_number='56', - is_deleted=False) + user = UnionMember( + id=5, surname='test_user', union_number='123', student_number='56', is_deleted=False + ) dbsession.add(user) dbsession.commit() body = { 'users': [ - { - 'username': 'test_user', - 'union_number': '123', - 'student_number': '56', - 'is_deleted': True - } + {'username': 'test_user', 'union_number': '123', 'student_number': '56', 'is_deleted': True} ] } _ = client.post(url, data=json.dumps(body)) - res = UnionMember.query(session=dbsession, with_deleted=True).filter(UnionMember.id == 5).one_or_none() + res = ( + UnionMember.query(session=dbsession, with_deleted=True).filter(UnionMember.id == 5).one_or_none() + ) assert res.is_deleted is False user.is_deleted = True dbsession.commit() body = { 'users': [ - { - 'username': 'test_user', - 'union_number': '123', - 'student_number': '56', - 'is_deleted': False - } + {'username': 'test_user', 'union_number': '123', 'student_number': '56', 'is_deleted': False} ] } res = client.post(url, data=json.dumps(body)) assert res.status_code == status.HTTP_404_NOT_FOUND - UnionMember.query(session=dbsession, with_deleted=True).filter(and_( - UnionMember.surname == func.upper(body['users'][0]['username']), - UnionMember.union_number == func.upper(body['users'][0]['union_number']), - UnionMember.student_number == func.upper(body['users'][0]['student_number']) - )).delete() + UnionMember.query(session=dbsession, with_deleted=True).filter( + and_( + UnionMember.surname == func.upper(body['users'][0]['username']), + UnionMember.union_number == func.upper(body['users'][0]['union_number']), + UnionMember.student_number == func.upper(body['users'][0]['student_number']), + ) + ).delete() dbsession.commit() From 9fdb0e8508c1f0fc4a9dd5f880586ede6129e587 Mon Sep 17 00:00:00 2001 From: gitfresnel <151745312+gitfresnel@users.noreply.github.com> Date: Thu, 7 Nov 2024 21:45:53 +0300 Subject: [PATCH 08/12] Delete migrations/versions/2b86076bf074_add_is_deleted_field_to_unionmember_.py --- ...74_add_is_deleted_field_to_unionmember_.py | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 migrations/versions/2b86076bf074_add_is_deleted_field_to_unionmember_.py diff --git a/migrations/versions/2b86076bf074_add_is_deleted_field_to_unionmember_.py b/migrations/versions/2b86076bf074_add_is_deleted_field_to_unionmember_.py deleted file mode 100644 index c0ac071..0000000 --- a/migrations/versions/2b86076bf074_add_is_deleted_field_to_unionmember_.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Add is_deleted field to UnionMember table - -Revision ID: 2b86076bf074 -Revises: a68c6bb2972c -Create Date: 2024-10-30 19:08:15.473750 - -""" - -import sqlalchemy as sa -from alembic import op - - -# revision identifiers, used by Alembic. -revision = '2b86076bf074' -down_revision = 'a68c6bb2972c' -branch_labels = None -depends_on = None - - -def upgrade(): - op.alter_column('file', 'source', existing_type=sa.VARCHAR(), nullable=False) - op.add_column('union_member', sa.Column('is_deleted', sa.Boolean(), nullable=True)) - op.execute(f'UPDATE "union_member" SET is_deleted = False') - op.alter_column('union_member', 'is_deleted', nullable=False) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('union_member', 'is_deleted') - op.alter_column('file', 'source', existing_type=sa.VARCHAR(), nullable=True) - # ### end Alembic commands ### From 3cb478415ff98c930be7aa4e7b9ce853d9419b2a Mon Sep 17 00:00:00 2001 From: gitfresnel <151745312+gitfresnel@users.noreply.github.com> Date: Thu, 7 Nov 2024 21:47:00 +0300 Subject: [PATCH 09/12] f --- ...f98fc21f5_add_is_deleted_field_to_unionmember.py} | 12 +++++------- print_service/models/base.py | 5 ++--- print_service/routes/user.py | 1 - 3 files changed, 7 insertions(+), 11 deletions(-) rename migrations/versions/{2b86076bf074_add_is_deleted_field_to_unionmember_.py => 808f98fc21f5_add_is_deleted_field_to_unionmember.py} (70%) diff --git a/migrations/versions/2b86076bf074_add_is_deleted_field_to_unionmember_.py b/migrations/versions/808f98fc21f5_add_is_deleted_field_to_unionmember.py similarity index 70% rename from migrations/versions/2b86076bf074_add_is_deleted_field_to_unionmember_.py rename to migrations/versions/808f98fc21f5_add_is_deleted_field_to_unionmember.py index c0ac071..973e10f 100644 --- a/migrations/versions/2b86076bf074_add_is_deleted_field_to_unionmember_.py +++ b/migrations/versions/808f98fc21f5_add_is_deleted_field_to_unionmember.py @@ -1,8 +1,8 @@ -"""Add is_deleted field to UnionMember table +"""Add is_deleted field to UnionMember -Revision ID: 2b86076bf074 +Revision ID: 808f98fc21f5 Revises: a68c6bb2972c -Create Date: 2024-10-30 19:08:15.473750 +Create Date: 2024-11-07 21:35:48.483333 """ @@ -11,22 +11,20 @@ # revision identifiers, used by Alembic. -revision = '2b86076bf074' +revision = '808f98fc21f5' down_revision = 'a68c6bb2972c' branch_labels = None depends_on = None def upgrade(): + op.alter_column('file', 'source', existing_type=sa.VARCHAR(), nullable=False) op.add_column('union_member', sa.Column('is_deleted', sa.Boolean(), nullable=True)) op.execute(f'UPDATE "union_member" SET is_deleted = False') op.alter_column('union_member', 'is_deleted', nullable=False) - # ### end Alembic commands ### def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### op.drop_column('union_member', 'is_deleted') op.alter_column('file', 'source', existing_type=sa.VARCHAR(), nullable=True) - # ### end Alembic commands ### diff --git a/print_service/models/base.py b/print_service/models/base.py index d663434..9cccd25 100644 --- a/print_service/models/base.py +++ b/print_service/models/base.py @@ -1,8 +1,8 @@ import re import sqlalchemy -from sqlalchemy import Integer, not_ -from sqlalchemy.orm import Mapped, Query, Session, as_declarative, declared_attr, mapped_column +from sqlalchemy import not_ +from sqlalchemy.orm import Query, Session, as_declarative, declared_attr @as_declarative() @@ -19,7 +19,6 @@ def __tablename__(cls) -> str: # pylint: disable=no-self-argument class BaseDbModel(Base): __abstract__ = True - id: Mapped[int] = mapped_column(Integer, primary_key=True) @classmethod def query(cls, session: Session, with_deleted: bool = False) -> Query: diff --git a/print_service/routes/user.py b/print_service/routes/user.py index 57b305a..90231a0 100644 --- a/print_service/routes/user.py +++ b/print_service/routes/user.py @@ -121,7 +121,6 @@ def update_list( surname=user.username, union_number=user.union_number, student_number=user.student_number, - is_deleted=False, ) ) db.session.flush() From 4a8981a2823de3b6f4794b313018f61002a739fa Mon Sep 17 00:00:00 2001 From: gitfresnel <151745312+gitfresnel@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:19:53 +0300 Subject: [PATCH 10/12] Delete migrations/versions/808f98fc21f5_add_is_deleted_field_to_unionmember.py --- ...1f5_add_is_deleted_field_to_unionmember.py | 30 ------------------- 1 file changed, 30 deletions(-) delete mode 100644 migrations/versions/808f98fc21f5_add_is_deleted_field_to_unionmember.py diff --git a/migrations/versions/808f98fc21f5_add_is_deleted_field_to_unionmember.py b/migrations/versions/808f98fc21f5_add_is_deleted_field_to_unionmember.py deleted file mode 100644 index 973e10f..0000000 --- a/migrations/versions/808f98fc21f5_add_is_deleted_field_to_unionmember.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Add is_deleted field to UnionMember - -Revision ID: 808f98fc21f5 -Revises: a68c6bb2972c -Create Date: 2024-11-07 21:35:48.483333 - -""" - -import sqlalchemy as sa -from alembic import op - - -# revision identifiers, used by Alembic. -revision = '808f98fc21f5' -down_revision = 'a68c6bb2972c' -branch_labels = None -depends_on = None - - -def upgrade(): - - op.alter_column('file', 'source', existing_type=sa.VARCHAR(), nullable=False) - op.add_column('union_member', sa.Column('is_deleted', sa.Boolean(), nullable=True)) - op.execute(f'UPDATE "union_member" SET is_deleted = False') - op.alter_column('union_member', 'is_deleted', nullable=False) - - -def downgrade(): - op.drop_column('union_member', 'is_deleted') - op.alter_column('file', 'source', existing_type=sa.VARCHAR(), nullable=True) From 77bca1ab141524d0162192cda4224130684efb64 Mon Sep 17 00:00:00 2001 From: gitfresnel <151745312+gitfresnel@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:51:13 +0300 Subject: [PATCH 11/12] f --- ...d4_add_is_deleted_field_to_unionmember.py} | 14 +++-- print_service/models/__init__.py | 2 +- print_service/models/base.py | 51 ++++++++++++++++++- print_service/routes/file.py | 18 ++++--- print_service/routes/user.py | 18 ++----- print_service/utils/__init__.py | 5 +- tests/test_routes/test_user.py | 30 ++++++----- 7 files changed, 92 insertions(+), 46 deletions(-) rename migrations/versions/{808f98fc21f5_add_is_deleted_field_to_unionmember.py => c29b6ffbfed4_add_is_deleted_field_to_unionmember.py} (50%) diff --git a/migrations/versions/808f98fc21f5_add_is_deleted_field_to_unionmember.py b/migrations/versions/c29b6ffbfed4_add_is_deleted_field_to_unionmember.py similarity index 50% rename from migrations/versions/808f98fc21f5_add_is_deleted_field_to_unionmember.py rename to migrations/versions/c29b6ffbfed4_add_is_deleted_field_to_unionmember.py index 973e10f..204f4cf 100644 --- a/migrations/versions/808f98fc21f5_add_is_deleted_field_to_unionmember.py +++ b/migrations/versions/c29b6ffbfed4_add_is_deleted_field_to_unionmember.py @@ -1,8 +1,8 @@ """Add is_deleted field to UnionMember -Revision ID: 808f98fc21f5 +Revision ID: c29b6ffbfed4 Revises: a68c6bb2972c -Create Date: 2024-11-07 21:35:48.483333 +Create Date: 2024-11-22 17:50:35.569723 """ @@ -11,18 +11,16 @@ # revision identifiers, used by Alembic. -revision = '808f98fc21f5' +revision = 'c29b6ffbfed4' down_revision = 'a68c6bb2972c' branch_labels = None depends_on = None def upgrade(): - - op.alter_column('file', 'source', existing_type=sa.VARCHAR(), nullable=False) - op.add_column('union_member', sa.Column('is_deleted', sa.Boolean(), nullable=True)) - op.execute(f'UPDATE "union_member" SET is_deleted = False') - op.alter_column('union_member', 'is_deleted', nullable=False) + op.add_column( + 'union_member', sa.Column('is_deleted', sa.Boolean(), nullable=False, server_default=sa.false()) + ) def downgrade(): diff --git a/print_service/models/__init__.py b/print_service/models/__init__.py index 50049b9..f62fc63 100644 --- a/print_service/models/__init__.py +++ b/print_service/models/__init__.py @@ -19,7 +19,7 @@ class UnionMember(BaseDbModel): surname: Mapped[str] = mapped_column(String, nullable=False) union_number: Mapped[str] = mapped_column(String, nullable=True) student_number: Mapped[str] = mapped_column(String, nullable=True) - is_deleted: Mapped[bool] = mapped_column(Boolean, nullable=True, default=False) + is_deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) files: Mapped[list[File]] = relationship('File', back_populates='owner') print_facts: Mapped[list[PrintFact]] = relationship('PrintFact', back_populates='owner') diff --git a/print_service/models/base.py b/print_service/models/base.py index 9cccd25..b307a67 100644 --- a/print_service/models/base.py +++ b/print_service/models/base.py @@ -1,9 +1,13 @@ +from __future__ import annotations + import re -import sqlalchemy from sqlalchemy import not_ +from sqlalchemy.exc import NoResultFound from sqlalchemy.orm import Query, Session, as_declarative, declared_attr +from print_service.exceptions import ObjectNotFound + @as_declarative() class Base: @@ -16,13 +20,56 @@ def __tablename__(cls) -> str: # pylint: disable=no-self-argument """ return re.sub(r"(? Query: + def create(cls, *, session: Session, **kwargs) -> BaseDbModel: + obj = cls(**kwargs) + session.add(obj) + session.flush() + return obj + + @classmethod + def query(cls, *, session: Session, with_deleted: bool = False) -> Query: + """Get all objects with soft deletes""" objs = session.query(cls) if not with_deleted and hasattr(cls, "is_deleted"): objs = objs.filter(not_(cls.is_deleted)) return objs + + @classmethod + def get(cls, id: int, *, with_deleted=False, session: Session) -> BaseDbModel: + """Get object with soft deletes""" + objs = session.query(cls) + if not with_deleted and hasattr(cls, "is_deleted"): + objs = objs.filter(not_(cls.is_deleted)) + try: + return objs.filter(cls.id == id).one() + except NoResultFound: + raise ObjectNotFound(cls, id) + + @classmethod + def update(cls, id: int, *, session: Session, **kwargs) -> BaseDbModel: + obj = cls.get(id, session=session) + for k, v in kwargs.items(): + setattr(obj, k, v) + session.flush() + return obj + + @classmethod + def delete(cls, id: int, *, session: Session) -> None: + """Soft delete object if possible, else hard delete""" + obj = cls.get(id, session=session) + if hasattr(obj, "is_deleted"): + obj.is_deleted = True + else: + session.delete(obj) + session.flush() diff --git a/print_service/routes/file.py b/print_service/routes/file.py index 47bbf4e..f2c108d 100644 --- a/print_service/routes/file.py +++ b/print_service/routes/file.py @@ -131,14 +131,18 @@ async def send(inp: SendInput, settings: Settings = Depends(get_settings)): except RuntimeError: raise PINGenerateError() filename = generate_filename(inp.filename) - file_model = FileModel(pin=pin, file=filename, source=inp.source) - file_model.owner = user - file_model.option_copies = inp.options.copies - file_model.option_pages = inp.options.pages - file_model.option_two_sided = inp.options.two_sided - db.session.add(file_model) - db.session.commit() + file_model = FileModel.create( + session=db.session, + pin=pin, + file=filename, + source=inp.source, + owner=user, + option_copies=inp.options.copies, + option_pages=inp.options.pages, + option_two_sided=inp.options.two_sided, + ) + db.session.commit() return { 'pin': file_model.pin, 'options': { diff --git a/print_service/routes/user.py b/print_service/routes/user.py index 90231a0..0795884 100644 --- a/print_service/routes/user.py +++ b/print_service/routes/user.py @@ -22,7 +22,7 @@ # region schemas class UserCreate(BaseModel): - username: constr(strip_whitespace=True, to_upper=True, min_length=1) + surname: constr(strip_whitespace=True, to_upper=True, min_length=1) union_number: Optional[constr(strip_whitespace=True, to_upper=True, min_length=1)] student_number: Optional[constr(strip_whitespace=True, to_upper=True, min_length=1)] @@ -112,19 +112,11 @@ def update_list( if db_user.is_deleted: raise UserNotFound else: - db_user.surname = user.username - db_user.union_number = user.union_number - db_user.student_number = user.student_number - else: - db.session.add( - UnionMember( - surname=user.username, - union_number=user.union_number, - student_number=user.student_number, + UnionMember.update( + session=db.session, id=db_user.id, **user.model_dump(exclude_unset=False) ) - ) - db.session.flush() - + else: + UnionMember.create(session=db.session, **user.model_dump(exclude_unset=False)) db.session.commit() return {"status": "ok", "count": len(input.users)} diff --git a/print_service/utils/__init__.py b/print_service/utils/__init__.py index 60a160a..daa6e38 100644 --- a/print_service/utils/__init__.py +++ b/print_service/utils/__init__.py @@ -86,8 +86,9 @@ def get_file(dbsession, pin: str or list[str]): if f.flatten_pages: if number_of_pages > max(f.flatten_pages): raise InvalidPageRequest() - file_model = PrintFact(file_id=f.id, owner_id=f.owner_id, sheets_used=f.sheets_count) - dbsession.add(file_model) + PrintFact.create( + session=dbsession, file_id=f.id, owner_id=f.owner_id, sheets_used=f.sheets_count + ) dbsession.commit() return result diff --git a/tests/test_routes/test_user.py b/tests/test_routes/test_user.py index 52bca80..6e1249f 100644 --- a/tests/test_routes/test_user.py +++ b/tests/test_routes/test_user.py @@ -43,7 +43,7 @@ def test_post_success(client, dbsession): body = { 'users': [ { - 'username': 'paul', + 'surname': 'paul', 'union_number': '1966', 'student_number': '1967', } @@ -53,7 +53,7 @@ def test_post_success(client, dbsession): assert res.status_code == status.HTTP_200_OK UnionMember.query(session=dbsession).filter( and_( - UnionMember.surname == func.upper(body['users'][0]['username']), + UnionMember.surname == func.upper(body['users'][0]['surname']), UnionMember.union_number == func.upper(body['users'][0]['union_number']), UnionMember.student_number == func.upper(body['users'][0]['student_number']), ) @@ -65,7 +65,7 @@ def test_post_is_deleted(client, union_member_user, add_is_deleted_flag): body = { 'users': [ { - 'username': 'new_test', + 'surname': 'new_test', 'union_number': '6666667', 'student_number': '13033224', } @@ -76,15 +76,19 @@ def test_post_is_deleted(client, union_member_user, add_is_deleted_flag): def test_restore_is_deleted(client, dbsession): - user = UnionMember( - id=5, surname='test_user', union_number='123', student_number='56', is_deleted=False + user = UnionMember.create( + session=dbsession, + id=5, + surname='test_user', + union_number='123', + student_number='56', + is_deleted=False, ) - dbsession.add(user) dbsession.commit() body = { 'users': [ - {'username': 'test_user', 'union_number': '123', 'student_number': '56', 'is_deleted': True} + {'surname': 'test_user', 'union_number': '123', 'student_number': '56', 'is_deleted': True} ] } _ = client.post(url, data=json.dumps(body)) @@ -96,14 +100,14 @@ def test_restore_is_deleted(client, dbsession): dbsession.commit() body = { 'users': [ - {'username': 'test_user', 'union_number': '123', 'student_number': '56', 'is_deleted': False} + {'surname': 'test_user', 'union_number': '123', 'student_number': '56', 'is_deleted': False} ] } res = client.post(url, data=json.dumps(body)) assert res.status_code == status.HTTP_404_NOT_FOUND UnionMember.query(session=dbsession, with_deleted=True).filter( and_( - UnionMember.surname == func.upper(body['users'][0]['username']), + UnionMember.surname == func.upper(body['users'][0]['surname']), UnionMember.union_number == func.upper(body['users'][0]['union_number']), UnionMember.student_number == func.upper(body['users'][0]['student_number']), ) @@ -117,12 +121,12 @@ def test_restore_is_deleted(client, dbsession): pytest.param( [ { - 'username': 'paul', + 'surname': 'paul', 'union_number': '404man', 'student_number': '30311', }, { - 'username': 'marty', + 'surname': 'marty', 'union_number': '404man', 'student_number': '303112', }, @@ -132,12 +136,12 @@ def test_restore_is_deleted(client, dbsession): pytest.param( [ { - 'username': 'alice', + 'surname': 'alice', 'union_number': '500', 'student_number': '42', }, { - 'username': 'polly', + 'surname': 'polly', 'union_number': '503', 'student_number': '42', }, From f175205ceac7f166f900d0de905bee0bac368008 Mon Sep 17 00:00:00 2001 From: gitfresnel <151745312+gitfresnel@users.noreply.github.com> Date: Sun, 16 Feb 2025 11:19:14 +0300 Subject: [PATCH 12/12] f --- print_service/routes/user.py | 9 ++------ tests/test_routes/test_user.py | 38 +++------------------------------- 2 files changed, 5 insertions(+), 42 deletions(-) diff --git a/print_service/routes/user.py b/print_service/routes/user.py index 0795884..680ed82 100644 --- a/print_service/routes/user.py +++ b/print_service/routes/user.py @@ -92,7 +92,7 @@ def update_list( for user in input.users: db_user: UnionMember = ( - UnionMember.query(session=db.session, with_deleted=True) + UnionMember.query(session=db.session) .filter( or_( and_( @@ -109,12 +109,7 @@ def update_list( ) if db_user: - if db_user.is_deleted: - raise UserNotFound - else: - UnionMember.update( - session=db.session, id=db_user.id, **user.model_dump(exclude_unset=False) - ) + UnionMember.update(session=db.session, id=db_user.id, **user.model_dump(exclude_unset=False)) else: UnionMember.create(session=db.session, **user.model_dump(exclude_unset=False)) db.session.commit() diff --git a/tests/test_routes/test_user.py b/tests/test_routes/test_user.py index 6e1249f..4307429 100644 --- a/tests/test_routes/test_user.py +++ b/tests/test_routes/test_user.py @@ -61,7 +61,7 @@ def test_post_success(client, dbsession): dbsession.commit() -def test_post_is_deleted(client, union_member_user, add_is_deleted_flag): +def test_post_is_deleted(client, dbsession, union_member_user, add_is_deleted_flag): body = { 'users': [ { @@ -72,40 +72,8 @@ def test_post_is_deleted(client, union_member_user, add_is_deleted_flag): ] } res = client.post(url, data=json.dumps(body)) - assert res.status_code == status.HTTP_404_NOT_FOUND - - -def test_restore_is_deleted(client, dbsession): - user = UnionMember.create( - session=dbsession, - id=5, - surname='test_user', - union_number='123', - student_number='56', - is_deleted=False, - ) - dbsession.commit() - - body = { - 'users': [ - {'surname': 'test_user', 'union_number': '123', 'student_number': '56', 'is_deleted': True} - ] - } - _ = client.post(url, data=json.dumps(body)) - res = ( - UnionMember.query(session=dbsession, with_deleted=True).filter(UnionMember.id == 5).one_or_none() - ) - assert res.is_deleted is False - user.is_deleted = True - dbsession.commit() - body = { - 'users': [ - {'surname': 'test_user', 'union_number': '123', 'student_number': '56', 'is_deleted': False} - ] - } - res = client.post(url, data=json.dumps(body)) - assert res.status_code == status.HTTP_404_NOT_FOUND - UnionMember.query(session=dbsession, with_deleted=True).filter( + assert res.status_code == status.HTTP_200_OK + UnionMember.query(session=dbsession).filter( and_( UnionMember.surname == func.upper(body['users'][0]['surname']), UnionMember.union_number == func.upper(body['users'][0]['union_number']),