Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# services-api

Ссылки:

1) Backend разработка – https://github.com/profcomff/.github/wiki/%5Bdev%5D-Backend-разработка


Для запуска проекта нужно иметь доступ к БД профкома/иметь локальную БД.

Локальную БД можно поднять так:
- Установить Docker
- В терминале запустить: ```docker run -d -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust --name db-services-backend postgres:15```


Переменные:
1) DB_DSN = 'postgres://логин:пароль@адрес:порт/бд'
55 changes: 48 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,68 @@
# services-api

Бэкэдн сервисов приложения Твой ФФ для профкома ФФ МГУ
Бэкэдн сервисов приложения Твой ФФ для профкома ФФ МГУ. Реализует логику работы с кнопками и категориями в приложении.
Репозиторий был создан для упрощения работы фронтэнд-разработчиков с бэкэндом сервисов, для переноса данных кнопок и категорий из захардкодженного json файла в Postgresql базу данных (cringe) и для разграничения доступа.

## Запуск
## Функционал

- Создание кнопок и категорий для отображения на фронте (в приложении)
- Управление доступами к категориям кнопок
- Редактирование любых атрибутов/полей кнопое и категорий


## Разработка
Backend разработка – https://github.com/profcomff/.github/wiki/%5Bdev%5D-Backend-разработка

CONTRIBUTING.md - [CONTRIBUTING.md](services-api/CONTRIBUTING.md)

## Quick Start

1) Перейдите в папку проекта

2) Создайте виртуальное окружение командой:
```console
foo@bar:~$ python3 -m venv ./venv/
```

3) Установите библиотеки
```console
foo@bar:~$ pip install -m requirements.txt
```
4) Запускайте приложение!
4) Установите все переменные окружения (см. CONTRIBUTING.md)

5) Запускайте приложение!
```console
foo@bar:~$ python -m services-backend
```

## ENV-file description

DB_DSN=
## Использование
1) Создание категории кнопок
*Необходимо иметь права services.category.create*
1. Создать новую категорию по запросу `POST /category` с телом `{"name": "имя_категории", "type": "тип отображения категории в приложении"}`

2. *Необходимо иметь права services.button.create* Создать в категории новую кнопку по запросу `POST /category/id_категории/button` с телом `{"name": "имя кнопки", "icon": "ссылка на иконку", "link": "ссылка сервиса, на которую ведет кнопка", "type": "тип ссылки"}`
Comment thread
Wudext marked this conversation as resolved.

3. *Опционально* Навесить права запросом `POST /category/{category_id}/scope` с телом `{"name": "название права доступа"}`


2) Получение категорий кнопок
*Нет необходимых прав*
1. Получить категории по запросу `GET /category`
2. *Опционально* Выбрать отображение кнопок принадлежащих категории по запросу `GET /category?info=buttons`


3) Удаление категории кнопок. *Необходимо иметь права services.category.delete*

ВАЖНОЕ УТОЧНЕНИЕ: При удалении категории все кнопки, принадлежащей ей также удаляются.
1. Удалить категорию кнопок по запросу 'DELETE /category/{category_id}

## Параметризация и плагины:
Никаких настроек кроме стандартных нет

## Ссылки:
Документация проекта - https://api.test.profcomff.com/?urls.primaryName=services#

Backend разработка – https://github.com/profcomff/.github/wiki/%5Bdev%5D-Backend-разработка

CONTRIBUTING.md - [CONTRIBUTING.md](services-api/CONTRIBUTING.md)

