From 0118edfa5ee0b29398cc8989ab1cb6f9016fbb55 Mon Sep 17 00:00:00 2001 From: Wudext Date: Thu, 16 Mar 2023 14:59:41 +0300 Subject: [PATCH 01/10] Initial commit --- migrations/versions/660bb7891726_scopes.py | 30 +++++++++++++ services_backend/models/database.py | 8 ++++ services_backend/routes/base.py | 3 ++ services_backend/routes/category.py | 13 ++++-- services_backend/routes/models/category.py | 4 ++ services_backend/routes/models/scope.py | 16 +++++++ services_backend/routes/scope.py | 52 ++++++++++++++++++++++ 7 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 migrations/versions/660bb7891726_scopes.py create mode 100644 services_backend/routes/models/scope.py create mode 100644 services_backend/routes/scope.py diff --git a/migrations/versions/660bb7891726_scopes.py b/migrations/versions/660bb7891726_scopes.py new file mode 100644 index 0000000..382255d --- /dev/null +++ b/migrations/versions/660bb7891726_scopes.py @@ -0,0 +1,30 @@ +"""Scopes + +Revision ID: 660bb7891726 +Revises: 6a486347af93 +Create Date: 2023-03-16 14:38:26.163590 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '660bb7891726' +down_revision = '6a486347af93' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table('scope', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('category_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['category_id'], ['category.id'], ), + sa.PrimaryKeyConstraint('id') + ) + + +def downgrade(): + op.drop_table('scope') diff --git a/services_backend/models/database.py b/services_backend/models/database.py index ce4790a..c0b6cca 100644 --- a/services_backend/models/database.py +++ b/services_backend/models/database.py @@ -12,6 +12,7 @@ class Category(Base): name: Mapped[str] = mapped_column(String) type: Mapped[str] = mapped_column(String) buttons: Mapped[list[Button]] = relationship("Button", back_populates="category", foreign_keys="Button.category_id") + scopes: Mapped[list[Scope]] = relationship("Scope", back_populates="category") class Button(Base): @@ -23,3 +24,10 @@ class Button(Base): icon: Mapped[str] = mapped_column(String) link: Mapped[str] = mapped_column(String) type: Mapped[str] = mapped_column(String) + + +class Scope(Base): + id: Mapped[int] = mapped_column(Integer, primary_key=True) + name: Mapped[str] = mapped_column(String, nullable=True) + category_id: Mapped[int] = mapped_column(Integer, ForeignKey("category.id")) + category: Mapped[Category] = relationship("Category", back_populates="scopes", foreign_keys=[category_id]) diff --git a/services_backend/routes/base.py b/services_backend/routes/base.py index 05582ca..7709190 100644 --- a/services_backend/routes/base.py +++ b/services_backend/routes/base.py @@ -7,6 +7,7 @@ from .button import button from .category import category +from .scope import scope settings = get_settings() @@ -33,3 +34,5 @@ app.include_router(button, prefix='/category/{category_id}/button', tags=["Button"]) app.include_router(category, prefix='/category', tags=["Category"]) +app.include_router(scope, prefix='/scope', tags=["Scope"]) + diff --git a/services_backend/routes/category.py b/services_backend/routes/category.py index b81d55b..7429a8f 100644 --- a/services_backend/routes/category.py +++ b/services_backend/routes/category.py @@ -5,7 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query from fastapi_sqlalchemy import db -from ..models.database import Button, Category +from ..models.database import Button, Category, Scope from .models.category import CategoryCreate, CategoryGet, CategoryUpdate @@ -41,7 +41,10 @@ def get_categories( Необходимые scopes: `-` """ - logger.info(f"User {user.get('id')} triggered get_categories") + if user is None: + logger.info("Unauthorised user triggered get_categories") + else: + logger.info(f"User {user.get('id')} triggered get_categories") return [ CategoryGet.from_orm(row).dict(exclude={"buttons"} if 'buttons' not in info else {}) for row in db.session.query(Category).order_by(Category.order).all() @@ -57,7 +60,10 @@ def get_category( Необходимые scopes: `-` """ - logger.info(f"User {user.get('id')} triggered get_category") + if user is None: + logger.info("Unauthorised user triggered get_category") + else: + logger.info(f"User {user.get('id')} triggered get_category") category = db.session.query(Category).filter(Category.id == category_id).one_or_none() if not category: raise HTTPException(status_code=404, detail="Category does not exist") @@ -66,6 +72,7 @@ def get_category( "order": category.order, "name": category.name, "type": category.type, + "scopes": category.scopes } diff --git a/services_backend/routes/models/category.py b/services_backend/routes/models/category.py index 0c48093..2a00729 100644 --- a/services_backend/routes/models/category.py +++ b/services_backend/routes/models/category.py @@ -1,16 +1,19 @@ from .base import Base from .button import ButtonGet +from .scope import ScopeGet class CategoryCreate(Base): type: str | None name: str | None + scopes: list[str] | None class CategoryUpdate(Base): order: int | None type: str | None name: str | None + scopes: list[str] | None class CategoryGet(Base): @@ -19,3 +22,4 @@ class CategoryGet(Base): type: str | None name: str | None buttons: list[ButtonGet] | None + scopes: list[ScopeGet] | None diff --git a/services_backend/routes/models/scope.py b/services_backend/routes/models/scope.py new file mode 100644 index 0000000..66b86ab --- /dev/null +++ b/services_backend/routes/models/scope.py @@ -0,0 +1,16 @@ +from .base import Base + + +class ScopeCreate(Base): + name: str + category_id: int | None + + +class ScopeGet(Base): + id: int + name: str + + +class ScopeUpdate(Base): + name: str | None + category_id: int | None diff --git a/services_backend/routes/scope.py b/services_backend/routes/scope.py new file mode 100644 index 0000000..45fe7e7 --- /dev/null +++ b/services_backend/routes/scope.py @@ -0,0 +1,52 @@ +from fastapi import HTTPException, APIRouter, Depends +from fastapi_sqlalchemy import db + +from auth_lib.fastapi import UnionAuth +from .models.scope import ScopeGet, ScopeCreate, ScopeUpdate +from ..models.database import Scope + +scope = APIRouter() + + +@scope.post("/", response_model=ScopeGet) +def create_scope(scope_inp: ScopeCreate, user=Depends(UnionAuth(['services.scope.create']))): + scope = Scope(**scope_inp.dict(exclude_none=True)) + db.session.add(scope) + db.session.commit() + return scope + + +@scope.get("/", response_model=list[ScopeGet]) +def get_scopes(offset: int = 0, limit: int = 100): + return db.session.query(Scope).offset(offset).limit(limit).all() + + +@scope.get("/{scope_id}", response_model=ScopeGet) +def get_scope(scope_id: int): + scope = db.session.query(Scope).filter(Scope.id == scope_id).one_or_none() + if not scope: + raise HTTPException(status_code=404, detail="Scope does not exist") + return scope + + +@scope.delete("/{scope_id}", response_model=None) +def delete_scope(scope_id: int, user=Depends(UnionAuth(['services.scope.delete']))): + scope = db.session.query(Scope).filter(Scope.id == scope_id).one_or_none() + if not scope: + raise HTTPException(status_code=404, detail="Scope does not exist") + db.session.delete(scope) + db.session.commit() + + +@scope.patch("/{scope_id}", response_model=ScopeUpdate) +def update_scope(scope_inp: ScopeUpdate, scope_id: int, user=Depends(UnionAuth(['services.scope.update']))): + scope = db.session.query(Scope).filter(Scope.id == scope_id).one_or_none() + if not scope: + raise HTTPException(status_code=404, detail="Scope does not exist") + if not any(scope_inp.dict().values()): + raise HTTPException(status_code=400, detail="Empty schema") + + query = db.session.query(Scope).filter(Scope.id == scope_id) + query.update(scope_inp.dict(exclude_unset=True, exclude_none=True)) + db.session.commit() + return scope From b44d7d6b3efdd130eb2645a55e0f58b1d7b595ec Mon Sep 17 00:00:00 2001 From: Wudext Date: Sat, 18 Mar 2023 11:53:53 +0300 Subject: [PATCH 02/10] Logic initialisation --- services_backend/routes/category.py | 45 ++++++++++++---------- services_backend/routes/models/category.py | 2 - 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/services_backend/routes/category.py b/services_backend/routes/category.py index 6aa06ef..e289dd0 100644 --- a/services_backend/routes/category.py +++ b/services_backend/routes/category.py @@ -4,19 +4,19 @@ from auth_lib.fastapi import UnionAuth from fastapi import APIRouter, Depends, HTTPException, Query from fastapi_sqlalchemy import db +from sqlalchemy import or_ from ..models.database import Button, Category, Scope from .models.category import CategoryCreate, CategoryGet, CategoryUpdate - logger = logging.getLogger(__name__) category = APIRouter() @category.post("/", response_model=CategoryGet) def create_category( - category_inp: CategoryCreate, - user=Depends(UnionAuth(['services.category.create'])), + category_inp: CategoryCreate, + user=Depends(UnionAuth(['services.category.create'])), ): """Создает категорию @@ -34,8 +34,8 @@ def create_category( @category.get("/", response_model=list[CategoryGet], response_model_exclude_none=True) def get_categories( - info: list[Literal['buttons']] = Query([]), - user=Depends(UnionAuth(allow_none=True, auto_error=False)), + info: list[Literal['buttons']] = Query([]), + user=Depends(UnionAuth(allow_none=True, auto_error=False)), ): """Показывает список категорий @@ -47,16 +47,18 @@ def get_categories( else: logger.info(f"User {user_id} triggered get_categories") + user_scopes = [scope["name"] for scope in user["session_scopes"]] if user else [] + return [ CategoryGet.from_orm(row).dict(exclude={"buttons"} if 'buttons' not in info else {}) - for row in db.session.query(Category).order_by(Category.order).all() + for row in list(filter(None, [categ if (set(user_scopes).intersection([scope.__dict__["name"] for scope in categ.scopes]) or (categ.scopes == [])) else None for categ in db.session.query(Category).all()])) ] @category.get("/{category_id}", response_model=CategoryGet, response_model_exclude_none=True) def get_category( - category_id: int, - user=Depends(UnionAuth(allow_none=True, auto_error=False)), + category_id: int, + user=Depends(UnionAuth(allow_none=True, auto_error=False)), ): """Показывает категорию @@ -67,23 +69,26 @@ def get_category( logger.info("Unauthorised user triggered get_category") else: logger.info(f"User {user_id} triggered get_category") - - category = db.session.query(Category).filter(Category.id == category_id).one_or_none() + + user_scopes = [scope["name"] for scope in user["session_scopes"]] if user else [] + + categories = list(filter(None, [categ if (set(user_scopes).intersection([scope.__dict__["name"] for scope in categ.scopes]) or (categ.scopes == [])) else None for categ in db.session.query(Category).all()])) + category = list(filter(None, [category if category.id == category_id else None for category in categories])) if not category: raise HTTPException(status_code=404, detail="Category does not exist") return { "id": category_id, - "order": category.order, - "name": category.name, - "type": category.type, - "scopes": category.scopes + "order": category[0].order, + "name": category[0].name, + "type": category[0].type, + "scopes": category[0].scopes } @category.delete("/{category_id}", response_model=None) def remove_category( - category_id: int, - user=Depends(UnionAuth(['services.category.delete'])), + category_id: int, + user=Depends(UnionAuth(['services.category.delete'])), ): """Удаляет категорию и все кнопки в ней @@ -104,9 +109,9 @@ def remove_category( @category.patch("/{category_id}", response_model=CategoryUpdate) def update_category( - category_inp: CategoryUpdate, - category_id: int, - user=Depends(UnionAuth(['services.category.update'])), + category_inp: CategoryUpdate, + category_id: int, + user=Depends(UnionAuth(['services.category.update'])), ): """Обновляет категорию @@ -128,7 +133,7 @@ def update_category( raise HTTPException( status_code=400, detail=f"Can`t create category with order {category_inp.order}. " - f"Last category is {last_category.order}", + f"Last category is {last_category.order}", ) if category.order > category_inp.order: diff --git a/services_backend/routes/models/category.py b/services_backend/routes/models/category.py index 2a00729..fffa8e3 100644 --- a/services_backend/routes/models/category.py +++ b/services_backend/routes/models/category.py @@ -6,14 +6,12 @@ class CategoryCreate(Base): type: str | None name: str | None - scopes: list[str] | None class CategoryUpdate(Base): order: int | None type: str | None name: str | None - scopes: list[str] | None class CategoryGet(Base): From af802cf95b976c30374717cc1032b37c99a78535 Mon Sep 17 00:00:00 2001 From: Wudext Date: Mon, 20 Mar 2023 09:28:22 +0300 Subject: [PATCH 03/10] Cleaning --- migrations/versions/660bb7891726_scopes.py | 16 ++++--- migrations/versions/6a486347af93_order.py | 16 +++++-- services_backend/routes/base.py | 1 - services_backend/routes/category.py | 55 ++++++++++++++++------ 4 files changed, 62 insertions(+), 26 deletions(-) diff --git a/migrations/versions/660bb7891726_scopes.py b/migrations/versions/660bb7891726_scopes.py index 382255d..97adc99 100644 --- a/migrations/versions/660bb7891726_scopes.py +++ b/migrations/versions/660bb7891726_scopes.py @@ -17,12 +17,16 @@ def upgrade(): - op.create_table('scope', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=True), - sa.Column('category_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['category_id'], ['category.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + 'scope', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('category_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ['category_id'], + ['category.id'], + ), + sa.PrimaryKeyConstraint('id'), ) diff --git a/migrations/versions/6a486347af93_order.py b/migrations/versions/6a486347af93_order.py index f0579e8..a3c9998 100644 --- a/migrations/versions/6a486347af93_order.py +++ b/migrations/versions/6a486347af93_order.py @@ -23,11 +23,15 @@ def upgrade(): conn = op.get_bind() res = conn.execute(sa.text("select id from button")).fetchall() for i in range(0, len(res)): - conn.execute(sa.text(f"""UPDATE "button" + conn.execute( + sa.text( + f"""UPDATE "button" SET "order"={i + 1}, "link"='#', "type"='external' - WHERE id={res[i][0]}""")) + WHERE id={res[i][0]}""" + ) + ) op.alter_column('button', 'order', nullable=False) op.alter_column('button', 'link', nullable=False) op.alter_column('button', 'type', nullable=False) @@ -37,9 +41,13 @@ def upgrade(): op.add_column('category', sa.Column('order', sa.Integer(), nullable=True)) res = conn.execute(sa.text("select id from category")).fetchall() for i in range(0, len(res)): - conn.execute(sa.text(f"""UPDATE "category" + conn.execute( + sa.text( + f"""UPDATE "category" SET "order"={i + 1} - WHERE id={res[i][0]}""")) + WHERE id={res[i][0]}""" + ) + ) op.alter_column('category', 'order', nullable=False) op.alter_column('category', 'name', existing_type=sa.VARCHAR(), nullable=False) op.alter_column('category', 'type', existing_type=sa.VARCHAR(), nullable=False) diff --git a/services_backend/routes/base.py b/services_backend/routes/base.py index 7709190..58e238f 100644 --- a/services_backend/routes/base.py +++ b/services_backend/routes/base.py @@ -35,4 +35,3 @@ app.include_router(button, prefix='/category/{category_id}/button', tags=["Button"]) app.include_router(category, prefix='/category', tags=["Category"]) app.include_router(scope, prefix='/scope', tags=["Scope"]) - diff --git a/services_backend/routes/category.py b/services_backend/routes/category.py index e289dd0..ab0ec11 100644 --- a/services_backend/routes/category.py +++ b/services_backend/routes/category.py @@ -15,8 +15,8 @@ @category.post("/", response_model=CategoryGet) def create_category( - category_inp: CategoryCreate, - user=Depends(UnionAuth(['services.category.create'])), + category_inp: CategoryCreate, + user=Depends(UnionAuth(['services.category.create'])), ): """Создает категорию @@ -34,8 +34,8 @@ def create_category( @category.get("/", response_model=list[CategoryGet], response_model_exclude_none=True) def get_categories( - info: list[Literal['buttons']] = Query([]), - user=Depends(UnionAuth(allow_none=True, auto_error=False)), + info: list[Literal['buttons']] = Query([]), + user=Depends(UnionAuth(allow_none=True, auto_error=False)), ): """Показывает список категорий @@ -48,17 +48,29 @@ def get_categories( logger.info(f"User {user_id} triggered get_categories") user_scopes = [scope["name"] for scope in user["session_scopes"]] if user else [] + categories = db.session.query(Category).all() + category_scopes = [] + for categ in categories: + category_scopes.append([scope.__dict__["name"] for scope in categ.scopes]) return [ CategoryGet.from_orm(row).dict(exclude={"buttons"} if 'buttons' not in info else {}) - for row in list(filter(None, [categ if (set(user_scopes).intersection([scope.__dict__["name"] for scope in categ.scopes]) or (categ.scopes == [])) else None for categ in db.session.query(Category).all()])) + for row in list( + filter( + None, + [ + categ if (set(user_scopes).intersection(category_scopes) or (categ.scopes == [])) else None + for categ in categories + ], + ) + ) ] @category.get("/{category_id}", response_model=CategoryGet, response_model_exclude_none=True) def get_category( - category_id: int, - user=Depends(UnionAuth(allow_none=True, auto_error=False)), + category_id: int, + user=Depends(UnionAuth(allow_none=True, auto_error=False)), ): """Показывает категорию @@ -72,7 +84,20 @@ def get_category( user_scopes = [scope["name"] for scope in user["session_scopes"]] if user else [] - categories = list(filter(None, [categ if (set(user_scopes).intersection([scope.__dict__["name"] for scope in categ.scopes]) or (categ.scopes == [])) else None for categ in db.session.query(Category).all()])) + categories = list( + filter( + None, + [ + categ + if ( + set(user_scopes).intersection([scope.__dict__["name"] for scope in categ.scopes]) + or (categ.scopes == []) + ) + else None + for categ in db.session.query(Category).all() + ], + ) + ) category = list(filter(None, [category if category.id == category_id else None for category in categories])) if not category: raise HTTPException(status_code=404, detail="Category does not exist") @@ -81,14 +106,14 @@ def get_category( "order": category[0].order, "name": category[0].name, "type": category[0].type, - "scopes": category[0].scopes + "scopes": category[0].scopes, } @category.delete("/{category_id}", response_model=None) def remove_category( - category_id: int, - user=Depends(UnionAuth(['services.category.delete'])), + category_id: int, + user=Depends(UnionAuth(['services.category.delete'])), ): """Удаляет категорию и все кнопки в ней @@ -109,9 +134,9 @@ def remove_category( @category.patch("/{category_id}", response_model=CategoryUpdate) def update_category( - category_inp: CategoryUpdate, - category_id: int, - user=Depends(UnionAuth(['services.category.update'])), + category_inp: CategoryUpdate, + category_id: int, + user=Depends(UnionAuth(['services.category.update'])), ): """Обновляет категорию @@ -133,7 +158,7 @@ def update_category( raise HTTPException( status_code=400, detail=f"Can`t create category with order {category_inp.order}. " - f"Last category is {last_category.order}", + f"Last category is {last_category.order}", ) if category.order > category_inp.order: From 466683caf901e6f602787f65e7fdb5fb9512e51a Mon Sep 17 00:00:00 2001 From: Stanislav Roslavtsev Date: Mon, 20 Mar 2023 09:35:15 +0300 Subject: [PATCH 04/10] Fixing --- services_backend/routes/category.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services_backend/routes/category.py b/services_backend/routes/category.py index ab0ec11..4d79616 100644 --- a/services_backend/routes/category.py +++ b/services_backend/routes/category.py @@ -59,7 +59,7 @@ def get_categories( filter( None, [ - categ if (set(user_scopes).intersection(category_scopes) or (categ.scopes == [])) else None + categ if (set(user_scopes).intersection(category_scopes) or (categ.scopes == None)) else None for categ in categories ], ) @@ -91,7 +91,7 @@ def get_category( categ if ( set(user_scopes).intersection([scope.__dict__["name"] for scope in categ.scopes]) - or (categ.scopes == []) + or (categ.scopes == None) ) else None for categ in db.session.query(Category).all() From 85f7fd60949db9d27639227e86d1517f4d129978 Mon Sep 17 00:00:00 2001 From: Stanislav Roslavtsev Date: Mon, 20 Mar 2023 18:08:08 +0300 Subject: [PATCH 05/10] Fixing tests and cleaning --- services_backend/routes/category.py | 48 +++++++++-------------------- tests/api/category.py | 1 + 2 files changed, 15 insertions(+), 34 deletions(-) diff --git a/services_backend/routes/category.py b/services_backend/routes/category.py index 4d79616..608ee14 100644 --- a/services_backend/routes/category.py +++ b/services_backend/routes/category.py @@ -48,22 +48,15 @@ def get_categories( logger.info(f"User {user_id} triggered get_categories") user_scopes = [scope["name"] for scope in user["session_scopes"]] if user else [] - categories = db.session.query(Category).all() - category_scopes = [] - for categ in categories: - category_scopes.append([scope.__dict__["name"] for scope in categ.scopes]) + filtered_categories = [] + for category in db.session.query(Category).order_by(Category.order).all(): + category_scopes = [scope.__dict__["name"] for scope in category.scopes] + if (category_scopes == []) or (set(user_scopes) & set(category_scopes)): + filtered_categories.append(category) return [ CategoryGet.from_orm(row).dict(exclude={"buttons"} if 'buttons' not in info else {}) - for row in list( - filter( - None, - [ - categ if (set(user_scopes).intersection(category_scopes) or (categ.scopes == None)) else None - for categ in categories - ], - ) - ) + for row in filtered_categories ] @@ -83,30 +76,17 @@ def get_category( logger.info(f"User {user_id} triggered get_category") user_scopes = [scope["name"] for scope in user["session_scopes"]] if user else [] - - categories = list( - filter( - None, - [ - categ - if ( - set(user_scopes).intersection([scope.__dict__["name"] for scope in categ.scopes]) - or (categ.scopes == None) - ) - else None - for categ in db.session.query(Category).all() - ], - ) - ) - category = list(filter(None, [category if category.id == category_id else None for category in categories])) - if not category: + category = db.session.query(Category).filter(Category.id == category_id).one_or_none() + if not category or ( + category.scopes and not (set(user_scopes) & set([scope.__dict__["name"] for scope in category.scopes])) + ): raise HTTPException(status_code=404, detail="Category does not exist") return { "id": category_id, - "order": category[0].order, - "name": category[0].name, - "type": category[0].type, - "scopes": category[0].scopes, + "order": category.order, + "name": category.name, + "type": category.type, + "scopes": category.scopes, } diff --git a/tests/api/category.py b/tests/api/category.py index 7531257..9af79c3 100644 --- a/tests/api/category.py +++ b/tests/api/category.py @@ -32,6 +32,7 @@ def test_post_success(self, client, dbsession): assert db_category_created.type == body["type"] assert db_category_created.order == 1 assert not db_category_created.buttons + client.delete(f'{self._url}{db_category_created.id}') def test_get_by_id_success(self, client, db_category): res = client.get(f'{self._url}{db_category.id}') From 4379e935759cb58df76cf6e47ef1bbe73a573002 Mon Sep 17 00:00:00 2001 From: Stanislav Roslavtsev Date: Mon, 20 Mar 2023 18:56:28 +0300 Subject: [PATCH 06/10] Finishing --- services_backend/routes/base.py | 2 +- services_backend/routes/category.py | 14 +++++------ services_backend/routes/models/scope.py | 6 ----- services_backend/routes/scope.py | 32 +++++++------------------ 4 files changed, 17 insertions(+), 37 deletions(-) diff --git a/services_backend/routes/base.py b/services_backend/routes/base.py index 58e238f..be67bb1 100644 --- a/services_backend/routes/base.py +++ b/services_backend/routes/base.py @@ -34,4 +34,4 @@ app.include_router(button, prefix='/category/{category_id}/button', tags=["Button"]) app.include_router(category, prefix='/category', tags=["Category"]) -app.include_router(scope, prefix='/scope', tags=["Scope"]) +app.include_router(scope, prefix='/category/{category_id}/scope', tags=["Scope"]) diff --git a/services_backend/routes/category.py b/services_backend/routes/category.py index 608ee14..514f22e 100644 --- a/services_backend/routes/category.py +++ b/services_backend/routes/category.py @@ -4,7 +4,7 @@ from auth_lib.fastapi import UnionAuth from fastapi import APIRouter, Depends, HTTPException, Query from fastapi_sqlalchemy import db -from sqlalchemy import or_ +from sqlalchemy.orm import joinedload from ..models.database import Button, Category, Scope from .models.category import CategoryCreate, CategoryGet, CategoryUpdate @@ -47,11 +47,11 @@ def get_categories( else: logger.info(f"User {user_id} triggered get_categories") - user_scopes = [scope["name"] for scope in user["session_scopes"]] if user else [] + user_scopes = set([scope["name"] for scope in user["session_scopes"]] if user else []) filtered_categories = [] - for category in db.session.query(Category).order_by(Category.order).all(): - category_scopes = [scope.__dict__["name"] for scope in category.scopes] - if (category_scopes == []) or (set(user_scopes) & set(category_scopes)): + for category in db.session.query(Category).order_by(Category.order).options(joinedload(Category.scopes)).all(): + category_scopes = set([scope.__dict__["name"] for scope in category.scopes]) + if (category_scopes == set()) or (user_scopes & category_scopes): filtered_categories.append(category) return [ @@ -75,10 +75,10 @@ def get_category( else: logger.info(f"User {user_id} triggered get_category") - user_scopes = [scope["name"] for scope in user["session_scopes"]] if user else [] + user_scopes = set([scope["name"] for scope in user["session_scopes"]] if user else []) category = db.session.query(Category).filter(Category.id == category_id).one_or_none() if not category or ( - category.scopes and not (set(user_scopes) & set([scope.__dict__["name"] for scope in category.scopes])) + category.scopes and not (user_scopes & set([scope.__dict__["name"] for scope in category.scopes])) ): raise HTTPException(status_code=404, detail="Category does not exist") return { diff --git a/services_backend/routes/models/scope.py b/services_backend/routes/models/scope.py index 66b86ab..438698f 100644 --- a/services_backend/routes/models/scope.py +++ b/services_backend/routes/models/scope.py @@ -3,14 +3,8 @@ class ScopeCreate(Base): name: str - category_id: int | None class ScopeGet(Base): id: int name: str - - -class ScopeUpdate(Base): - name: str | None - category_id: int | None diff --git a/services_backend/routes/scope.py b/services_backend/routes/scope.py index 45fe7e7..bcb7f1f 100644 --- a/services_backend/routes/scope.py +++ b/services_backend/routes/scope.py @@ -2,51 +2,37 @@ from fastapi_sqlalchemy import db from auth_lib.fastapi import UnionAuth -from .models.scope import ScopeGet, ScopeCreate, ScopeUpdate +from .models.scope import ScopeGet, ScopeCreate from ..models.database import Scope scope = APIRouter() @scope.post("/", response_model=ScopeGet) -def create_scope(scope_inp: ScopeCreate, user=Depends(UnionAuth(['services.scope.create']))): - scope = Scope(**scope_inp.dict(exclude_none=True)) +def create_scope(scope_inp: ScopeCreate, category_id: int, user=Depends(UnionAuth(['services.scope.create']))): + scope = Scope(**{"name": scope_inp.name, "category_id": category_id}) db.session.add(scope) db.session.commit() return scope @scope.get("/", response_model=list[ScopeGet]) -def get_scopes(offset: int = 0, limit: int = 100): - return db.session.query(Scope).offset(offset).limit(limit).all() +def get_scopes(category_id: int, offset: int = 0, limit: int = 100): + return db.session.query(Scope).filter(category_id == Scope.category_id).offset(offset).limit(limit).all() @scope.get("/{scope_id}", response_model=ScopeGet) -def get_scope(scope_id: int): - scope = db.session.query(Scope).filter(Scope.id == scope_id).one_or_none() +def get_scope(category_id: int, scope_id: int): + scope = db.session.query(Scope).filter(category_id == Scope.category_id).filter(Scope.id == scope_id).one_or_none() if not scope: raise HTTPException(status_code=404, detail="Scope does not exist") return scope @scope.delete("/{scope_id}", response_model=None) -def delete_scope(scope_id: int, user=Depends(UnionAuth(['services.scope.delete']))): - scope = db.session.query(Scope).filter(Scope.id == scope_id).one_or_none() +def delete_scope(category_id: int, scope_id: int, user=Depends(UnionAuth(['services.scope.delete']))): + scope = db.session.query(Scope).filter(category_id == Scope.category_id).filter(Scope.id == scope_id).one_or_none() if not scope: raise HTTPException(status_code=404, detail="Scope does not exist") db.session.delete(scope) db.session.commit() - - -@scope.patch("/{scope_id}", response_model=ScopeUpdate) -def update_scope(scope_inp: ScopeUpdate, scope_id: int, user=Depends(UnionAuth(['services.scope.update']))): - scope = db.session.query(Scope).filter(Scope.id == scope_id).one_or_none() - if not scope: - raise HTTPException(status_code=404, detail="Scope does not exist") - if not any(scope_inp.dict().values()): - raise HTTPException(status_code=400, detail="Empty schema") - - query = db.session.query(Scope).filter(Scope.id == scope_id) - query.update(scope_inp.dict(exclude_unset=True, exclude_none=True)) - db.session.commit() - return scope From a69bc9eb79441b9995129c25519a7fa0bbb20819 Mon Sep 17 00:00:00 2001 From: Dyakov Roman Date: Mon, 20 Mar 2023 19:13:25 +0300 Subject: [PATCH 07/10] Better makefile --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 61dde27..01e5bf1 100644 --- a/Makefile +++ b/Makefile @@ -8,12 +8,12 @@ venv: python3.11 -m venv venv format: - autoflake -r --in-place --remove-all-unused-imports ./services_backend - isort ./services_backend - black ./services_backend + source ./venv/bin/activate && autoflake -r --in-place --remove-all-unused-imports ./services_backend + source ./venv/bin/activate && isort ./services_backend + source ./venv/bin/activate && black ./services_backend db: docker run -d -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust --name db-services-backend postgres:15 migrate: - alembic upgrade head + source ./venv/bin/activate && alembic upgrade head From 0fe7b448ec274b8fea1ed9e0cd6a3d09711fea73 Mon Sep 17 00:00:00 2001 From: Dyakov Roman Date: Mon, 20 Mar 2023 19:13:49 +0300 Subject: [PATCH 08/10] Fix autocommit --- services_backend/routes/base.py | 6 +++++- services_backend/routes/button.py | 6 +++--- services_backend/routes/category.py | 6 +++--- services_backend/routes/scope.py | 4 ++-- tests/api/conftest.py | 4 ++-- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/services_backend/routes/base.py b/services_backend/routes/base.py index be67bb1..67697cc 100644 --- a/services_backend/routes/base.py +++ b/services_backend/routes/base.py @@ -22,7 +22,11 @@ ) -app.add_middleware(DBSessionMiddleware, db_url=settings.DB_DSN, engine_args={"pool_pre_ping": True}) +app.add_middleware( + DBSessionMiddleware, + db_url=settings.DB_DSN, + engine_args={"pool_pre_ping": True, "isolation_level": "AUTOCOMMIT"}, +) app.add_middleware( CORSMiddleware, diff --git a/services_backend/routes/button.py b/services_backend/routes/button.py index 4e2933c..f876826 100644 --- a/services_backend/routes/button.py +++ b/services_backend/routes/button.py @@ -33,7 +33,7 @@ def create_button( if last_button: button.order = last_button.order + 1 db.session.add(button) - db.session.commit() + db.session.flush() return button @@ -98,7 +98,7 @@ def remove_button( raise HTTPException(status_code=404, detail="Button is not in this category") db.session.delete(button) db.session.query(Button).filter(Button.order > button.order).update({"order": Button.order - 1}) - db.session.commit() + db.session.flush() @button.patch("/{button_id}", response_model=ButtonUpdate) @@ -143,5 +143,5 @@ def update_button( db.session.query(Button).filter(Button.order > button.order).update({"order": Button.order - 1}) query.update(button_inp.dict(exclude_unset=True, exclude_none=True)) - db.session.commit() + db.session.flush() return button diff --git a/services_backend/routes/category.py b/services_backend/routes/category.py index 514f22e..cc0c5c3 100644 --- a/services_backend/routes/category.py +++ b/services_backend/routes/category.py @@ -28,7 +28,7 @@ def create_category( if last_category: category.order = last_category.order + 1 db.session.add(category) - db.session.commit() + db.session.flush() return category @@ -109,7 +109,7 @@ def remove_category( db.session.flush() db.session.query(Category).filter(Category.order > category.order).update({"order": Category.order - 1}) db.session.delete(category) - db.session.commit() + db.session.flush() @category.patch("/{category_id}", response_model=CategoryUpdate) @@ -148,5 +148,5 @@ def update_category( query = db.session.query(Category).filter(Category.id == category_id) query.update(category_inp.dict(exclude_unset=True, exclude_none=True)) - db.session.commit() + db.session.flush() return category diff --git a/services_backend/routes/scope.py b/services_backend/routes/scope.py index bcb7f1f..54c8cd0 100644 --- a/services_backend/routes/scope.py +++ b/services_backend/routes/scope.py @@ -12,7 +12,7 @@ def create_scope(scope_inp: ScopeCreate, category_id: int, user=Depends(UnionAuth(['services.scope.create']))): scope = Scope(**{"name": scope_inp.name, "category_id": category_id}) db.session.add(scope) - db.session.commit() + db.session.flush() return scope @@ -35,4 +35,4 @@ def delete_scope(category_id: int, scope_id: int, user=Depends(UnionAuth(['servi if not scope: raise HTTPException(status_code=404, detail="Scope does not exist") db.session.delete(scope) - db.session.commit() + db.session.flush() diff --git a/tests/api/conftest.py b/tests/api/conftest.py index 203b3c8..33cd018 100644 --- a/tests/api/conftest.py +++ b/tests/api/conftest.py @@ -11,7 +11,7 @@ def db_category(dbsession): yield category for button in dbsession.query(Button).filter(Button.category_id == category.id).all(): dbsession.delete(button) - dbsession.commit() + dbsession.flush() dbsession.delete(category) dbsession.commit() @@ -25,4 +25,4 @@ def db_button(dbsession, db_category): yield _button if _button: dbsession.delete(_button) - dbsession.commit() + dbsession.commit() From a0e9ae99a1742989351fcc820ebd69e87b5340f5 Mon Sep 17 00:00:00 2001 From: Stanislav Roslavtsev Date: Mon, 20 Mar 2023 19:17:09 +0300 Subject: [PATCH 09/10] Update scope.py --- services_backend/routes/scope.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/services_backend/routes/scope.py b/services_backend/routes/scope.py index bcb7f1f..418624c 100644 --- a/services_backend/routes/scope.py +++ b/services_backend/routes/scope.py @@ -21,14 +21,6 @@ def get_scopes(category_id: int, offset: int = 0, limit: int = 100): return db.session.query(Scope).filter(category_id == Scope.category_id).offset(offset).limit(limit).all() -@scope.get("/{scope_id}", response_model=ScopeGet) -def get_scope(category_id: int, scope_id: int): - scope = db.session.query(Scope).filter(category_id == Scope.category_id).filter(Scope.id == scope_id).one_or_none() - if not scope: - raise HTTPException(status_code=404, detail="Scope does not exist") - return scope - - @scope.delete("/{scope_id}", response_model=None) def delete_scope(category_id: int, scope_id: int, user=Depends(UnionAuth(['services.scope.delete']))): scope = db.session.query(Scope).filter(category_id == Scope.category_id).filter(Scope.id == scope_id).one_or_none() From 8add3fbee591bbb2cd2c1cb8876c2ed912c85612 Mon Sep 17 00:00:00 2001 From: Stanislav Roslavtsev Date: Mon, 20 Mar 2023 19:42:44 +0300 Subject: [PATCH 10/10] Adding new tests --- services_backend/routes/scope.py | 4 +- tests/api/category.py | 71 +++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/services_backend/routes/scope.py b/services_backend/routes/scope.py index 009c4c0..826a719 100644 --- a/services_backend/routes/scope.py +++ b/services_backend/routes/scope.py @@ -9,7 +9,7 @@ @scope.post("/", response_model=ScopeGet) -def create_scope(scope_inp: ScopeCreate, category_id: int, user=Depends(UnionAuth(['services.scope.create']))): +def create_scope(scope_inp: ScopeCreate, category_id: int, user=Depends(UnionAuth(['services.category.permission.create']))): scope = Scope(**{"name": scope_inp.name, "category_id": category_id}) db.session.add(scope) db.session.flush() @@ -22,7 +22,7 @@ def get_scopes(category_id: int, offset: int = 0, limit: int = 100): @scope.delete("/{scope_id}", response_model=None) -def delete_scope(category_id: int, scope_id: int, user=Depends(UnionAuth(['services.scope.delete']))): +def delete_scope(category_id: int, scope_id: int, user=Depends(UnionAuth(['services.category.permission.delete']))): scope = db.session.query(Scope).filter(category_id == Scope.category_id).filter(Scope.id == scope_id).one_or_none() if not scope: raise HTTPException(status_code=404, detail="Scope does not exist") diff --git a/tests/api/category.py b/tests/api/category.py index 9af79c3..73b0ca3 100644 --- a/tests/api/category.py +++ b/tests/api/category.py @@ -2,6 +2,7 @@ from starlette import status from services_backend.settings import get_settings from services_backend.models.database import Category +from pytest_mock import MockerFixture class TestCategory: @@ -34,7 +35,26 @@ def test_post_success(self, client, dbsession): assert not db_category_created.buttons client.delete(f'{self._url}{db_category_created.id}') - def test_get_by_id_success(self, client, db_category): + def test_get_by_id_success(self, client, db_category, mocker: MockerFixture): + res = client.get(f'{self._url}{db_category.id}') + assert res.status_code == status.HTTP_200_OK + res_body = res.json() + assert res_body['id'] == db_category.id + assert res_body['type'] == db_category.type + assert res_body['name'] == db_category.name + assert res_body['order'] == db_category.order + + user_mock = mocker.patch('auth_lib.fastapi.UnionAuth.__call__') + user_mock.return_value = { + "session_scopes": [{"id": 0, "name": "string", "comment": "string"}, + {"id": 1, "name": "test", "comment": "string"}], + "user_scopes": [{"id": 0, "name": "string", "comment": "string"}], + "indirect_groups": [{"id": 0, "name": "string", "parent_id": 0}], + "groups": [{"id": 0, "name": "string", "parent_id": 0}], + "id": 0, + "email": "string", + } + res = client.get(f'{self._url}{db_category.id}') assert res.status_code == status.HTTP_200_OK res_body = res.json() @@ -153,3 +173,52 @@ def test_delete_order(self, db_category, client): res = client.get(f"{self._url}{res1.json()['id']}") assert res.json()['order'] == 1 + + def test_category_scopes(self, client, dbsession, mocker: MockerFixture): + category_body = {"name": "t", "type": "string"} + res1 = client.post(self._url, data=json.dumps(category_body)) + assert res1.status_code == status.HTTP_200_OK + res1_body = res1.json() + + scope_body = {"name": "test"} + res2 = client.post(f"{self._url}{res1_body['id']}/scope/", data=json.dumps(scope_body)) + assert res2.status_code == status.HTTP_200_OK + + res = client.get(f"{self._url}{res1_body['id']}") + assert res.status_code == status.HTTP_404_NOT_FOUND + + user_mock = mocker.patch('auth_lib.fastapi.UnionAuth.__call__') + user_mock.return_value = { + "session_scopes": [{"id": 0, "name": "string", "comment": "string"}, {"id": 1, "name": "test", "comment": "string"}], + "user_scopes": [{"id": 0, "name": "string", "comment": "string"}], + "indirect_groups": [{"id": 0, "name": "string", "parent_id": 0}], + "groups": [{"id": 0, "name": "string", "parent_id": 0}], + "id": 0, + "email": "string", + } + + res = client.get(f"{self._url}{res1_body['id']}") + assert res.status_code == status.HTTP_200_OK + + def test_category_invalid_scopes(self, client, dbsession, mocker: MockerFixture): + category_body = {"name": "t", "type": "string"} + res1 = client.post(self._url, data=json.dumps(category_body)) + assert res1.status_code == status.HTTP_200_OK + res1_body = res1.json() + + scope_body = {"name": "test"} + res2 = client.post(f"{self._url}{res1_body['id']}/scope/", data=json.dumps(scope_body)) + assert res2.status_code == status.HTTP_200_OK + + user_mock = mocker.patch('auth_lib.fastapi.UnionAuth.__call__') + user_mock.return_value = { + "session_scopes": [{"id": 0, "name": "string", "comment": "string"}, {"id": 3, "name": "lmao", "comment": "string"}], + "user_scopes": [{"id": 0, "name": "string", "comment": "string"}], + "indirect_groups": [{"id": 0, "name": "string", "parent_id": 0}], + "groups": [{"id": 0, "name": "string", "parent_id": 0}], + "id": 0, + "email": "string", + } + + res = client.get(f"{self._url}{res1_body['id']}") + assert res.status_code == status.HTTP_404_NOT_FOUND