From 4e21448aac57574de72573571d4c9abb3dd39967 Mon Sep 17 00:00:00 2001 From: Timur Enikeev Date: Mon, 29 Jul 2024 23:09:33 -0400 Subject: [PATCH 1/6] Added the ability to hide button --- .../52230bfc3f86_56_add_is_hidden_field.py | 29 +++++++++++ services_backend/models/database.py | 1 + services_backend/routes/button.py | 38 +++++++++++--- services_backend/routes/category.py | 2 +- tests/api/button.py | 49 +++++++++++++++++++ 5 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 migrations/versions/52230bfc3f86_56_add_is_hidden_field.py diff --git a/migrations/versions/52230bfc3f86_56_add_is_hidden_field.py b/migrations/versions/52230bfc3f86_56_add_is_hidden_field.py new file mode 100644 index 0000000..6d00b8f --- /dev/null +++ b/migrations/versions/52230bfc3f86_56_add_is_hidden_field.py @@ -0,0 +1,29 @@ +"""#56_add_is_hidden_field + +Revision ID: 52230bfc3f86 +Revises: 1bb798899506 +Create Date: 2024-07-29 20:05:34.777852 + +""" + +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision = '52230bfc3f86' +down_revision = '1bb798899506' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('button', sa.Column('is_hidden', sa.Boolean(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('button', 'is_hidden') + # ### end Alembic commands ### diff --git a/services_backend/models/database.py b/services_backend/models/database.py index f79fd0b..611beac 100644 --- a/services_backend/models/database.py +++ b/services_backend/models/database.py @@ -63,6 +63,7 @@ class Button(Base): icon: Mapped[str] = mapped_column(String) link: Mapped[str] = mapped_column(String) type: Mapped[Type] = mapped_column(DbEnum(Type, native_enum=False), nullable=False) + is_hidden: Mapped[bool] = mapped_column(Boolean, default=False) _scopes: Mapped[list[Scope]] = relationship("Scope", back_populates="button", lazy='joined', cascade='delete') diff --git a/services_backend/routes/button.py b/services_backend/routes/button.py index 3468b28..37d88b4 100644 --- a/services_backend/routes/button.py +++ b/services_backend/routes/button.py @@ -22,8 +22,9 @@ class ButtonCreate(Base): name: str = Field(description='Название кнопки') link: str = Field(description='Ссылка, на которую перенаправляет кнопка') type: Type = Field(description='Тип открываемой ссылки (Ссылка приложения/Браузер в приложении/Браузер') - required_scopes: set[str] | None = Field(description='Каким скоупы нужны, чтобы кнопка была доступна', default=None) - optional_scopes: set[str] | None = Field(description='Каким скоупы желательны', default=None) + is_hidden: bool = Field(description='Спрятана ли кнопка от пользователя', default=False) + required_scopes: set[str] | None = Field(description='Какие скоупы нужны, чтобы кнопка была доступна', default=None) + optional_scopes: set[str] | None = Field(description='Какиеп скоупы желательны', default=None) class ButtonUpdate(Base): @@ -35,13 +36,15 @@ class ButtonUpdate(Base): type: Type | None = Field( description='Тип открываемой ссылки (Ссылка приложения/Браузер в приложении/Браузер', default=None ) - required_scopes: set[str] | None = Field(description='Каким скоупы нужны, чтобы кнопка была доступна', default=None) - optional_scopes: set[str] | None = Field(description='Каким скоупы желательны', default=None) + is_hidden: bool | None = Field(description='Спрятана ли кнопка от пользователя', default=None) + required_scopes: set[str] | None = Field(description='Какие скоупы нужны, чтобы кнопка была доступна', default=None) + optional_scopes: set[str] | None = Field(description='Какие скоупы желательны', default=None) class ButtonView(Enum): ACTIVE = "active" BLOCKED = "blocked" + HIDDEN = "hidden" class ButtonGet(Base): @@ -95,7 +98,20 @@ def create_button( button.optional_scopes = optional_scopes db.session.add(button) db.session.flush() - return button + view = ButtonView.HIDDEN if button.is_hidden else ButtonView.ACTIVE + result = { + "id": button.id, + "icon": button.icon, + "name": button.name, + "link": button.link if view == ButtonView.ACTIVE else None, + "order": button.order, + "type": button.type, + "view": view.value, + "required_scopes": button.required_scopes, + "optional_scopes": button.optional_scopes, + "scopes": [], + } + return result @button.get("", response_model=ButtonsGet, response_model_exclude_unset=True) @@ -120,7 +136,9 @@ def get_buttons( for button in category.buttons: view = ButtonView.ACTIVE scopes = set() - if button.required_scopes - user_scopes: + if button.is_hidden: + view = ButtonView.HIDDEN + elif button.required_scopes - user_scopes: view = ButtonView.BLOCKED else: scopes |= button.required_scopes @@ -169,7 +187,9 @@ def get_button( raise HTTPException(status_code=404, detail="Button is not this category") view = ButtonView.ACTIVE scopes = set() - if button.required_scopes - user_scopes: + if button.is_hidden: + view = ButtonView.HIDDEN + elif button.required_scopes - user_scopes: view = ButtonView.BLOCKED else: scopes |= button.required_scopes @@ -297,7 +317,9 @@ def get_service( raise HTTPException(status_code=404, detail="Button does not exist") view = ButtonView.ACTIVE scopes = set() - if button.required_scopes - user_scopes: + if button.is_hidden: + view = ButtonView.HIDDEN + elif button.required_scopes - user_scopes: view = ButtonView.BLOCKED else: scopes |= button.required_scopes diff --git a/services_backend/routes/category.py b/services_backend/routes/category.py index fb1cc84..07b1fe0 100644 --- a/services_backend/routes/category.py +++ b/services_backend/routes/category.py @@ -6,7 +6,7 @@ from fastapi_sqlalchemy import db from pydantic import Field, conint -from services_backend.models.database import Button, Category, Type +from services_backend.models.database import Button, Category from services_backend.schemas import Base from .button import ButtonGet, ButtonView diff --git a/tests/api/button.py b/tests/api/button.py index 821c90e..c37bcaf 100644 --- a/tests/api/button.py +++ b/tests/api/button.py @@ -235,3 +235,52 @@ def test_type_not_enum(client, dbsession, db_category): } res = client.post(f"/category/{db_category.id}/button", data=json.dumps(body)) assert res.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + +def test_post_hidden_success(client, dbsession, db_category): + body = {"icon": "qq", "name": "new", "link": "google.com", "type": "inapp", "is_hidden": "True"} + res = client.post(f"/category/{db_category.id}/button", data=json.dumps(body)) + assert res.status_code == status.HTTP_200_OK + res_body = res.json() + assert res_body["icon"] == body["icon"] + assert res_body["order"] == 1 + assert res_body["name"] == body["name"] + assert "link" not in res_body + assert res_body["type"] == body["type"] + db_button_created: Button = dbsession.query(Button).filter(Button.id == res_body["id"]).one_or_none() + assert db_button_created + assert db_button_created.icon == body["icon"] + assert db_button_created.name == body["name"] + assert db_button_created.category == db_category + assert db_button_created.link == body["link"] + assert db_button_created.type == body["type"] + assert db_button_created.order == 1 + + +def test_get_hidden_by_id_success(client, db_button, db_category): + db_button.is_hidden = True + res = client.get(f"/category/{db_category.id}/button/{db_button.id}") + assert res.status_code == status.HTTP_200_OK + res_body = res.json() + assert res_body['icon'] == db_button.icon + assert res_body['name'] == db_button.name + assert res_body['order'] == db_button.order + assert res_body['link'] is None + assert res_body['type'] == db_button.type + + +def test_patch_to_hide_success(client, db_button, db_category): + body = {"is_hidden": "True"} + res = client.patch(f"/category/{db_category.id}/button/{db_button.id}", data=json.dumps(body)) + assert res.status_code == status.HTTP_200_OK + res_body = res.json() + assert res_body["icon"] == db_button.icon + assert res_body["order"] == db_button.order + assert res_body["name"] == db_button.name + assert res_body["link"] == db_button.link + assert res_body["type"] == db_button.type + assert res_body["is_hidden"] == True + res = client.get(f"/category/{db_category.id}/button/{db_button.id}") + assert res.status_code == status.HTTP_200_OK + assert res.json()["link"] is None + assert res.json()["view"] == "hidden" From 857543156a0f0a7a29484de0765629f6fa8f142e Mon Sep 17 00:00:00 2001 From: Timur Enikeev Date: Tue, 30 Jul 2024 01:00:23 -0400 Subject: [PATCH 2/6] Add test to delete hidden button --- tests/api/button.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/api/button.py b/tests/api/button.py index c37bcaf..df3dcd7 100644 --- a/tests/api/button.py +++ b/tests/api/button.py @@ -238,14 +238,14 @@ def test_type_not_enum(client, dbsession, db_category): def test_post_hidden_success(client, dbsession, db_category): - body = {"icon": "qq", "name": "new", "link": "google.com", "type": "inapp", "is_hidden": "True"} + body = {"icon": "qq", "name": "new", "link": "google.com", "type": "inapp", "is_hidden": True} res = client.post(f"/category/{db_category.id}/button", data=json.dumps(body)) assert res.status_code == status.HTTP_200_OK res_body = res.json() assert res_body["icon"] == body["icon"] assert res_body["order"] == 1 assert res_body["name"] == body["name"] - assert "link" not in res_body + assert res_body["link"] == body["link"] assert res_body["type"] == body["type"] db_button_created: Button = dbsession.query(Button).filter(Button.id == res_body["id"]).one_or_none() assert db_button_created @@ -255,10 +255,14 @@ def test_post_hidden_success(client, dbsession, db_category): assert db_button_created.link == body["link"] assert db_button_created.type == body["type"] assert db_button_created.order == 1 + assert db_button_created.is_hidden == True + dbsession.delete(db_button_created) + dbsession.commit() -def test_get_hidden_by_id_success(client, db_button, db_category): +def test_get_hidden_by_id_success(client, dbsession, db_button, db_category): db_button.is_hidden = True + dbsession.commit() res = client.get(f"/category/{db_category.id}/button/{db_button.id}") assert res.status_code == status.HTTP_200_OK res_body = res.json() @@ -267,6 +271,7 @@ def test_get_hidden_by_id_success(client, db_button, db_category): assert res_body['order'] == db_button.order assert res_body['link'] is None assert res_body['type'] == db_button.type + assert res_body['view'] == "hidden" def test_patch_to_hide_success(client, db_button, db_category): @@ -284,3 +289,13 @@ def test_patch_to_hide_success(client, db_button, db_category): assert res.status_code == status.HTTP_200_OK assert res.json()["link"] is None assert res.json()["view"] == "hidden" + + +def test_delete_hidden_success(client, dbsession, db_button, db_category): + db_button.is_hidden = True + dbsession.commit() + res = client.delete(f"/category/{db_category.id}/button/{db_button.id}") + assert res.status_code == status.HTTP_200_OK + q = dbsession.query(Button).filter(Button.id == db_button.id) + assert not q.one_or_none() + From 098be3b7b1ecd46678250abaa9e86fffccfde2b0 Mon Sep 17 00:00:00 2001 From: Timur Enikeev Date: Tue, 30 Jul 2024 01:02:10 -0400 Subject: [PATCH 3/6] Fix post route --- services_backend/routes/button.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/services_backend/routes/button.py b/services_backend/routes/button.py index 37d88b4..94cfd28 100644 --- a/services_backend/routes/button.py +++ b/services_backend/routes/button.py @@ -98,20 +98,7 @@ def create_button( button.optional_scopes = optional_scopes db.session.add(button) db.session.flush() - view = ButtonView.HIDDEN if button.is_hidden else ButtonView.ACTIVE - result = { - "id": button.id, - "icon": button.icon, - "name": button.name, - "link": button.link if view == ButtonView.ACTIVE else None, - "order": button.order, - "type": button.type, - "view": view.value, - "required_scopes": button.required_scopes, - "optional_scopes": button.optional_scopes, - "scopes": [], - } - return result + return button @button.get("", response_model=ButtonsGet, response_model_exclude_unset=True) From 2f7531f241f07e0af4d1c49b2b1d3ad0e6229705 Mon Sep 17 00:00:00 2001 From: Timur Enikeev Date: Tue, 30 Jul 2024 01:04:17 -0400 Subject: [PATCH 4/6] Fix bool type in tests --- tests/api/button.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/button.py b/tests/api/button.py index df3dcd7..b3dc9e3 100644 --- a/tests/api/button.py +++ b/tests/api/button.py @@ -275,7 +275,7 @@ def test_get_hidden_by_id_success(client, dbsession, db_button, db_category): def test_patch_to_hide_success(client, db_button, db_category): - body = {"is_hidden": "True"} + body = {"is_hidden": True} res = client.patch(f"/category/{db_category.id}/button/{db_button.id}", data=json.dumps(body)) assert res.status_code == status.HTTP_200_OK res_body = res.json() From 67e865424c6b3102f7f30ead3164165b32635a57 Mon Sep 17 00:00:00 2001 From: Timur Enikeev Date: Tue, 30 Jul 2024 01:06:43 -0400 Subject: [PATCH 5/6] Format fix in tests --- tests/api/button.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/api/button.py b/tests/api/button.py index b3dc9e3..a789d3f 100644 --- a/tests/api/button.py +++ b/tests/api/button.py @@ -298,4 +298,3 @@ def test_delete_hidden_success(client, dbsession, db_button, db_category): assert res.status_code == status.HTTP_200_OK q = dbsession.query(Button).filter(Button.id == db_button.id) assert not q.one_or_none() - From 7cd7bcdfed9330a79800d2c5d397fa06d564a27a Mon Sep 17 00:00:00 2001 From: Timur Enikeev Date: Tue, 30 Jul 2024 02:11:45 -0400 Subject: [PATCH 6/6] Fix visability of hidden buttons from categories --- services_backend/routes/category.py | 6 ++++-- tests/api/category.py | 20 +++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/services_backend/routes/category.py b/services_backend/routes/category.py index 07b1fe0..5fa0fdf 100644 --- a/services_backend/routes/category.py +++ b/services_backend/routes/category.py @@ -109,7 +109,9 @@ def get_categories( for button in row.buttons: view = ButtonView.ACTIVE scopes = set() - if button.required_scopes - user_scopes: + if button.is_hidden: + view = ButtonView.HIDDEN + elif button.required_scopes - user_scopes: view = ButtonView.BLOCKED else: scopes |= button.required_scopes @@ -119,7 +121,7 @@ def get_categories( "id": button.id, "icon": button.icon, "name": button.name, - "link": (button.link if view == ButtonView.ACTIVE else None), + "link": button.link if view == ButtonView.ACTIVE else None, "order": button.order, "type": button.type, "view": view.value, diff --git a/tests/api/category.py b/tests/api/category.py index 255571d..faa104e 100644 --- a/tests/api/category.py +++ b/tests/api/category.py @@ -187,9 +187,10 @@ def test_delete_order(db_category, client): res = client.get(f"/category/{res1.json()['id']}") assert res.json()['order'] == 1 + client.delete(f"/category/{res1.json()['id']}") -def test_scopes(client, mocker: MockerFixture): +def test_scopes(client, dbsession, mocker: MockerFixture): user_mock = mocker.patch('auth_lib.fastapi.UnionAuth.__call__') user_mock.return_value = { "session_scopes": [{"id": 0, "name": "string", "comment": "string"}], @@ -238,3 +239,20 @@ def test_scopes(client, mocker: MockerFixture): res8 = client.get(f'/category/{id_}') assert res8.status_code == status.HTTP_404_NOT_FOUND + + category = dbsession.query(Category).filter(Category.id == id_).one_or_none() + dbsession.delete(category) + dbsession.commit() + + +def test_get_hidden_button_success(client, dbsession, db_button): + db_button.is_hidden = True + dbsession.commit() + res = client.get('/category', params={"info": "buttons"}) + assert res.status_code == status.HTTP_200_OK + res_body = res.json() + assert len(res_body) == 1 + assert len(res_body[0]["buttons"]) == 1 + assert res_body[0]["buttons"][0]["id"] == db_button.id + assert res_body[0]["buttons"][0]["view"] == "hidden" + assert "link" not in res_body[0]["buttons"][0]