---
12 changes: 5 additions & 7 deletions migrations/versions/6a486347af93_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ def upgrade():
op.add_column('button', sa.Column('link', sa.String(), nullable=True))
op.add_column('button', sa.Column('type', sa.String(), nullable=True))
conn = op.get_bind()
res = conn.execute(sa.text("select id from button")).fetchall()
res = conn.execute(sa.text("select * from button")).fetchall()
for i in range(0, len(res)):
conn.execute(
sa.text(
f"""UPDATE "button"
SET "order"={i + 1},
"link"='#',
Comment thread
Wudext marked this conversation as resolved.
"type"='external'
"type"='external'
WHERE id={res[i][0]}"""
)
)
Expand All @@ -39,13 +39,13 @@ def upgrade():
op.alter_column('button', 'category_id', existing_type=sa.INTEGER(), nullable=False)
op.alter_column('button', 'icon', existing_type=sa.VARCHAR(), nullable=False)
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)):
res_c = conn.execute(sa.text("select id from category")).fetchall()
for i in range(0, len(res_c)):
conn.execute(
sa.text(
f"""UPDATE "category"
SET "order"={i + 1}
WHERE id={res[i][0]}"""
WHERE id={res_c[i][0]}"""
)
)
op.alter_column('category', 'order', nullable=False)
Expand All @@ -55,7 +55,6 @@ def upgrade():


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('category', 'type', existing_type=sa.VARCHAR(), nullable=True)
op.alter_column('category', 'name', existing_type=sa.VARCHAR(), nullable=True)
op.drop_column('category', 'order')
Expand All @@ -65,4 +64,3 @@ def downgrade():
op.drop_column('button', 'type')
op.drop_column('button', 'link')
op.drop_column('button', 'order')
# ### end Alembic commands ###
58 changes: 58 additions & 0 deletions migrations/versions/d35e88f39f85_fixing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Fixing

Revision ID: d35e88f39f85
Revises: 660bb7891726
Create Date: 2023-04-09 11:28:59.326067

"""
import operator

import sqlalchemy as sa
from alembic import op


# revision identifiers, used by Alembic.
revision = 'd35e88f39f85'
down_revision = '660bb7891726'
branch_labels = None
depends_on = None


def upgrade():
conn = op.get_bind()
res_c = conn.execute(sa.text("select * from category ORDER BY category.order")).fetchall()
for category in res_c:
res_b = conn.execute(
sa.text(f"select * from button WHERE category_id={category[0]} ORDER BY button.order")
).fetchall()
for i in range(0, len(res_b)):
conn.execute(
sa.text(
f"""UPDATE "button"
SET "order"={i + 1},
"link"='{res_b[i][5]}',
"type"='{res_b[i][6]}'
WHERE id={res_b[i][0]}"""
)
)


def downgrade():
k = 0
conn = op.get_bind()
res_c = conn.execute(sa.text("select * from category ORDER BY category.order")).fetchall()
for category in res_c:
res_b = conn.execute(
sa.text(f"select * from button WHERE category_id={category[0]} ORDER BY button.order")
).fetchall()
for i in range(0, len(res_b)):
conn.execute(
sa.text(
f"""UPDATE "button"
SET "order"={k + 1},
"link"='{res_b[i][5]}',
"type"='{res_b[i][6]}'
WHERE id={res_b[i][0]}"""
)
)
k += 1
15 changes: 13 additions & 2 deletions services_backend/models/database.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from __future__ import annotations

from enum import Enum

from sqlalchemy import Enum as DbEnum
from sqlalchemy import ForeignKey, Integer, String
from sqlalchemy.orm import Mapped, mapped_column, relationship

Expand All @@ -11,10 +14,18 @@ class Category(Base):
order: Mapped[int] = mapped_column(Integer, default=1)
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")
buttons: Mapped[list[Button]] = relationship(
"Button", back_populates="category", foreign_keys="Button.category_id", order_by='Button.order'
)
scopes: Mapped[list[Scope]] = relationship("Scope", back_populates="category")


class Type(str, Enum):
INAPP: str = "inapp"
INTERNAL: str = "internal"
EXTERNAL: str = "external"


class Button(Base):
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String)
Expand All @@ -23,7 +34,7 @@ class Button(Base):
category: Mapped[Category] = relationship("Category", back_populates="buttons", foreign_keys=[category_id])
icon: Mapped[str] = mapped_column(String)
link: Mapped[str] = mapped_column(String)
type: Mapped[str] = mapped_column(String)
type: Mapped[Type] = mapped_column(DbEnum(Type, native_enum=False), nullable=False)


class Scope(Base):
Expand Down
6 changes: 4 additions & 2 deletions services_backend/routes/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ def create_button(
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")
last_button = db.session.query(Button).order_by(Button.order.desc()).first()
last_button = (
db.session.query(Button).filter(Button.category_id == category_id).order_by(Button.order.desc()).first()
)
button = Button(**button_inp.dict(exclude_none=True))
button.category_id = category_id
if last_button:
Expand Down Expand Up @@ -81,7 +83,7 @@ def get_button(
def remove_button(
button_id: int,
category_id: int,
user=Depends(UnionAuth(['services.button.remove'])),
user=Depends(UnionAuth(['services.button.delete'])),
):
"""Удалить кнопку

Expand Down
2 changes: 1 addition & 1 deletion services_backend/routes/category.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from fastapi_sqlalchemy import db
from sqlalchemy.orm import joinedload

from ..models.database import Button, Category, Scope
from ..models.database import Button, Category
from .models.category import CategoryCreate, CategoryGet, CategoryUpdate


Expand Down
35 changes: 19 additions & 16 deletions services_backend/routes/models/button.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
from pydantic import Field

from ...models.database import Type
from .base import Base


class ButtonCreate(Base):
icon: str | None
name: str | None
link: str | None
type: str | None
icon: str = Field(description='Иконка кнопки')
name: str = Field(description='Название кнопки')
link: str = Field(description='Ссылка, на которую перенаправляет кнопка')
type: Type = Field(description='Тип открываемой ссылки (Ссылка приложения/Браузер в приложении/Браузер')


class ButtonUpdate(Base):
category_id: int | None
icon: str | None
name: str | None
order: int | None
link: str | None
type: str | None
category_id: int | None = Field(description='Айди категории')
icon: str | None = Field(description='Иконка кнопки')
name: str | None = Field(description='Название кнопки')
order: int | None = Field(description='Порядок, в котором отображаются кнопки')
link: str | None = Field(description='Ссылка, на которую перенаправляет кнопка')
type: str | None = Field(description='Тип открываемой ссылки (Ссылка приложения/Браузер в приложении/Браузер')


class ButtonGet(Base):
id: int
order: int
icon: str | None
name: str | None
link: str | None
type: str | None
id: int = Field(description='Айди кнопки')
order: int = Field(description='Порядок, в котором отображаются кнопки')
icon: str | None = Field(description='Иконка кнопки')
name: str | None = Field(description='Название кнопки')
link: str | None = Field(description='Ссылка, на которую перенаправляет кнопка')
type: str | None = Field(description='Тип открываемой ссылки (Ссылка приложения/Браузер в приложении/Браузер')
6 changes: 4 additions & 2 deletions services_backend/routes/models/category.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from pydantic import Field

from .base import Base
from .button import ButtonGet
from .scope import ScopeGet


class CategoryCreate(Base):
type: str | None
name: str | None
type: str = Field(description='Тип отображения категории')
Comment thread
Wudext marked this conversation as resolved.
name: str = Field(description='Имя категории')


class CategoryUpdate(Base):
Expand Down
12 changes: 6 additions & 6 deletions tests/api/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def test_post_success(self, client, db_category, dbsession):
"icon": "https://lh3.googleusercontent.com/yURn6ISxDySTdXZAW2PUcADMnU3y9YX0M1RyXOH8a3sa1Tr0pHhPLGw5BKuiLiXa3Eh0fyHm7Dfsd9FodK3fxJge6g=w640-h400-e365-rj-sc0x00ffffff",
"name": "string",
"link": "google.com",
"type": "test",
"type": "inapp",
}
res = client.post(f"/category/{db_category.id}/button/", data=json.dumps(body))
assert res.status_code == status.HTTP_200_OK
Expand Down Expand Up @@ -57,7 +57,7 @@ def test_delete_by_id_success(self, client, dbsession, db_button, db_category):
assert get_res.status_code == status.HTTP_404_NOT_FOUND

def test_patch_by_id_success(self, db_button, client, db_category):
body = {"icon": "cool icon", "name": "nice name", "order": 2, "link": "ya.ru", "type": "nice type"}
body = {"icon": "cool icon", "name": "nice name", "order": 2, "link": "ya.ru", "type": "inapp"}
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()
Expand Down Expand Up @@ -99,7 +99,7 @@ def test_create_first(self, client, db_button, db_category):
"icon": "test",
"name": "test",
"link": "test",
"type": "test",
"type": "inapp",
}

res = client.post(f"/category/{db_category.id}/button/", data=json.dumps(body))
Expand All @@ -115,7 +115,7 @@ def test_patch_order_fail(self, client, db_button, db_category):
"icon": "test",
"name": "new",
"link": "test",
"type": "test",
"type": "inapp",
}
res1 = client.post(f"/category/{db_category.id}/button/", data=json.dumps(body))
assert res1.status_code == status.HTTP_200_OK
Expand All @@ -132,7 +132,7 @@ def test_patch_negative_order_fail(self, db_button, client, db_category):
"icon": "test",
"name": "new",
"link": "test",
"type": "test",
"type": "inapp",
}
res = client.post(f"/category/{db_category.id}/button/", data=json.dumps(body))
res1 = client.patch(f"/category/{db_category.id}/button/{res.json()['id']}", data=json.dumps({"order": -10}))
Expand All @@ -143,7 +143,7 @@ def test_delete_order(self, db_button, client, db_category):
"icon": "test",
"name": "new",
"link": "test",
"type": "test",
"type": "inapp",
}
res1 = client.post(f"/category/{db_category.id}/button/", data=json.dumps(body))
assert res1.status_code == status.HTTP_200_OK
Expand Down
2 changes: 1 addition & 1 deletion tests/api/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def db_category(dbsession):

@pytest.fixture
def db_button(dbsession, db_category):
_button = Button(name='button', category_id=db_category.id, icon='test', link='g', type='d')
_button = Button(name='button', category_id=db_category.id, icon='test', link='g', type='inapp')
dbsession.add(_button)
dbsession.commit()
_button = dbsession.query(Button).filter(Button.id == _button.id).one_or_none()
Expand Down