diff --git a/.gitignore b/.gitignore index 8cb5f06..e29c056 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,9 @@ __pycache__/ *.pyc *.pyo *.pyd -*.local \ No newline at end of file +*.local + +# database +test.db +pre-production.db +production.db diff --git a/Makefile b/Makefile index b78ebc1..4804913 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,7 @@ win_format: up: - uvicorn $(CODE).app:app --reload + uvicorn $(CODE).app:app --host=0.0.0.0 --reload ci: lint test diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/accounts/__init__.py b/src/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/accounts/api.py b/src/accounts/api.py new file mode 100644 index 0000000..5f568ed --- /dev/null +++ b/src/accounts/api.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter +from .client_account.routers import client_router as client_router +from .developer_account.routers import developer_router + +accounts_router = APIRouter() + +accounts_router.include_router(client_router, prefix='/clients', tags=['Client']) +accounts_router.include_router(developer_router, prefix='/developers', tags=['Developer']) diff --git a/src/accounts/client_account/__init__.py b/src/accounts/client_account/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/accounts/client_account/models.py b/src/accounts/client_account/models.py new file mode 100644 index 0000000..71fde65 --- /dev/null +++ b/src/accounts/client_account/models.py @@ -0,0 +1,18 @@ +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, sql + +from ...db.db import Base + + +class Client(Base): + __tablename__ = 'client' + + id = Column(Integer, primary_key=True, index=True, unique=True) + name = Column(String, nullable=False) + avatar = Column(String, nullable=True) + is_active = Column(Boolean, default=False, nullable=False) + date_create = Column(DateTime(timezone=True), server_default=sql.func.now()) + date_block = Column(DateTime, default=None, nullable=True) + owner_id = Column(String, ForeignKey('user.id'), nullable=False) + + +clients = Client.__table__ diff --git a/src/accounts/client_account/routers.py b/src/accounts/client_account/routers.py new file mode 100644 index 0000000..eed063a --- /dev/null +++ b/src/accounts/client_account/routers.py @@ -0,0 +1,52 @@ +from fastapi import APIRouter, Depends, status, HTTPException +from pydantic.types import UUID4 + +from .schemas import ClientDB, Client, ClientUpdate, ClientAndOwnerCreate, ClientsPage +from .services import add_client, update_client, get_clients_page, get_client_page, \ + block_client, get_dev_client_page, get_client_info +from src.users.models import UserTable +from src.users.logic import developer_user, any_user, get_owner_with_superuser, get_client_users, default_uuid +from ..employee_account.routers import employee_router + +client_router = APIRouter() + + +@client_router.get("/", response_model=ClientsPage, status_code=status.HTTP_200_OK) +async def clients_list(user: UserTable = Depends(developer_user), last_id: int = 0, limit: int = 9): + return await get_clients_page(last_id, limit) + + +@client_router.post("/", response_model=ClientDB, status_code=status.HTTP_201_CREATED) +async def create_client(item: ClientAndOwnerCreate, user: UserTable = Depends(developer_user)): + return await add_client(item) + + +@client_router.get("/{id}", status_code=status.HTTP_200_OK) +async def client(id: int, user: UserTable = Depends(any_user), last_id: UUID4 = default_uuid, limit: int = 9): + if user.is_superuser: + return await get_dev_client_page(id, last_id, limit) + elif await get_client_users(id, user): + return await get_client_page(id, last_id, limit) + else: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + + +@client_router.get("/{id}/info", response_model=Client, status_code=status.HTTP_200_OK) +async def client_info(id: int, user: UserTable = Depends(any_user)): + user = await get_owner_with_superuser(id, user) + return await get_client_info(id) + + +@client_router.patch("/{id}/info", response_model=ClientDB, status_code=status.HTTP_201_CREATED) +async def update_client_by_id(id: int, item: ClientUpdate, user: UserTable = Depends(any_user)): + # TODO разделить изменение аватарки владельцем и изменение владельца владельцем или разработчиком + user = await get_owner_with_superuser(id, user) + return await update_client(id, item) + + +@client_router.patch("/{id}/block", response_model=ClientDB, status_code=status.HTTP_201_CREATED) +async def block_client_by_id(id: int, user: UserTable = Depends(developer_user)): + return await block_client(id) + + +client_router.include_router(employee_router) diff --git a/src/accounts/client_account/schemas.py b/src/accounts/client_account/schemas.py new file mode 100644 index 0000000..24966f0 --- /dev/null +++ b/src/accounts/client_account/schemas.py @@ -0,0 +1,91 @@ +from datetime import datetime +from typing import Optional, List + +from pydantic import BaseModel, EmailStr, validator + +from ...reference_book.schemas import LicenceDB, SoftwareDB, Licence +from ...users.schemas import UserDB, generate_pwd, Employee, EmployeeList + + +class ClientBase(BaseModel): + name: str + avatar: str + + +class ClientCreate(ClientBase): + name: Optional[str] + owner_id: Optional[str] + + +class ClientAndOwnerCreate(ClientCreate): + owner_name: str + surname: str + patronymic: Optional[str] + email: EmailStr + password: str = generate_pwd() + avatar: Optional[str] + owner_avatar: Optional[str] + is_active: Optional[bool] = True + is_superuser: Optional[bool] = False + is_verified: Optional[bool] = False + owner_licence: int + licences_list: List[int] + + @validator('password') + def valid_password(cls, v: str): + if len(v) < 6: + raise ValueError('Password should be at least 6 characters') + return v + + +class ClientUpdate(ClientBase): # TODO доработать изменение заказчика + name: Optional[str] + owner_id: Optional[str] + avatar: Optional[str] + + +class ClientDB(ClientBase): + id: int + is_active: bool + avatar: str + date_create: datetime + date_block: Optional[datetime] + owner_id: str + + +class ClientShort(ClientDB): + is_active: bool + owner: UserDB + count_employees: int = 0 + + +class Client(ClientBase): + id: int + is_active: bool + date_create: datetime + owner: Employee + licences: List[Licence] + + +class ClientPage(BaseModel): + client: Client + employees_list: List[EmployeeList] + licences_list: List[Licence] + + +class DevClientPage(BaseModel): + client: Client + employees_list: List[EmployeeList] = [] + software_list: List[SoftwareDB] + + +class ClientsPage(BaseModel): + clients_list: List[ClientShort] + licences_list: List[LicenceDB] + + +class EmployeePage(BaseModel): + employee: Employee + client: Client + licences: List[Licence] + diff --git a/src/accounts/client_account/services.py b/src/accounts/client_account/services.py new file mode 100644 index 0000000..168c3c9 --- /dev/null +++ b/src/accounts/client_account/services.py @@ -0,0 +1,220 @@ +from datetime import datetime +from typing import List, Optional + +from fastapi import HTTPException, status +from fastapi_users.router import ErrorCode +from pydantic.types import UUID4 +from sqlalchemy import desc + +from .schemas import ClientCreate, ClientUpdate, ClientAndOwnerCreate, ClientsPage, ClientShort, ClientPage, ClientDB, \ + Client, DevClientPage +from ...db.db import database +from .models import clients +from ...desk.models import appeals +from ...errors import Errors +from ...reference_book.schemas import LicenceDB +from ...reference_book.services import add_client_licence, get_client_licences, get_software_db_list, \ + add_employee_licence, get_employee_licence, get_licences_db +from ...users.logic import all_users, get_or_404, pre_update_user, user_is_active, get_user_by_email, default_uuid +from ...users.models import users +from ...users.schemas import UserCreate, OwnerCreate, Employee, UserDB, EmployeeList, EmployeeUpdate +from ...service import send_mail, Email + + +async def get_count_appeals(employee_id: str) -> int: + query = appeals.select().where(appeals.c.author_id == employee_id) + result = await database.fetch_all(query=query) + return len(result) + + +async def get_employees(client_id: int, last_id: UUID4 = default_uuid, limit: int = 9) -> List[EmployeeList]: + query = users.select()\ + .where((users.c.client_id == client_id) & (users.c.id > str(last_id))).order_by(desc(users.c.id)).limit(limit) + result = await database.fetch_all(query=query) + employees_list = [] + for employee in result: + employee = dict(employee) + licence: LicenceDB = await get_employee_licence(str(employee["id"])) + count_appeals: int = await get_count_appeals(str(employee["id"])) + employees_list.append(EmployeeList(**dict({**employee, "licence": licence, "count_appeals": count_appeals}))) + return employees_list + + +async def get_count_employees(client_id: int) -> int: + result = await database.fetch_all(users.select().where(users.c.client_id == client_id)) + return len(result) + + +async def get_clients_db() -> List[ClientDB]: + result = await database.fetch_all(clients.select()) + return [ClientDB(**dict(client)) for client in result] + + +async def get_clients(last_id: int = 0, limit: int = 9) -> List[ClientShort]: + query = clients.select().where(clients.c.id > last_id).limit(limit) + result = await database.fetch_all(query=query) + clients_list = [] + for client in result: + client = dict(client) + owner = await get_or_404(UUID4(client["owner_id"])) + count_employees = await get_count_employees(client["id"]) + clients_list.append(ClientShort(**dict({**client, + "owner": owner, + "count_employees": count_employees}))) + return clients_list + + +async def get_clients_page(last_id: int = 0, limit: int = 9) -> ClientsPage: + clients_list = await get_clients(last_id, limit) + licences_list = await get_licences_db() + return ClientsPage(**dict({"clients_list": clients_list, "licences_list": licences_list})) + + +async def get_client_info(client_id: int) -> Client: + client = await get_client(client_id) + return client + + +async def get_client_page(client_id: int, last_id: UUID4 = default_uuid, limit: int = 9) -> ClientPage: + client = await get_client(client_id) + employees_list = await get_employees(client_id, last_id, limit) + licences_list = await get_client_licences(client_id) + return ClientPage(**dict({"client": client, + "employees_list": employees_list, + "licences_list": licences_list})) + + +async def get_dev_client_page(client_id: int, last_id: UUID4 = default_uuid, limit: int = 9) -> DevClientPage: + client = await get_client(client_id) + employees_list = await get_employees(client_id, last_id, limit) + software_list = await get_software_db_list() + return DevClientPage(**dict({"client": client, + "employees_list": employees_list, + "software_list": software_list})) + + +async def get_client(client_id: int) -> Optional[Client]: + query = clients.select().where(clients.c.id == client_id) + client = await database.fetch_one(query=query) + if client is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=Errors.CLIENT_NOT_FOUND) + client = dict(client) + owner = await get_or_404(UUID4(str(client["owner_id"]))) + licences_list = await get_client_licences(client_id) + return Client(**dict({**client, + "owner": owner, + "licences": licences_list})) + + +async def get_client_db(client_id: int) -> Optional[ClientDB]: + query = clients.select().where(clients.c.id == client_id) + client = await database.fetch_one(query=query) + if client: + return ClientDB(**dict(client)) + return None + + +async def add_client(data: ClientAndOwnerCreate) -> Optional[ClientDB]: + client = ClientCreate(**dict(data)) + query = clients.insert().values({**client.dict(), "is_active": False, "owner_id": "undefined"}) + if await get_user_by_email(data.email): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=Errors.COMPANY_IS_EXIST, + ) + try: + client_id = await database.execute(query) + except Exception: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=Errors.COMPANY_IS_EXIST, + ) + for licence_id in data.licences_list: + licence = await add_client_licence(client_id, licence_id) + owner = OwnerCreate( + email=data.email, + password=data.password, + name=data.owner_name, + surname=data.surname, + patronymic=data.patronymic, + avatar=data.owner_avatar, + client_id=client_id, + is_owner=True, + date_reg=datetime.utcnow(), + ) + owner = await add_owner(client_id, owner) + await add_employee_licence(str(owner.id), data.owner_licence) + new_client = await get_client_db(client_id) + return new_client + + +async def update_client(client_id: int, client: ClientUpdate) -> Optional[ClientDB]: + new_client_dict = dict(client) + old_client_dict = dict(await get_client_db(client_id)) + for field in new_client_dict: + if new_client_dict[field]: + old_client_dict[field] = new_client_dict[field] + query = clients.update().where(clients.c.id == client_id).values(**old_client_dict) + updated_client = await database.execute(query) + updated_client = await get_client_db(client_id) + return updated_client + + +async def block_client(client_id: int) -> Optional[ClientDB]: + new_client = ClientUpdate(is_activa=False) + updated_client = await update_client(client_id, new_client) + return updated_client + + +async def get_client_owner(client_id: int) -> Employee: + query = users.select().where((users.c.client_id == client_id) & (users.c.is_owner == 1)) + owner = await database.fetch_one(query=query) + if owner is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=Errors.USER_NOT_FOUND + ) + owner = dict(owner) + licence: LicenceDB = await get_employee_licence(str(owner["id"])) + return Employee(**dict({**owner, "licence": licence})) + + +async def update_client_owner(client_id: int, new_owner_id: UUID4) -> Optional[UserDB]: + client = await get_client_db(client_id) + if client and await user_is_active(new_owner_id): + new_client = ClientUpdate(owner_id=str(new_owner_id)) + if client.owner_id == "undefined": + client = await update_client(client_id, new_client) + new_owner = await get_or_404(new_owner_id) + else: + update_old = EmployeeUpdate(is_owner=False) + update_new = EmployeeUpdate(is_owner=True) + old_owner = await pre_update_user(UUID4(client.owner_id), update_old) + new_owner = await pre_update_user(new_owner_id, update_new) + client = await update_client(client_id, new_client) + return new_owner + return None + + +async def add_owner(client_id: int, owner: OwnerCreate): + user = UserCreate(**owner.dict()) + try: + created_owner = await all_users.create_user(user, safe=True) + except Exception: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ErrorCode.REGISTER_USER_ALREADY_EXISTS, + ) + + await send_mail_with_owner_pwd(owner) + updated_owner = await update_client_owner(client_id, created_owner.id) + return updated_owner + + +async def send_mail_with_owner_pwd(user: OwnerCreate) -> None: + message = f"Добро пожаловать в UDV Service Desk!\n\n" \ + f"Ваш логин в системе: {user.email}\nВаш пароль: {user.password}" + email = Email(recipient=user.email, title="Регистрация в UDV Service Desk", message=message) + await send_mail(email) diff --git a/src/accounts/developer_account/__init__.py b/src/accounts/developer_account/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/accounts/developer_account/routers.py b/src/accounts/developer_account/routers.py new file mode 100644 index 0000000..778eb66 --- /dev/null +++ b/src/accounts/developer_account/routers.py @@ -0,0 +1,51 @@ +from typing import List + +from fastapi import APIRouter, Depends, status, Response, HTTPException +from pydantic.types import UUID4 + +from .services import get_developer, add_developer, delete_developer +from src.users.models import UserTable +from src.users.logic import developer_user, get_developers, change_pwd, pre_update_developer, \ + get_email_with_changed_pwd, default_uuid +from src.users.schemas import UserDB, UserUpdate, DeveloperList, DeveloperCreate +from .statistics.routers import statistics_router + + +developer_router = APIRouter() + + +@developer_router.get("/", response_model=List[DeveloperList], status_code=status.HTTP_200_OK) +async def developers_list(user: UserTable = Depends(developer_user), last_id: UUID4 = default_uuid, limit: int = 9): + return await get_developers(last_id, limit) + + +@developer_router.get("/{id:uuid}", response_model=UserDB, status_code=status.HTTP_200_OK) +async def developer(id: UUID4, user: UserTable = Depends(developer_user)): + return await get_developer(str(id)) + + +@developer_router.post("/") +async def create_developer(item: DeveloperCreate, user: UserTable = Depends(developer_user)): + return await add_developer(item) + + +@developer_router.patch("/{id:uuid}", response_model=UserDB, status_code=status.HTTP_201_CREATED) +async def update_developer_by_id(id: UUID4, item: UserUpdate, user: UserTable = Depends(developer_user)): + return await pre_update_developer(id, item) + + +@developer_router.patch("/{id:uuid}/pwd", response_model=UserDB, status_code=status.HTTP_201_CREATED) +async def change_dev_pwd(id: UUID4, new_pwd: str, user: UserTable = Depends(developer_user)): + if str(user.id) == str(id): + email = await get_email_with_changed_pwd(new_pwd, user) + return await change_pwd(id, new_pwd, email) + else: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + + +@developer_router.delete("/{id:uuid}", response_class=Response, status_code=status.HTTP_204_NO_CONTENT) +async def delete_developer_by_id(id: UUID4, user: UserTable = Depends(developer_user)): + await delete_developer(id) + + +developer_router.include_router(statistics_router, prefix='/statistics', tags=['Statistics']) diff --git a/src/accounts/developer_account/services.py b/src/accounts/developer_account/services.py new file mode 100644 index 0000000..6228441 --- /dev/null +++ b/src/accounts/developer_account/services.py @@ -0,0 +1,44 @@ +from typing import Optional + +from fastapi import HTTPException, status +from fastapi_users.router import ErrorCode +from pydantic.types import UUID4 + +from src.db.db import database +from src.service import send_mail, Email +from src.users.logic import all_users, delete_user, pre_update_user +from src.users.models import users +from src.users.schemas import UserCreate, DeveloperCreate, UserUpdate, UserDB + + +async def get_developer(developer_id: str) -> Optional[UserDB]: + query = users.select().where((users.c.is_superuser == 1) & (users.c.id == developer_id)) + developer = await database.fetch_one(query=query) + if developer: + return UserDB(**dict(developer)) + return None + + +async def add_developer(developer: DeveloperCreate) -> UserDB: + developer = DeveloperCreate(**dict({**dict(developer), "is_superuser": True})) + try: + created_developer = await all_users.create_user(developer, safe=False) + except Exception: + print(Exception) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ErrorCode.REGISTER_USER_ALREADY_EXISTS, + ) + await send_mail_with_dev_pwd(developer) + return created_developer + + +async def send_mail_with_dev_pwd(user: DeveloperCreate) -> None: + message = f"Добро пожаловать в UDV Service Desk!\n\n" \ + f"Ваш логин в системе: {user.email}\nВаш пароль: {user.password}" + email = Email(recipient=user.email, title="Регистрация в UDV Service Desk", message=message) + await send_mail(email) + + +async def delete_developer(id: UUID4): + await delete_user(id) diff --git a/src/accounts/developer_account/statistics/__init__.py b/src/accounts/developer_account/statistics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/accounts/developer_account/statistics/routers.py b/src/accounts/developer_account/statistics/routers.py new file mode 100644 index 0000000..ef9df0d --- /dev/null +++ b/src/accounts/developer_account/statistics/routers.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter, Depends, status + +from src.accounts.developer_account.statistics.schemas import DevelopersStatistics, ClientsStatistics, AppealsStatistics +from src.accounts.developer_account.statistics.services import get_developers_statistics, get_clients_statistics, \ + get_appeals_statistics +from src.users.logic import developer_user +from src.users.models import UserTable + + +statistics_router = APIRouter() + + +@statistics_router.get("/appeals", response_model=AppealsStatistics, status_code=status.HTTP_200_OK) +async def developers_list(user: UserTable = Depends(developer_user)): + return await get_appeals_statistics() + + +@statistics_router.get("/clients", response_model=ClientsStatistics, status_code=status.HTTP_200_OK) +async def developer(user: UserTable = Depends(developer_user)): + return await get_clients_statistics() + + +@statistics_router.get("/developers", response_model=DevelopersStatistics, status_code=status.HTTP_200_OK) +async def developer(user: UserTable = Depends(developer_user)): + return await get_developers_statistics() diff --git a/src/accounts/developer_account/statistics/schemas.py b/src/accounts/developer_account/statistics/schemas.py new file mode 100644 index 0000000..48aa5c0 --- /dev/null +++ b/src/accounts/developer_account/statistics/schemas.py @@ -0,0 +1,56 @@ +from typing import List + +from pydantic.main import BaseModel + + +class SoftwareStatistics(BaseModel): + name: str + count_appeals: int + + +class ModuleStatistics(BaseModel): + name: str + count_appeals: int + + +class StatusStatistics(BaseModel): + name: str + count_appeals: int + + +class AppealsStatistics(BaseModel): + software_list: List[SoftwareStatistics] + modules_list: List[ModuleStatistics] + statuses_list: List[StatusStatistics] + + +class ClientStatistics(BaseModel): + name: str + count_appeals: int + open_statuses: int + closed: int + canceled: int + + +class ClientsStatistics(BaseModel): + clients_list: List[ClientStatistics] + + +class DeveloperStatistics(BaseModel): + name: str + count_appeals: int + open_statuses: int + closed_statuses: int + + +class DevelopersStatistics(BaseModel): + developers_list: List[DeveloperStatistics] + + +class StatusesDistribution(BaseModel): + new: int + registered: int + in_work: int + closed: int + canceled: int + reopen: int diff --git a/src/accounts/developer_account/statistics/services.py b/src/accounts/developer_account/statistics/services.py new file mode 100644 index 0000000..232de98 --- /dev/null +++ b/src/accounts/developer_account/statistics/services.py @@ -0,0 +1,127 @@ +from typing import List + +from src.accounts.client_account.services import get_clients_db +from src.accounts.developer_account.statistics.schemas import AppealsStatistics, ClientsStatistics, \ + DevelopersStatistics, DeveloperStatistics, SoftwareStatistics, ModuleStatistics, StatusStatistics, ClientStatistics +from src.desk.services import get_appeals_by_developer, get_appeals_by_client, \ + get_appeals_by_software, get_appeals_by_module, get_appeals_db, get_status_distribution, get_statuses_list +from src.reference_book.services import get_software_db_list, get_modules_db +from src.users.logic import get_developers_db + + +async def get_software_list_stat() -> List[SoftwareStatistics]: + softwares = await get_software_db_list() + software_list = [] + for software in softwares: + appeals = await get_appeals_by_software(software.id) + count_appeals = len(appeals) + software_list.append(SoftwareStatistics(name=software.name, count_appeals=count_appeals)) + return software_list + + +async def get_module_list_stat() -> List[ModuleStatistics]: + modules = await get_modules_db() + modules_list = [] + for module in modules: + appeals = await get_appeals_by_module(module.id) + count_appeals = len(appeals) + modules_list.append(ModuleStatistics(name=module.name, count_appeals=count_appeals)) + return modules_list + + +async def get_status_list_stat() -> List[StatusStatistics]: + appeals = await get_appeals_db() + return await get_statuses_list(appeals) + + +async def get_appeals_statistics() -> AppealsStatistics: + software_list = await get_software_list_stat() + modules_list = await get_module_list_stat() + statuses_list = await get_status_list_stat() + await quick_sort(software_list) + await quick_sort(modules_list) + await quick_sort(statuses_list) + software_list = software_list[::-1] + modules_list = modules_list[::-1] + statuses_list = statuses_list[::-1] + return AppealsStatistics(software_list=software_list, + modules_list=modules_list, + statuses_list=statuses_list) + + +async def get_clients_statistics() -> ClientsStatistics: + clients = await get_clients_db() + clients_list = [] + for client in clients: + appeals = await get_appeals_by_client(client.id) + statuses = await get_status_distribution(appeals) + count_appeals = len(appeals) + open_statuses = statuses.registered + statuses.in_work + statuses.reopen + closed = statuses.closed + canceled = statuses.canceled + clients_list.append(ClientStatistics( + name=client.name, + count_appeals=count_appeals, + open_statuses=open_statuses, + closed=closed, + canceled=canceled + )) + await quick_sort(clients_list) + clients_list = clients_list[::-1] + return ClientsStatistics(clients_list=clients_list) + + +async def get_developers_statistics() -> DevelopersStatistics: + developers = await get_developers_db() + developers_list = [] + for developer in developers: + appeals = await get_appeals_by_developer(str(developer.id)) + statuses = await get_status_distribution(appeals) + count_appeals = len(appeals) + open_statuses = statuses.registered + statuses.in_work + statuses.reopen + closed_statuses = statuses.canceled + statuses.closed + developers_list.append(DeveloperStatistics( + name=developer.name, + count_appeals=count_appeals, + open_statuses=open_statuses, + closed_statuses=closed_statuses + )) + await quick_sort(developers_list) + developers_list = developers_list[::-1] + return DevelopersStatistics(developers_list=developers_list) + + +async def partition(nums, low, high): + # Выбираем средний элемент в качестве опорного + # Также возможен выбор первого, последнего + # или произвольного элементов в качестве опорного + pivot = nums[(low + high) // 2].count_appeals + i = low - 1 + j = high + 1 + while True: + i += 1 + while nums[i].count_appeals < pivot: + i += 1 + + j -= 1 + while nums[j].count_appeals > pivot: + j -= 1 + + if i >= j: + return j + + # Если элемент с индексом i (слева от опорного) больше, чем + # элемент с индексом j (справа от опорного), меняем их местами + nums[i], nums[j] = nums[j], nums[i] + + +async def quick_sort(nums): + # Создадим вспомогательную функцию, которая вызывается рекурсивно + async def _quick_sort(items, low, high): + if low < high: + # This is the index after the pivot, where our lists are split + split_index = await partition(items, low, high) + await _quick_sort(items, low, split_index) + await _quick_sort(items, split_index + 1, high) + + await _quick_sort(nums, 0, len(nums) - 1) diff --git a/src/accounts/employee_account/__init__.py b/src/accounts/employee_account/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/accounts/employee_account/routers.py b/src/accounts/employee_account/routers.py new file mode 100644 index 0000000..0a84426 --- /dev/null +++ b/src/accounts/employee_account/routers.py @@ -0,0 +1,72 @@ +from typing import Optional + +from fastapi import APIRouter, status, Depends, HTTPException, Response +from pydantic.types import UUID4 + +from .services import add_employee, get_count_allowed_employees, get_employee, delete_employee, block_employee +from src.errors import Errors +from src.users.logic import get_owner, any_user, get_client_users_with_superuser, pre_update_user, change_pwd, \ + get_email_with_changed_pwd +from src.users.models import UserTable +from src.users.schemas import UserDB, PreEmployeeCreate, EmployeeUpdate +from ..client_account.schemas import EmployeePage +from ..client_account.services import update_client_owner + +employee_router = APIRouter() + + +@employee_router.get("/{id}/employees/{pk}", response_model=Optional[EmployeePage], status_code=status.HTTP_200_OK) +async def employee(id: int, pk: UUID4, user: UserTable = Depends(any_user)): + user = await get_client_users_with_superuser(id, user) + return await get_employee(id, pk) + + +@employee_router.post("/{id}/employees", response_model=UserDB, status_code=status.HTTP_201_CREATED) +async def create_employee(id: int, item: PreEmployeeCreate, user: UserTable = Depends(any_user)): + user = await get_owner(id, user) + if await get_count_allowed_employees(id) > 0: + return await add_employee(id, item) + else: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=Errors.CLIENT_HAS_NOT_FREE_VACANCIES, + ) + + +@employee_router.patch("/{id}/employees/{pk}", response_model=UserDB, status_code=status.HTTP_201_CREATED) +async def update_employee_by_id(id: int, pk: UUID4, item: EmployeeUpdate, user: UserTable = Depends(any_user)): + user = await get_owner(id, user) + return await pre_update_user(pk, item) + + +@employee_router.patch("/{id}/employees/{pk}/make_owner", response_model=UserDB, status_code=status.HTTP_201_CREATED) +async def make_employee_owner(id: int, pk: UUID4, user: UserTable = Depends(any_user)): + user = await get_owner(id, user) + return await update_client_owner(id, pk) + + +@employee_router.patch("/{id}/employees/{pk}/block", response_model=UserDB, status_code=status.HTTP_201_CREATED) +async def block_employee_by_id(id: int, pk: UUID4, user: UserTable = Depends(any_user)): + user = await get_owner(id, user) + return await block_employee(pk) + + +@employee_router.patch("/{id}/employees/{pk}/pwd", response_model=UserDB, status_code=status.HTTP_201_CREATED) +async def change_employee_pwd(id: int, pk: UUID4, new_pwd: str, user: UserTable = Depends(any_user)): + if str(user.id) == str(pk): + email = await get_email_with_changed_pwd(new_pwd, user) + return await change_pwd(pk, new_pwd, email) + else: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + + +@employee_router.delete("/{id}/employees/{pk}", response_class=Response, status_code=status.HTTP_204_NO_CONTENT) +async def delete_employee_by_id(id: int, pk: UUID4, user: UserTable = Depends(any_user)): + user = await get_owner(id, user) + if user.id == pk: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=Errors.IMPOSSIBLE_DELETE_OWNER, + ) + + await delete_employee(pk) diff --git a/src/accounts/employee_account/services.py b/src/accounts/employee_account/services.py new file mode 100644 index 0000000..1fc5bcd --- /dev/null +++ b/src/accounts/employee_account/services.py @@ -0,0 +1,73 @@ +from typing import Optional + +from fastapi import HTTPException, status +from fastapi_users.router import ErrorCode +from pydantic.types import UUID4 + +from src.accounts.client_account.schemas import EmployeePage +from src.accounts.client_account.services import get_client, get_count_employees +from src.db.db import database +from src.reference_book.services import get_client_licences, add_employee_licence +from src.service import send_mail, Email +from src.users.logic import all_users, update_user, delete_user, get_or_404 +from src.users.models import users +from src.users.schemas import EmployeeCreate, PreEmployeeCreate, UserDB, UserUpdate + + +async def get_employee(client_id: int, pk: UUID4) -> Optional[EmployeePage]: + employee = await database.fetch_one(users.select().where((users.c.client_id == client_id) & (users.c.id == pk))) + if employee: + employee = dict(employee) + client = await get_client(client_id) + client_licences = client.licences + return EmployeePage(**dict({"employee": employee, "client": client, "licences": client_licences})) + return None + + +async def get_count_allowed_employees(client_id: int) -> int: + count_allowed_employees = 0 + client_licences = await get_client_licences(client_id) + for licence in client_licences: + count_allowed_employees += licence.count_members + count_employees = await get_count_employees(client_id) + return count_allowed_employees - count_employees + + +async def add_employee(id: int, user: PreEmployeeCreate) -> UserDB: + user.client_id = id + user.is_owner = False + employee = EmployeeCreate(**user.dict()) + print(employee) + try: + created_user = await all_users.create_user(employee, safe=True) + except Exception: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ErrorCode.REGISTER_USER_ALREADY_EXISTS, + ) + licence_id = user.licence_id + licence = await add_employee_licence(created_user.id, licence_id) + + await send_mail_with_pwd(user) + return created_user + + +async def send_mail_with_pwd(user: PreEmployeeCreate) -> None: + message = f"Добро пожаловать в UDV Service Desk!\n\n" \ + f"Ваш логин в системе: {user.email}\nВаш пароль: {user.password}" + email = Email(recipient=user.email, title="Регистрация в UDV Service Desk", message=message) + await send_mail(email) + + +async def delete_employee(pk: UUID4): + # TODO заменить в обращениях пользователя автора на владельца компании + await delete_user(pk) + + +async def block_employee(pk: UUID4): + item = await get_or_404(pk) + update_employee = UserUpdate(**dict({**dict(item), "is_active": False})) + update_dict = update_employee.dict(exclude_unset=True, + exclude={"id", "email", "is_superuser", "is_verified"}) + updated_employee = await update_user(pk, update_dict) + return updated_employee diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..6c49219 --- /dev/null +++ b/src/app.py @@ -0,0 +1,46 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from src.db.db import database, engine +from .db.base import Base +from .accounts.api import accounts_router +from .desk.routes import router as desk_router +from .reference_book.api.routes import router as book_router +from .users.logic import create_developer +from .users.routers import router as users_routes + +Base.metadata.create_all(bind=engine) +app = FastAPI() + +origins = [ + "http://localhost:3000" +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.on_event('startup') +async def startup(): + await database.connect() + + +@app.on_event('shutdown') +async def shutdown(): + await database.disconnect() + + +@app.get('/', tags=['Home']) +def read_root(): + return {'Hello': 'World'} + + +app.include_router(accounts_router) +app.include_router(book_router, prefix='/references', tags=['Reference book']) +app.include_router(desk_router, prefix='/desk', tags=['Desk']) +app.include_router(users_routes) diff --git a/src/db/__init__.py b/src/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/db/base.py b/src/db/base.py new file mode 100644 index 0000000..fa3440e --- /dev/null +++ b/src/db/base.py @@ -0,0 +1,5 @@ +from .db import Base +from ..accounts.client_account import models +from ..reference_book import models +from ..desk import models +from ..users import models diff --git a/src/db/db.py b/src/db/db.py new file mode 100644 index 0000000..37d37db --- /dev/null +++ b/src/db/db.py @@ -0,0 +1,15 @@ +import databases +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base + + +# SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" +SQLALCHEMY_DATABASE_URL = "sqlite:///./pre-production.db" +# SQLALCHEMY_DATABASE_URL = "sqlite:///./production.db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) + +database = databases.Database(SQLALCHEMY_DATABASE_URL) +Base: DeclarativeMeta = declarative_base() diff --git a/src/desk/__init__.py b/src/desk/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/desk/models.py b/src/desk/models.py new file mode 100644 index 0000000..e47f3ed --- /dev/null +++ b/src/desk/models.py @@ -0,0 +1,57 @@ +import enum + +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, sql, Enum + +from ..db.db import Base + + +class StatusTasks(enum.Enum): + new = "New" + registered = "Registered" + in_work = "In work" + closed = "Closed" + canceled = "Canceled" + reopen = "Reopen" + + +class Appeal(Base): + __tablename__ = 'appeal' + + id = Column(Integer, primary_key=True, index=True, unique=True) + topic = Column(String(100), nullable=False) + text = Column(String(500), nullable=False) + client_id = Column(Integer, ForeignKey('client.id')) + author_id = Column(String, ForeignKey('user.id')) + responsible_id = Column(String, ForeignKey('user.id'), nullable=True) + status = Column(Enum(StatusTasks), default=StatusTasks.new) + date_create = Column(DateTime(timezone=True), server_default=sql.func.now()) + date_edit = Column(DateTime, default=None) + date_processing = Column(DateTime, default=None) + software_id = Column(Integer, ForeignKey('software.id'), nullable=False) + module_id = Column(Integer, ForeignKey('module.id'), nullable=False) + importance = Column(Integer, default=1) + + +class Comment(Base): + __tablename__ = 'comment' + + id = Column(Integer, primary_key=True, index=True, unique=True) + text = Column(String(300), nullable=False) + appeal_id = Column(Integer, ForeignKey('appeal.id'), nullable=False) + author_id = Column(String, ForeignKey('user.id'), nullable=False) + date_create = Column(DateTime(timezone=True), server_default=sql.func.now()) + + +class Attachment(Base): + __tablename__ = 'attachment' + + id = Column(Integer, primary_key=True, index=True, unique=True) + filename = Column(String(300), nullable=False) + appeal_id = Column(Integer, ForeignKey('appeal.id'), nullable=False) + author_id = Column(String, ForeignKey('user.id'), nullable=False) + date_create = Column(DateTime(timezone=True), server_default=sql.func.now()) + + +appeals = Appeal.__table__ +comments = Comment.__table__ +attachments = Attachment.__table__ diff --git a/src/desk/routes.py b/src/desk/routes.py new file mode 100644 index 0000000..c9683d9 --- /dev/null +++ b/src/desk/routes.py @@ -0,0 +1,91 @@ +from fastapi import APIRouter, status, Depends, Response, HTTPException, UploadFile, File +from typing import List + +from .services import get_all_appeals, get_appeal, get_comments, get_comment, \ + add_appeal, add_comment, update_appeal, update_comment, delete_comment, get_appeals_page, upload_attachment, \ + delete_attachment, get_attachment, update_dev_appeal, check_access, get_dev_appeal +from .schemas import CommentShort, Comment, AppealCreate, CommentCreate, CommentDB, \ + AppealUpdate, AppealDB, CommentUpdate, AttachmentDB +from ..users.models import UserTable +from ..users.logic import employee, any_user + +router = APIRouter() + + +@router.get("/", status_code=status.HTTP_200_OK) +async def appeals_list(last_id: int = 0, limit: int = 9, user: UserTable = Depends(any_user)): + if user.is_superuser: + return await get_all_appeals(last_id, limit) + return await get_appeals_page(user, last_id, limit) + + +@router.get("/{id}", status_code=status.HTTP_200_OK) +async def appeal(id: int, user: UserTable = Depends(any_user)): + await check_access(id, user, status.HTTP_403_FORBIDDEN) + result = await get_appeal(id, user) + if user.is_superuser: + result = await get_dev_appeal(id, user, result) + return result + + +@router.post("/", response_model=AppealDB, status_code=status.HTTP_201_CREATED) +async def create_appeal(item: AppealCreate, user: UserTable = Depends(employee)): + if user.is_superuser is True: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + return await add_appeal(item, user) + + +@router.patch("/{id}", response_model=AppealDB, status_code=status.HTTP_201_CREATED) +async def update_appeal_by_id(id: int, item: AppealUpdate, user: UserTable = Depends(any_user)): + if user.is_superuser: + return await update_dev_appeal(id, item, user) + return await update_appeal(id, item, user) + + +@router.get("/{id}/comments", response_model=List[CommentShort], status_code=status.HTTP_200_OK) +async def comments_list(id: int, user: UserTable = Depends(any_user)): + await check_access(id, user, status.HTTP_403_FORBIDDEN) + return await get_comments(id, user) + + +@router.get("/{id}/comments/{pk}", response_model=Comment, status_code=status.HTTP_200_OK) +async def comment(id: int, pk: int, user: UserTable = Depends(any_user)): + await check_access(id, user, status.HTTP_403_FORBIDDEN) + return await get_comment(id, pk, user) + + +@router.post("/{id}/comments/", response_model=CommentDB, status_code=status.HTTP_201_CREATED) +async def create_comment(id: int, item: CommentCreate, user: UserTable = Depends(any_user)): + await check_access(id, user, status.HTTP_403_FORBIDDEN) + return await add_comment(id, item, user) + + +@router.patch("/{id}/comments/{pk}", response_model=CommentDB, status_code=status.HTTP_201_CREATED) +async def update_comment_by_id(id: int, pk: int, item: CommentUpdate, user: UserTable = Depends(any_user)): + await check_access(id, user, status.HTTP_403_FORBIDDEN) + return await update_comment(id, pk, item, user) + + +@router.delete("/{id}/comments/{pk}", response_class=Response, status_code=status.HTTP_204_NO_CONTENT) +async def delete_comment_by_id(id: int, pk: int, user: UserTable = Depends(any_user)): + await check_access(id, user, status.HTTP_403_FORBIDDEN) + await delete_comment(id, pk, user) + + +@router.get("/{id}/attachments/{pk}", status_code=status.HTTP_200_OK) +async def get_attachment_by_id(id: int, pk: int, user: UserTable = Depends(any_user)): + await check_access(id, user, status.HTTP_403_FORBIDDEN) + return await get_attachment(pk) + + +@router.post("/{id}/attachments/", response_model=AttachmentDB, status_code=status.HTTP_201_CREATED) +async def upload_attachments(id: int, file: UploadFile = File(...), user: UserTable = Depends(any_user)): + await check_access(id, user, status.HTTP_403_FORBIDDEN) + return await upload_attachment(id, file, user) + + +@router.delete("/{id}/comments/{pk}", response_class=Response, status_code=status.HTTP_204_NO_CONTENT) +async def delete_attachment_by_id(id: int, pk: int, user: UserTable = Depends(any_user)): + await check_access(id, user, status.HTTP_403_FORBIDDEN) + await delete_attachment(pk) + diff --git a/src/desk/schemas.py b/src/desk/schemas.py new file mode 100644 index 0000000..527bac2 --- /dev/null +++ b/src/desk/schemas.py @@ -0,0 +1,143 @@ +from typing import Optional, List +from datetime import datetime + +from .models import StatusTasks +from pydantic import BaseModel + +from ..accounts.client_account.schemas import ClientDB, Client +from ..reference_book.schemas import SoftwareDB, ModuleDB +from ..users.schemas import UserDB, DeveloperList + + +class CommentBase(BaseModel): + text: str + + +class CommentCreate(CommentBase): + pass + + +class CommentUpdate(CommentCreate): + pass + + +class Comment(CommentBase): + id: int + appeal_id: int + author: UserDB + date_create: datetime + + +class CommentDB(CommentBase): + id: int + appeal_id: int + author_id: str + date_create: datetime + + +class CommentShort(CommentBase): + id: int + author_id: str + date_create: datetime + + +class AttachmentBase(BaseModel): + filename: str + + +class AttachmentCreate(AttachmentBase): + appeal_id: int + author_id: str + date_create: datetime = datetime.utcnow() + + +class AttachmentDB(AttachmentBase): + id: int + appeal_id: int + author_id: str + date_create: datetime + + +class AppealBase(BaseModel): + topic: str + importance: int + date_edit: Optional[datetime] + + +class AppealCreate(AppealBase): + text: str + importance: Optional[int] + software_id: int + module_id: int + date_create: datetime = datetime.utcnow() + importance: Optional[int] = 1 + + +class AppealUpdate(AppealBase): + topic: Optional[str] + text: Optional[str] + status: Optional[StatusTasks] + software_id: Optional[int] + module_id: Optional[int] + responsible_id: Optional[str] + importance: Optional[int] + date_edit: datetime = datetime.utcnow() + + +class AppealShort(AppealBase): + id: int + text: str + status: StatusTasks + + +class AppealDB(AppealBase): + id: int + text: str + client_id: int + author_id: str + status: StatusTasks + date_create: datetime + date_processing: Optional[datetime] + responsible_id: Optional[str] + software_id: int + module_id: int + + +class AppealList(AppealBase): + id: int + importance: int + author: UserDB + client: ClientDB + date_create: datetime + responsible: Optional[UserDB] + status: StatusTasks + software: SoftwareDB + module: ModuleDB + + +class Appeal(AppealBase): + id: int + text: str + status: StatusTasks + date_create: datetime + date_processing: Optional[datetime] + client: ClientDB + author: UserDB + responsible: Optional[UserDB] + software: SoftwareDB + module: ModuleDB + comments: Optional[List[CommentShort]] + + +class AppealsPage(BaseModel): + appeals: List[AppealList] + client: Client + software_list: List[SoftwareDB] + modules_list: List[ModuleDB] + + +class DevAppeal(Appeal): + software_list: List[SoftwareDB] + modules_list: List[ModuleDB] + developers: List[DeveloperList] + allowed_statuses: List[StatusTasks] diff --git a/src/desk/services.py b/src/desk/services.py new file mode 100644 index 0000000..6fa2a54 --- /dev/null +++ b/src/desk/services.py @@ -0,0 +1,450 @@ +import shutil +from datetime import datetime +from typing import List, Optional, Dict + +from pydantic.types import UUID4 + +from .schemas import AppealCreate, CommentCreate, AppealUpdate, CommentUpdate, AppealList, AppealDB, Appeal, \ + DevAppeal, AppealsPage, AttachmentDB, AttachmentCreate +from ..accounts.client_account.services import get_client_db, get_client +from ..accounts.developer_account.statistics.schemas import StatusesDistribution, StatusStatistics +from ..db.db import database +from .models import appeals, comments, attachments +from ..errors import Errors +from ..reference_book.services import get_software, get_module, get_modules, get_software_db_list, get_modules_db +from ..users.logic import get_or_404, get_user, get_developers_db +from ..users.models import UserTable, users +from .models import StatusTasks +from ..service import check_dict, send_mail, Email + +from fastapi import HTTPException, status, UploadFile +from fastapi.responses import FileResponse + + +async def get_all_appeals(last_id: int = 0, limit: int = 9) -> List[AppealList]: + query = appeals.select().where(appeals.c.id > last_id).limit(limit) + result = await database.fetch_all(query=query) + appeals_list = [] + for appeal in result: + appeal = dict(appeal) + appeals_list.append(await get_appeal_list(appeal)) + return appeals_list + + +async def get_appeals(user: UserTable, last_id: int = 0, limit: int = 9) -> List[AppealList]: + query = appeals.select().where((appeals.c.client_id == user.client_id) & (appeals.c.id > last_id)).limit(limit) + result = await database.fetch_all(query=query) + appeals_list = [] + for appeal in result: + appeal = dict(appeal) + appeals_list.append(await get_appeal_list(appeal)) + return appeals_list + + +async def get_appeals_by_developer(developer_id: str) -> List[AppealDB]: + query = appeals.select().where(appeals.c.responsible_id == developer_id) + result = await database.fetch_all(query=query) + return [AppealDB(**dict(appeal)) for appeal in result] + + +async def get_appeals_by_client(client_id: int) -> List[AppealDB]: + query = appeals.select().where(appeals.c.client_id == client_id) + result = await database.fetch_all(query=query) + return [AppealDB(**dict(appeal)) for appeal in result] + + +async def get_appeals_by_software(software_id: int) -> List[AppealDB]: + query = appeals.select().where(appeals.c.software_id == software_id) + result = await database.fetch_all(query=query) + return [AppealDB(**dict(appeal)) for appeal in result] + + +async def get_appeals_by_module(module_id: int) -> List[AppealDB]: + query = appeals.select().where(appeals.c.module_id == module_id) + result = await database.fetch_all(query=query) + return [AppealDB(**dict(appeal)) for appeal in result] + + +async def get_appeals_db() -> List[AppealDB]: + result = await database.fetch_all(appeals.select()) + return [AppealDB(**dict(appeal)) for appeal in result] + + +async def get_status_distribution(appeals_list: List[AppealDB]) -> StatusesDistribution: + new = 0 + registered = 0 + in_work = 0 + closed = 0 + canceled = 0 + reopen = 0 + for appeal in appeals_list: + if appeal.status == StatusTasks.new: + new += 1 + elif appeal.status == StatusTasks.registered: + registered += 1 + elif appeal.status == StatusTasks.in_work: + in_work += 1 + elif appeal.status == StatusTasks.closed: + closed += 1 + elif appeal.status == StatusTasks.canceled: + canceled += 1 + elif appeal.status == StatusTasks.reopen: + reopen += 1 + return StatusesDistribution(new=new, + registered=registered, + in_work=in_work, + closed=closed, + canceled=canceled, + reopen=reopen) + + +async def get_statuses_list(appeals_list: List[AppealDB]) -> List[StatusStatistics]: + statuses_list = [] + statuses = { + "new": 0, + "registered": 0, + "in_work": 0, + "closed": 0, + "canceled": 0, + "reopen": 0, + } + for appeal in appeals_list: + if appeal.status == StatusTasks.new: + statuses["new"] += 1 + elif appeal.status == StatusTasks.registered: + statuses["registered"] += 1 + elif appeal.status == StatusTasks.in_work: + statuses["in_work"] += 1 + elif appeal.status == StatusTasks.closed: + statuses["closed"] += 1 + elif appeal.status == StatusTasks.canceled: + statuses["canceled"] += 1 + elif appeal.status == StatusTasks.reopen: + statuses["reopen"] += 1 + for current_status in statuses: + statuses_list.append(StatusStatistics(name=current_status, count_appeals=statuses[current_status])) + return statuses_list + + +async def get_appeal_list(appeal: Dict) -> AppealList: + author = await get_or_404(appeal["author_id"]) + client = await get_client_db(appeal["client_id"]) + responsible = await get_user(appeal["responsible_id"]) + software = await get_software(appeal["software_id"]) + module = await get_module(appeal["module_id"]) + return AppealList(**dict({ + **appeal, + "author": author, + "client": client, + "responsible": responsible, + "software": software, + "module": module})) + + +async def get_appeals_page(user: UserTable, last_id: int = 0, limit: int = 9) -> AppealsPage: + appeals_list = await get_appeals(user, last_id, limit) + client = await get_client(user.client_id) + software_list = await get_software_db_list() # TODO выдавать софт клиента + modules_list = await get_modules() # TODO выдавать модули клиента + return AppealsPage(**dict({"appeals": appeals_list, + "client": client, + "software_list": software_list, + "modules_list": modules_list})) + + +async def get_appeal(appeal_id: int, user: UserTable) -> Appeal: + appeal = await check_access(appeal_id, user, status.HTTP_404_NOT_FOUND) + client = await get_client_db(appeal.client_id) + author = await get_user(UUID4(appeal.author_id)) + responsible = None + if appeal.responsible_id: + responsible = await get_user(UUID4(appeal.responsible_id)) + software = await get_software(appeal.software_id) + module = await get_module(appeal.module_id) + comment = await get_comments(appeal_id, user) + result = Appeal(**dict({**dict(appeal), + "client": client, + "author": author, + "responsible": responsible, + "software": software, + "module": module, + "comments": comment})) + return result + + +async def get_dev_appeal(appeal_id: int, user: UserTable, appeal: Appeal) -> DevAppeal: + developers = await get_developers_db() + allowed_statuses = await get_next_status(appeal_id, user) + modules_list = await get_modules_db() + software_list = await get_software_db_list() + result = DevAppeal(**dict({**dict(appeal), + "software_list": software_list, + "modules_list": modules_list, + "developers": developers, + "allowed_statuses": allowed_statuses})) + return result + + +async def get_appeal_db(appeal_id: int) -> AppealDB: + query = appeals.select().where(appeals.c.id == appeal_id) + result = await database.fetch_one(query=query) + if result: + return AppealDB(**dict(result)) + else: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + +async def add_appeal(appeal: AppealCreate, user: UserTable) -> AppealDB: + if appeal.importance: + if appeal.importance > 5: + appeal.importance = 5 + elif appeal.importance < 1: + appeal.importance = 1 + item = {**appeal.dict(), "client_id": int(user.client_id), "author_id": str(user.id), "status": StatusTasks.new} + query = appeals.insert().values(item) + appeal_id = await database.execute(query) + await notify_create_appeal(appeal_id, user) + return await get_appeal_db(appeal_id) + + +async def update_appeal(appeal_id: int, appeal: AppealUpdate, user: UserTable) -> AppealDB: + old_appeal = await check_access(appeal_id, user, status.HTTP_403_FORBIDDEN) + appeal = appeal.dict(exclude_unset=True) + + if old_appeal.status == StatusTasks.closed or old_appeal.status == StatusTasks.canceled: + if "status" not in appeal.keys() or appeal["status"] != StatusTasks.reopen: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=Errors.APPEAL_IS_CLOSED) + + if "importance" in appeal: + if appeal["importance"] < 1: + appeal["importance"] = 1 + if appeal["importance"] > 5: + appeal["importance"] = 5 + old_appeal_dict = dict(old_appeal) + for field in appeal: + if field not in ["status", "text"]: + continue + if field == "status" and (appeal[field] != StatusTasks.reopen + or (old_appeal_dict["status"] != StatusTasks.canceled + and old_appeal_dict["status"] != StatusTasks.closed)): + continue + elif appeal[field] is not None: + old_appeal_dict[field] = appeal[field] + if old_appeal_dict != dict(old_appeal): + old_appeal_dict["date_edit"] = datetime.utcnow() + query = appeals.update().where(appeals.c.id == appeal_id).values(old_appeal_dict) + await database.execute(query) + await notify_update_appeal(appeal_id, user) + return await get_appeal_db(appeal_id) + + +async def update_dev_appeal(appeal_id: int, appeal: AppealUpdate, user: UserTable) -> AppealDB: + old_appeal = await get_appeal_db(appeal_id) + + if old_appeal.status == StatusTasks.closed or old_appeal.status == StatusTasks.canceled: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=Errors.APPEAL_IS_CLOSED) + appeal = appeal.dict(exclude_unset=True) + + if "status" in appeal and (appeal["status"] == StatusTasks.closed or appeal["status"] == StatusTasks.canceled): + appeal["date_processing"] = datetime.utcnow() + + if "responsible_id" in appeal and old_appeal.status == StatusTasks.new: + appeal["status"] = StatusTasks.registered + + if "importance" in appeal: + if appeal["importance"] < 1: + appeal["importance"] = 1 + if appeal["importance"] > 5: + appeal["importance"] = 5 + + old_appeal_dict = dict(old_appeal) + for field in appeal: + if appeal[field] is not None: + old_appeal_dict[field] = appeal[field] + + if old_appeal_dict != dict(old_appeal): + old_appeal_dict["date_edit"] = datetime.utcnow() + + query = appeals.update().where(appeals.c.id == appeal_id).values(old_appeal_dict) + await database.execute(query) + await notify_update_appeal(appeal_id, user) + return await get_appeal_db(appeal_id) + + +async def delete_appeal(appeal_id: int): + query = appeals.delete().where(appeals.c.id == appeal_id) + result = await database.execute(query) + return result + + +async def get_comments(comment_id: int, user: UserTable): + await check_access(comment_id, user, status.HTTP_404_NOT_FOUND) + query = comments.select().where(comments.c.appeal_id == comment_id) + result = await database.fetch_all(query) + return [dict(comment) for comment in result] + + +async def get_comment(comment_id: int, pk: int, user: UserTable): + await check_access(comment_id, user, status.HTTP_404_NOT_FOUND) + query = comments.select().where((comments.c.id == pk) & (comments.c.appeal_id == comment_id)) + comment = await database.fetch_one(query) + if comment: + comment = dict(comment) + author = await database.fetch_one(query=users.select().where(users.c.id == comment["author_id"])) + return {**comment, "author": author} + return None + + +async def get_comment_db(comment_id: int): + query = comments.select().where(comments.c.id == comment_id) + result = await database.fetch_one(query=query) + return await check_dict(result) + + +async def add_comment(appeal_id: int, comment: CommentCreate, user: UserTable): + await check_access(appeal_id, user, status.HTTP_403_FORBIDDEN) + item = {**comment.dict(), "appeal_id": int(appeal_id), "author_id": str(user.id)} + query = comments.insert().values(item) + comment_id = await database.execute(query) + await notify_comment(appeal_id, user) + return await get_comment_db(comment_id) + + +async def update_comment(appeal_id: int, pk: int, comment: CommentUpdate, user: UserTable): + await check_access(appeal_id, user, status.HTTP_403_FORBIDDEN) + query = comments.update().where((comments.c.id == pk) & (comments.c.author_id == user.id)).values(**comment.dict()) + result_id = await database.execute(query) + return await get_comment_db(result_id) + + +async def delete_comment(appeal_id: int, pk: int, user: UserTable): + await check_access(appeal_id, user, status.HTTP_403_FORBIDDEN) + query = comments.delete().where((comments.c.id == pk) & (comments.c.author_id == str(user.id))) + await database.execute(query) + + +async def check_access(appeal_id: int, user: UserTable, status_code: status): + appeal = await get_appeal_db(appeal_id) + if appeal.client_id != user.client_id and not user.is_superuser: + raise HTTPException(status_code=status_code) + return appeal + + +async def get_next_status(appeal_id: int, user: UserTable) -> List[StatusTasks]: + appeal = await get_appeal_db(appeal_id) + current_status = appeal.status + if user.is_superuser: + if current_status is StatusTasks.new: + return [StatusTasks.registered] + elif current_status is StatusTasks.registered: + return [StatusTasks.in_work] + elif current_status is StatusTasks.in_work: + return [StatusTasks.closed, StatusTasks.canceled] + elif current_status is StatusTasks.reopen: + return [StatusTasks.in_work] + else: + return [] + elif current_status is StatusTasks.closed or current_status is StatusTasks.canceled: + return [StatusTasks.reopen] + else: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=Errors.USER_CAN_NOT_CHANGE_STATUS) + + +async def get_attachment_db(attachment_id: int) -> Optional[AttachmentDB]: + query = attachments.select().where(attachments.c.id == attachment_id) + result = await database.fetch_one(query=query) + if result: + return AttachmentDB(**dict(result)) + return None + + +async def get_attachment(attachment_id: int): + attachment = await get_attachment_db(attachment_id) + if attachment: + return FileResponse(attachment.filename) + else: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + +async def add_attachment(appeal_id: int, filename: str, user_id: str) -> Optional[AttachmentDB]: + attachment = AttachmentCreate(filename=filename, + appeal_id=appeal_id, + author_id=user_id, + date_create=datetime.utcnow()) + query = attachments.insert().values(**dict(attachment)) + attachment_id = await database.execute(query) + return await get_attachment_db(attachment_id) + + +async def upload_attachment(appeal_id: int, file: UploadFile, user: UserTable) -> Optional[AttachmentDB]: + filename = f'{datetime.utcnow()}-{file.filename}' + with open(f'/attachments/{filename}', 'wb') as buffer: + shutil.copyfileobj(file.file, buffer) + await notify_attachment(appeal_id, user) + return await add_attachment(appeal_id, filename, str(user.id)) + + +async def delete_attachment(attachment_id: int) -> None: + query = attachments.delete().where(attachments.c.id == attachment_id) + await database.execute(query) + + +async def notify_create_appeal(appeal_id: int, user: UserTable) -> None: + appeal = await get_appeal_db(appeal_id) + client = await get_client_db(appeal.client_id) + message = f"Представителем закачика «{client.name}» - {user.name} {user.surname} " \ + f"было создано обращение №{appeal_id} - «{appeal.topic}»" + developers = await get_developers_db() + for developer in developers: + await notify_user(developer.email, message) + return None + + +async def notify_update_appeal(appeal_id: int, user: UserTable) -> None: + appeal = await get_appeal_db(appeal_id) + message = f"Обращение №{appeal_id} - «{appeal.topic}» было измененно" + if user.is_superuser: + author = await get_or_404(UUID4(appeal.author_id)) + to_addr = author.email + message += " разработчиком" + await notify_user(to_addr, message) + elif appeal.responsible_id: + developer = await get_or_404(UUID4(appeal.responsible_id)) + to_addr = developer.email + message += " представителем заказчика" + await notify_user(to_addr, message) + return None + + +async def notify_comment(appeal_id: int, user: UserTable) -> None: + appeal = await get_appeal_db(appeal_id) + message = f"В обращении №{appeal_id} - «{appeal.topic}» был оставлен комментарий" + if user.is_superuser: + author = await get_or_404(UUID4(appeal.author_id)) + to_addr = author.email + await notify_user(to_addr, message) + elif appeal.responsible_id: + developer = await get_or_404(UUID4(appeal.responsible_id)) + to_addr = developer.email + await notify_user(to_addr, message) + return None + + +async def notify_attachment(appeal_id: int, user: UserTable) -> None: + appeal = await get_appeal_db(appeal_id) + message = f"В обращении №{appeal_id} - «{appeal.topic}» были изменены вложения" + if user.is_superuser: + author = await get_or_404(UUID4(appeal.author_id)) + to_addr = author.email + await notify_user(to_addr, message) + elif appeal.responsible_id: + developer = await get_or_404(UUID4(appeal.responsible_id)) + to_addr = developer.email + await notify_user(to_addr, message) + return None + + +async def notify_user(to_addr: str, message: str) -> None: + title = "Изменение обращения" + email = Email(recipient=to_addr, title=title, message=message) + await send_mail(email) diff --git a/src/errors.py b/src/errors.py new file mode 100644 index 0000000..5511593 --- /dev/null +++ b/src/errors.py @@ -0,0 +1,25 @@ +class Errors: + CLIENT_HAS_NOT_FREE_VACANCIES = "CLIENT_HAS_NOT_FREE_VACANCIES" + USER_NOT_FOUND = "USER_NOT_FOUND" + IMPOSSIBLE_DELETE_OWNER = "IMPOSSIBLE_DELETE_OWNER" + USER_CAN_NOT_CHANGE_STATUS = "USER_CAN_NOT_CHANGE_STATUS_ON_THE_NEXT" + APPEAL_IS_CLOSED = "APPEAL_IS_CLOSED" + COMPANY_IS_EXIST = "COMPANY_IS_EXIST" + + USER_HAS_ANOTHER_LICENCE = "USER_HAS_ANOTHER_LICENCE" + LICENCE_IS_FULL = "LICENCE_IS_FULL" + CLIENT_HAS_THIS_LICENCE = "CLIENT_HAS_THIS_LICENCE" + + MODULE_IS_EXIST = "MODULE_IS_EXIST" + SOFTWARE_IS_EXIST = "SOFTWARE_IS_EXIST" + LICENCE_IS_EXIST = "LICENCE_IS_EXIST" + + MODULE_IS_NOT_EXIST = "MODULE_IS_NOT_EXIST" + SOFTWARE_IS_NOT_EXIST = "SOFTWARE_IS_NOT_EXIST" + LICENCE_IS_NOT_EXIST = "LICENCE_IS_NOT_EXIST" + + MODULE_FOR_THIS_SOFTWARE_IS_EXIST = "MODULE_FOR_THIS_SOFTWARE_IS_EXIST" + MODULE_FOR_THIS_SOFTWARE_IS_NOT_EXIST = "MODULE_FOR_THIS_SOFTWARE_IS_NOT_EXIST" + + CLIENT_NOT_FOUND = "CLIENT_NOT_FOUND" + FORBIDDEN_CHANGE_EMAIL = "FORBIDDEN_CHANGE_EMAIL" diff --git a/src/reference_book/__init__.py b/src/reference_book/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/reference_book/api/__init__.py b/src/reference_book/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/reference_book/api/licence.py b/src/reference_book/api/licence.py new file mode 100644 index 0000000..bb91810 --- /dev/null +++ b/src/reference_book/api/licence.py @@ -0,0 +1,34 @@ +from fastapi import APIRouter, Depends, status, Response +from ..schemas import Licence, LicenceCreate, LicenceDB, LicenceUpdate, LicencePage +from ..services import get_licence, add_licence, update_licence, delete_licence, get_licence_page +from ...users.models import UserTable +from ...users.logic import developer_user + +router = APIRouter() + + +@router.get("/", response_model=LicencePage, status_code=status.HTTP_200_OK) +async def licence_list(last_id: int = 0, limit: int = 9, user: UserTable = Depends(developer_user)): + if user.is_superuser: + return await get_licence_page(last_id, limit) + return None + + +@router.get('/{id}', response_model=Licence, status_code=status.HTTP_200_OK) +async def licence(id: int, user: UserTable = Depends(developer_user)): + return await get_licence(id) + + +@router.post("/", response_model=LicenceDB, status_code=status.HTTP_201_CREATED) +async def create_licence(licence: LicenceCreate, user: UserTable = Depends(developer_user)): + return await add_licence(licence) + + +@router.patch("/{id}", response_model=LicenceDB, status_code=status.HTTP_201_CREATED) +async def update_licence_by_id(id: int, item: LicenceUpdate, user: UserTable = Depends(developer_user)): + return await update_licence(id, item) + + +@router.delete("/{id}", response_class=Response, status_code=status.HTTP_204_NO_CONTENT) +async def delete_licence_by_id(id: int, user: UserTable = Depends(developer_user)): + await delete_licence(id) diff --git a/src/reference_book/api/module.py b/src/reference_book/api/module.py new file mode 100644 index 0000000..209a9c7 --- /dev/null +++ b/src/reference_book/api/module.py @@ -0,0 +1,34 @@ +from typing import List + +from fastapi import APIRouter, status, Depends, Response +from ..schemas import ModuleCreate, ModuleDB, ModuleUpdate +from ..services import get_module, add_module, delete_module, update_module, get_modules +from ...users.logic import developer_user +from ...users.models import UserTable + +router = APIRouter() + + +@router.get('/', response_model=List[ModuleDB], status_code=status.HTTP_200_OK) +async def modules_list(last_id: int = 0, limit: int = 9, user: UserTable = Depends(developer_user)): + return await get_modules(last_id, limit) + + +@router.get('/{id}', response_model=ModuleDB, status_code=status.HTTP_200_OK) +async def module(id: int, user: UserTable = Depends(developer_user)): + return await get_module(id) + + +@router.post("/", response_model=ModuleDB, status_code=status.HTTP_201_CREATED) +async def create_module(item: ModuleCreate, user: UserTable = Depends(developer_user)): + return await add_module(item) + + +@router.patch("/{id}", response_model=ModuleDB, status_code=status.HTTP_201_CREATED) +async def update_module_by_id(id: int, item: ModuleUpdate, user: UserTable = Depends(developer_user)): + return await update_module(id, item) + + +@router.delete("/{id}", response_class=Response, status_code=status.HTTP_204_NO_CONTENT) +async def delete_module_by_id(id: int, user: UserTable = Depends(developer_user)): + await delete_module(id) diff --git a/src/reference_book/api/routes.py b/src/reference_book/api/routes.py new file mode 100644 index 0000000..cd95fdc --- /dev/null +++ b/src/reference_book/api/routes.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter, Depends, status +from .software import router as software_router, software_list +from .licence import router as licence_router, licence_list +from .module import router as module_router, modules_list +from ...users.logic import developer_user +from ...users.models import UserTable + +router = APIRouter() + + +@router.get('/', status_code=status.HTTP_200_OK) +async def get_reference_book(user: UserTable = Depends(developer_user)): + licences = await licence_list(user=user) + modules = await modules_list(user=user) + softwares = await software_list(user=user) + return {"licences": licences, "modules": modules, "softwares": softwares} + +router.include_router(licence_router, prefix='/licences') +router.include_router(software_router, prefix='/software') +router.include_router(module_router, prefix='/modules') diff --git a/src/reference_book/api/software.py b/src/reference_book/api/software.py new file mode 100644 index 0000000..0944432 --- /dev/null +++ b/src/reference_book/api/software.py @@ -0,0 +1,34 @@ +from fastapi import APIRouter, status, Depends, Response + +from ..services import get_software_with_modules, delete_software, update_software, \ + get_software_page, add_software_with_modules +from ..schemas import Software, SoftwareDB, SoftwareUpdate, SoftwarePage, SoftwareWithModulesCreate +from ...users.logic import developer_user +from ...users.models import UserTable + +router = APIRouter() + + +@router.get('/', response_model=SoftwarePage, status_code=status.HTTP_200_OK) +async def software_list(last_id: int = 0, limit: int = 9, user: UserTable = Depends(developer_user)): + return await get_software_page(last_id, limit) + + +@router.get('/{id}', response_model=Software, status_code=status.HTTP_200_OK) +async def software(id: int, user: UserTable = Depends(developer_user)): + return await get_software_with_modules(id) + + +@router.post("/", response_model=SoftwareDB, status_code=status.HTTP_201_CREATED) +async def create_software(new_software: SoftwareWithModulesCreate, user: UserTable = Depends(developer_user)): + return await add_software_with_modules(new_software) + + +@router.patch("/{id}", response_model=SoftwareDB, status_code=status.HTTP_201_CREATED) +async def update_software_by_id(id: int, item: SoftwareUpdate, user: UserTable = Depends(developer_user)): + return await update_software(id, item) + + +@router.delete("/{id}", response_class=Response, status_code=status.HTTP_204_NO_CONTENT) +async def delete_software_by_id(id: int, user: UserTable = Depends(developer_user)): + await delete_software(id) diff --git a/src/reference_book/models.py b/src/reference_book/models.py new file mode 100644 index 0000000..cef1944 --- /dev/null +++ b/src/reference_book/models.py @@ -0,0 +1,61 @@ +from sqlalchemy import Column, DateTime, String, Integer, sql, ForeignKey, UniqueConstraint + +from ..db.db import Base + + +class Licence(Base): + __tablename__ = 'licence' + + id = Column(Integer, primary_key=True, index=True, unique=True) + number = Column(Integer, unique=True, nullable=False) + count_members = Column(Integer, default=0, nullable=False) + date_end = Column(DateTime(timezone=True), server_default=sql.func.now()) + software_id = Column(Integer, ForeignKey('software.id'), nullable=False) + + +class Module(Base): + __tablename__ = 'module' + + id = Column(Integer, primary_key=True, index=True, unique=True) + name = Column(String, nullable=False) + + +class Software(Base): + __tablename__ = 'software' + + id = Column(Integer, primary_key=True, index=True, unique=True) + name = Column(String, unique=True, nullable=False) + + +class EmployeeLicence(Base): + __tablename__ = 'EmployeeLicence' + + id = Column(Integer, primary_key=True, index=True, unique=True) + employee_id = Column(String, ForeignKey('user.id'), nullable=False, unique=True) + licence_id = Column(Integer, ForeignKey('licence.id'), nullable=False) + + +class ClientLicence(Base): + __tablename__ = 'ClientLicence' + + id = Column(Integer, primary_key=True, index=True, unique=True) + client_id = Column(String, ForeignKey('client.id'), nullable=False) + licence_id = Column(Integer, ForeignKey('licence.id'), nullable=False) + UniqueConstraint(client_id, licence_id) + + +class SoftwareModules(Base): + __tablename__ = 'SoftwareModules' + + id = Column(Integer, primary_key=True, index=True, unique=True) + software_id = Column(Integer, ForeignKey('software.id'), nullable=False) + module_id = Column(Integer, ForeignKey('module.id'), nullable=False) + UniqueConstraint(software_id, module_id) + + +modules = Module.__table__ +licences = Licence.__table__ +softwares = Software.__table__ +employee_licences = EmployeeLicence.__table__ +software_modules = SoftwareModules.__table__ +client_licences = ClientLicence.__table__ \ No newline at end of file diff --git a/src/reference_book/schemas.py b/src/reference_book/schemas.py new file mode 100644 index 0000000..8f37e12 --- /dev/null +++ b/src/reference_book/schemas.py @@ -0,0 +1,146 @@ +from datetime import datetime +from typing import List, Optional + +from pydantic import BaseModel + + +class ModuleBase(BaseModel): + name: str + + +class ModuleCreate(ModuleBase): + pass + + +class ModuleUpdate(ModuleBase): + pass + + +class ModuleDB(ModuleBase): + id: int + + +class SoftwareBase(BaseModel): + name: str = '' + + +class SoftwareCreate(SoftwareBase): + pass + + +class SoftwareWithModulesCreate(SoftwareCreate): + modules: List[int] + + +class SoftwareUpdate(SoftwareBase): + pass + + +class SoftwareDB(SoftwareBase): + id: int + + +class Software(SoftwareBase): + id: int + modules: List[ModuleDB] + + +class SoftwarePage(BaseModel): + software_list: List[Software] + modules_list: List[ModuleDB] + + +class LicenceBase(BaseModel): + count_members: int + date_end: datetime + + +class LicenceCreate(LicenceBase): + number: int + software_id: int + + +class LicenceUpdate(LicenceBase): + count_members: Optional[int] + date_end: Optional[datetime] + + +class LicenceDB(LicenceBase): + id: int + number: int + software_id: int + + +class Licence(LicenceBase): + id: int + number: int + closed_vacancies: int = -1 + software: SoftwareDB + + +class LicencePage(BaseModel): + licences_list: List[Licence] + software_list: List[SoftwareDB] + + +class EmployeeLicenceBase(BaseModel): + pass + + +class EmployeeLicenceCreate(EmployeeLicenceBase): + employee_id: str + licence_id: int + + +class EmployeeLicenceUpdate(EmployeeLicenceBase): + licence_id: int + + +class EmployeeLicenceDB(EmployeeLicenceBase): + id: int + employee_id: str + licence_id: int + + +class EmployeeLicence(EmployeeLicenceBase): + id: int + employee_id: str + licence: LicenceDB + + +class ClientLicenceBase(BaseModel): + pass + + +class ClientLicenceCreate(EmployeeLicenceBase): + client_id: int + licence_id: int + + +class ClientLicenceUpdate(EmployeeLicenceBase): + licence_id: int + + +class ClientLicenceDB(EmployeeLicenceBase): + id: int + client_id: int + licence_id: int + + +class ClientLicence(EmployeeLicenceBase): + id: int + client_id: int + licence: LicenceDB + + +class SoftwareModulesBase(BaseModel): + software_id: int + module_id: int + + +class SoftwareModulesCreate(SoftwareModulesBase): + pass + + +class SoftwareModulesDB(SoftwareModulesBase): + id: int diff --git a/src/reference_book/services.py b/src/reference_book/services.py new file mode 100644 index 0000000..196b381 --- /dev/null +++ b/src/reference_book/services.py @@ -0,0 +1,410 @@ +from fastapi import HTTPException, status + +from .schemas import ModuleCreate, LicenceCreate, SoftwareCreate, EmployeeLicenceCreate, EmployeeLicenceUpdate, \ + SoftwareUpdate, SoftwareDB, Software, ModuleDB, ModuleUpdate, LicenceDB, Licence, LicenceUpdate, \ + EmployeeLicenceDB, SoftwareModulesCreate, SoftwareModulesDB, SoftwarePage, \ + SoftwareWithModulesCreate, LicencePage, ClientLicenceDB, ClientLicenceCreate +from ..accounts.client_account.models import clients +from ..accounts.client_account.schemas import ClientDB +from ..db.db import database +from .models import softwares, modules, licences, employee_licences, software_modules, client_licences +from ..errors import Errors +from typing import List, Optional + + +async def get_software_list(last_id: int = 0, limit: int = 9) -> List[Software]: + query = softwares.select().where(softwares.c.id > last_id).limit(limit) + result = await database.fetch_all(query=query) + list_of_software = [] + for software in result: + software = dict(software) + module = await get_software_modules(software["id"]) + list_of_software.append(Software(**dict({**software, "modules": module}))) + return list_of_software + + +async def get_software_db_list() -> List[SoftwareDB]: + result = await database.fetch_all(query=softwares.select()) + return [SoftwareDB(**dict(software)) for software in result] + + +async def get_software_page(last_id: int = 0, limit: int = 9) -> SoftwarePage: + software_list = await get_software_list(last_id, limit) + modules_list = await get_modules_db() + return SoftwarePage(**dict({"software_list": software_list, "modules_list": modules_list})) + + +async def get_software(software_id: int) -> Optional[SoftwareDB]: + result = await database.fetch_one(query=softwares.select().where(softwares.c.id == software_id)) + if result is not None: + return SoftwareDB(**dict(result)) + return None + + +async def get_software_by_name(software_name: str) -> Optional[SoftwareDB]: + result = await database.fetch_one(query=softwares.select().where(softwares.c.name == software_name)) + if result: + return SoftwareDB(**dict(result)) + return None + + +async def get_software_with_modules(software_id: int) -> Optional[Software]: + result = await database.fetch_one(query=softwares.select().where(softwares.c.id == software_id)) + if result is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=Errors.SOFTWARE_IS_NOT_EXIST + ) + module = await get_software_modules(software_id) + return Software(**dict({**dict(result), "modules": module})) + + +async def add_software(software: SoftwareCreate) -> SoftwareDB: + if await get_software_by_name(software.name): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=Errors.SOFTWARE_IS_EXIST, + ) + query = softwares.insert().values(**software.dict()) + software_id = await database.execute(query) + return SoftwareDB(**dict({"id": software_id, **software.dict()})) + + +async def add_software_with_modules(software_with_modules: SoftwareWithModulesCreate) -> SoftwareDB: + software = await add_software(SoftwareCreate(name=software_with_modules.name)) + modules_list = software_with_modules.modules + for module_id in modules_list: + try: + module = await add_software_module(software.id, module_id) + except Exception as e: + print(e) + return software + + +async def update_software(software_id: int, software: SoftwareUpdate) -> SoftwareDB: + if get_software(software_id) is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=Errors.SOFTWARE_IS_NOT_EXIST, + ) + query = softwares.update().where(softwares.c.id == software_id).values(**software.dict()) + await database.execute(query) + return SoftwareDB(**dict({"id": software_id, **software.dict()})) + + +async def delete_software(software_id: int) -> None: + if get_software(software_id) is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=Errors.SOFTWARE_IS_NOT_EXIST, + ) + query = softwares.delete().where(softwares.c.id == software_id) + await database.execute(query) + + +async def get_software_module(software_id: int, module_id: int) -> Optional[SoftwareModulesDB]: + query = software_modules.select(). \ + where((software_modules.c.software_id == software_id) & (software_modules.c.module_id == module_id)) + result = await database.fetch_one(query=query) + if result: + return SoftwareModulesDB(**dict(result)) + return None + + +async def get_software_modules(software_id: int) -> List[ModuleDB]: + query = software_modules.select().where(software_modules.c.software_id == software_id) + result = await database.fetch_all(query=query) + return [await get_module(software_module.module_id) for software_module in result] + + +async def add_software_module(software_id: int, module_id: int) -> SoftwareModulesDB: + if await get_software_module(software_id, module_id): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=Errors.MODULE_FOR_THIS_SOFTWARE_IS_EXIST, + ) + if await get_module(module_id) is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=Errors.MODULE_IS_NOT_EXIST, + ) + software_module = SoftwareModulesCreate(**dict({"software_id": software_id, "module_id": module_id})) + query = software_modules.insert().values(software_module.dict()) + software_module_id = await database.execute(query) + return SoftwareModulesDB(**dict({"id": software_module_id, **software_module.dict()})) + + +async def delete_software_module(software_id: int, module_id: int) -> None: + if await get_software_module(software_id, module_id) is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=Errors.MODULE_FOR_THIS_SOFTWARE_IS_NOT_EXIST, + ) + query = software_modules.delete(). \ + where((software_modules.c.software_id == software_id) & (software_modules.c.module_id == module_id)) + await database.execute(query) + + +async def get_modules_db() -> List[ModuleDB]: + result = await database.fetch_all(query=modules.select()) + return [ModuleDB(**dict(module)) for module in result] + + +async def get_modules(last_id: int = 0, limit: int = 9) -> List[ModuleDB]: + query = modules.select().where(modules.c.id > last_id).limit(limit) + result = await database.fetch_all(query=query) + modules_list = [] + for module in result: + modules_list.append(ModuleDB(**dict(module))) + return modules_list + + +async def get_module(module_id: int) -> Optional[ModuleDB]: + result = await database.fetch_one(query=modules.select().where(modules.c.id == module_id)) + if result is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=Errors.MODULE_IS_NOT_EXIST, + ) + return ModuleDB(**dict(result)) + + +async def get_module_by_name(module_name: str) -> Optional[ModuleDB]: + result = await database.fetch_one(query=modules.select().where(modules.c.name == module_name)) + if result is not None: + return ModuleDB(**dict(result)) + return None + + +async def add_module(module: ModuleCreate) -> ModuleDB: + if await get_module_by_name(module.name) is not None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=Errors.MODULE_IS_EXIST, + ) + query = modules.insert().values(**module.dict()) + module_id = await database.execute(query) + return ModuleDB(**dict({"id": module_id, **module.dict()})) + + +async def update_module(module_id: int, module: ModuleUpdate) -> ModuleDB: + if await get_module(module_id) is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=Errors.MODULE_IS_NOT_EXIST, + ) + query = modules.update().where(modules.c.id == module_id).values(**module.dict()) + await database.execute(query) + return ModuleDB(**dict({"id": module_id, **module.dict()})) + + +async def delete_module(module_id: int) -> None: + if await get_module(module_id) is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=Errors.MODULE_IS_NOT_EXIST, + ) + query = modules.delete().where(modules.c.id == module_id) + await database.execute(query) + + +async def get_licences_db() -> List[LicenceDB]: + result = await database.fetch_all(query=licences.select()) + licences_list = [] + for licence in result: + licences_list.append(LicenceDB(**dict(licence))) + return licences_list + + +async def get_licences(last_id: int = 0, limit: int = 9) -> List[Licence]: + query = licences.select().where(licences.c.id > last_id).limit(limit) + result = await database.fetch_all(query=query) + licences_list = [] + for licence in result: + licence = dict(licence) + software = await get_software(licence["software_id"]) + closed_vacancies = await get_count_employee_for_licence_id(licence["id"]) + licences_list.append(Licence(**dict({**licence, "software": software, "closed_vacancies": closed_vacancies}))) + return licences_list + + +async def get_licence_page(last_id: int = 0, limit: int = 9) -> LicencePage: + licences_list = await get_licences(last_id, limit) + software_list = await get_software_db_list() + return LicencePage(**dict({"licences_list": licences_list, "software_list": software_list})) + + +async def get_licence(licence_id: int) -> Optional[Licence]: + result = await database.fetch_one(query=licences.select().where(licences.c.id == licence_id)) + if result is not None: + licence = dict(result) + software = await get_software(licence["software_id"]) + closed_vacancies = await get_count_employee_for_licence_id(licence["id"]) + return Licence(**dict({**licence, "software": software, "closed_vacancies": closed_vacancies})) + return None + + +async def get_licence_db(licence_id: int) -> Optional[LicenceDB]: + result = await database.fetch_one(licences.select().where(licences.c.id == licence_id)) + if result: + return LicenceDB(**dict(result)) + return None + + +async def get_licence_by_number(licence_number: int) -> Optional[LicenceDB]: + result = await database.fetch_one(licences.select().where(licences.c.number == int(licence_number))) + if result: + return LicenceDB(**dict(result)) + return None + + +async def add_licence(licence: LicenceCreate) -> LicenceDB: + if await get_licence_by_number(licence.number): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=Errors.LICENCE_IS_EXIST, + ) + if await get_software(licence.software_id) is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=Errors.SOFTWARE_IS_NOT_EXIST, + ) + query = licences.insert().values(licence.dict()) + licence_id = await database.execute(query) + return LicenceDB(**dict({"id": licence_id, **licence.dict()})) + + +async def update_licence(licence_id: int, licence: LicenceUpdate) -> LicenceDB: + old_licence = await get_licence_db(licence_id) + if old_licence is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=Errors.LICENCE_IS_NOT_EXIST, + ) + licence = dict(licence) + old_licence = dict(old_licence) + for field in licence: + if licence[field]: + old_licence[field] = licence[field] + query = licences.update().where(licences.c.id == licence_id).values(**old_licence) + result = await database.execute(query=query) + return await get_licence_db(licence_id) + + +async def delete_licence(licence_id: int) -> None: + if await get_licence_db(licence_id) is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=Errors.LICENCE_IS_NOT_EXIST, + ) + query = licences.delete().where(licences.c.id == licence_id) + await database.execute(query) + + +async def get_count_employee_for_licence_id(licence_id: int) -> int: + query = employee_licences.select().where(employee_licences.c.licence_id == licence_id) + result = await database.fetch_all(query=query) + return len(result) + + +async def get_free_vacancy_in_licence(licence_id: int) -> int: + licence = await get_licence_db(licence_id) + count_employees = await get_count_employee_for_licence_id(licence_id) + if licence is not None: + return licence.count_members - count_employees + return 0 + + +async def get_employee_licence(employee_id: str) -> Optional[LicenceDB]: + query = employee_licences.select().where(employee_licences.c.employee_id == employee_id) + result = await database.fetch_one(query=query) + if result: + employee_licence = dict(result) + licence = await get_licence_db(employee_licence["licence_id"]) + return licence + return None + + +async def add_employee_licence(employee_id: str, licence_id: int) -> EmployeeLicenceDB: + if await get_employee_licence(str(employee_id)): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=Errors.USER_HAS_ANOTHER_LICENCE, + ) + if await get_licence_db(licence_id) is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=Errors.LICENCE_IS_NOT_EXIST, + ) + if await get_free_vacancy_in_licence(licence_id) <= 0: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=Errors.LICENCE_IS_FULL, + ) + employee_licence = EmployeeLicenceCreate(**dict({"employee_id": str(employee_id), "licence_id": licence_id})) + query = employee_licences.insert().values(**employee_licence.dict()) + employee_licence_id = await database.execute(query) + return EmployeeLicenceDB(**dict({"id": employee_licence_id, **employee_licence.dict()})) + + +async def update_employee_licence(employee_id: str, licence_id: int) -> EmployeeLicenceDB: + if await get_free_vacancy_in_licence(licence_id) <= 0: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=Errors.LICENCE_IS_FULL, + ) + employee_licence = EmployeeLicenceUpdate(**dict({"licence_id": licence_id})) + query = employee_licences.update().where(employee_licences.c.employee_id == employee_id).values( + **employee_licence.dict()) + employee_licence_id = await database.execute(query) + return EmployeeLicenceDB(**dict({"id": employee_licence_id, "employee_id": employee_id, **employee_licence.dict()})) + + +async def get_client_licence(client_id: int, licence_id: int) -> Optional[ClientLicenceDB]: + query = client_licences.select().\ + where((client_licences.c.client_id == client_id) & (client_licences.c.licence_id == licence_id)) + result = await database.fetch_one(query=query) + if result: + return ClientLicenceDB(**dict(result)) + return None + + +async def get_client_licences(client_id: int) -> List[Licence]: + query = client_licences.select().where(client_licences.c.client_id == client_id) + result = await database.fetch_all(query=query) + client_licences_list = [] + for client_licence in result: + client_licence = dict(client_licence) + licence = dict(await get_licence_db(client_licence["licence_id"])) + closed_vacancies = await get_count_employee_for_licence_id(client_licence["licence_id"]) + software = await get_software(licence["software_id"]) + client_licences_list.append( + Licence(**dict({**licence, "closed_vacancies": closed_vacancies, "software": software}))) + return client_licences_list + + +async def add_client_licence(client_id: int, licence_id: int) -> ClientLicenceDB: + if await get_client_licence(client_id, licence_id) is not None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=Errors.CLIENT_HAS_THIS_LICENCE, + ) + if await get_licence_db(licence_id) is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=Errors.LICENCE_IS_NOT_EXIST, + ) + client_licence = ClientLicenceCreate(**dict({"client_id": client_id, "licence_id": licence_id})) + query = client_licences.insert().values(**client_licence.dict()) + employee_licence_id = await database.execute(query) + client = await activate_client(client_id) + return ClientLicenceDB(**dict({"id": employee_licence_id, **client_licence.dict()})) + + +async def activate_client(client_id: int) -> Optional[ClientDB]: + current_client = await database.fetch_one(query=clients.select().where(clients.c.id == client_id)) + if current_client: + current_client = dict(current_client) + current_client["is_active"] = True + await database.execute(query=clients.update().where(clients.c.id == client_id).values(**current_client)) + return current_client diff --git a/src/service.py b/src/service.py new file mode 100644 index 0000000..9d76ea3 --- /dev/null +++ b/src/service.py @@ -0,0 +1,35 @@ +import asyncio +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart + +from pydantic import BaseModel + +from src.config import MAIL_LOGIN, MAIL_PWD + + +async def check_dict(result): + if result: + return dict(result) + return None + + +class Email(BaseModel): + recipient: str + title: str + message: str + + +async def send_mail(email: Email): + multipart_msg = MIMEMultipart() + multipart_msg['From'] = MAIL_LOGIN + multipart_msg['To'] = email.recipient + multipart_msg['Subject'] = email.title + multipart_msg.attach(MIMEText(email.message, 'plain')) + + smtp_obj = smtplib.SMTP('smtp.gmail.com', 587) + smtp_obj.starttls() + smtp_obj.login(MAIL_LOGIN, MAIL_PWD) + smtp_obj.send_message(multipart_msg) + smtp_obj.quit() + print(f"письмо отправлено {email.recipient}") diff --git a/src/users/__init__.py b/src/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/users/logic.py b/src/users/logic.py new file mode 100644 index 0000000..8e7e09d --- /dev/null +++ b/src/users/logic.py @@ -0,0 +1,232 @@ +from datetime import datetime + +from fastapi import HTTPException, Depends, status, Request +from fastapi_users import FastAPIUsers +from fastapi_users.authentication import JWTAuthentication +from fastapi_users.password import get_password_hash +from fastapi_users.router import ErrorCode +from sqlalchemy import desc + +from ..config import SECRET +from .schemas import User, UserCreate, UserUpdate, UserDB, DeveloperList, generate_pwd, EmployeeUpdate, DeveloperCreate +from .models import user_db, UserTable, users +from src.db.db import database +from src.errors import Errors + +from typing import Dict, Any, List, Optional +from pydantic.types import UUID4 +from pydantic import EmailStr +from ..desk.models import appeals +from ..reference_book.services import update_employee_licence +from ..service import send_mail, Email + +auth_backends = [] + +jwt_authentication = JWTAuthentication(secret=SECRET, lifetime_seconds=3600) + +auth_backends.append(jwt_authentication) + +all_users = FastAPIUsers( + user_db, + auth_backends, + User, + UserCreate, + UserUpdate, + UserDB, +) + +any_user = all_users.current_user(active=True) +employee = all_users.current_user(active=True, superuser=False) +developer_user = all_users.current_user(active=True, superuser=True) + + +default_uuid = UUID4("00000000-0000-0000-0000-000000000000") + + +async def get_owner(client_id: int, user: UserTable = Depends(any_user)): + if not (user.client_id == client_id and user.is_owner): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + return user + + +async def get_owner_with_superuser(client_id: int, user: UserTable = Depends(any_user)): + if not user.is_superuser and not (user.client_id == client_id and user.is_owner): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + return user + + +async def get_client_users(client_id: int, user: UserTable = Depends(any_user)): + if user.client_id != client_id: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + return user + + +async def get_client_users_with_superuser(client_id: int, user: UserTable = Depends(any_user)): + if not user.is_superuser and user.client_id != client_id: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + return user + + +async def user_is_active(user_id: UUID4) -> bool: + user = await get_or_404(user_id) + if user.is_active: + return True + return False + + +async def on_after_forgot_password(user: UserDB, token: str, request: Request): + print(f"User {user.id} has forgot their password. Reset token: {token}") + + +async def on_after_reset_password(user: UserDB, request: Request): + print(f"User {user.id} has reset their password.") + + +async def after_verification_request(user: UserDB, token: str, request: Request): + print(f"Verification requested for user {user.id}. Verification token: {token}") + + +async def after_verification(user: UserDB, request: Request): + print(f"{user.id} is now verified.") + + +async def get_count_dev_appeals(developer_id: str) -> int: + query = appeals.select().where(appeals.c.responsible_id == developer_id) + result = await database.fetch_all(query=query) + return len(result) + + +async def get_developers_db() -> List[UserDB]: + query = users.select().where(users.c.is_superuser == 1) + result = await database.fetch_all(query=query) + return [UserDB(**dict(developer)) for developer in result] + + +async def get_developers(last_id: UUID4 = default_uuid, limit: int = 9) -> List[DeveloperList]: + query = users.select()\ + .where((users.c.is_superuser == 1) & (users.c.id > str(last_id))).order_by(desc(users.c.id)).limit(limit) + result = await database.fetch_all(query=query) + developers = [] + for developer in result: + developer = dict(developer) + count_appeals = await get_count_dev_appeals(str(developer["id"])) + developers.append(DeveloperList(**dict({**developer, "count_appeals": count_appeals}))) + return developers + + +async def get_or_404(user_id: UUID4) -> UserDB: + user = await database.fetch_one(query=users.select().where(users.c.id == user_id)) + if user is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=Errors.USER_NOT_FOUND) + return user + + +async def get_user(user_id: UUID4) -> Optional[UserDB]: + user = await database.fetch_one(query=users.select().where(users.c.id == user_id)) + return user + + +async def pre_update_user(user_id: UUID4, item: EmployeeUpdate) -> UserDB: + if item.licence_id: + licence = await update_employee_licence(str(user_id), item.licence_id) + update_employee = UserUpdate(**dict(item)) + update_dict = update_employee.dict(exclude_unset=True, + exclude={"id", "email", "is_superuser", "is_verified"}) + updated_user = await update_user(user_id, update_dict) + return updated_user + + +async def pre_update_developer(user_id: UUID4, item: UserUpdate) -> UserDB: + update_dict = item.dict(exclude_unset=True, exclude={"id"}) + if "email" in update_dict.keys(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=Errors.FORBIDDEN_CHANGE_EMAIL + ) + updated_developer = await update_user(user_id, update_dict) + return updated_developer + + +async def update_user(user_id: UUID4, update_dict: Dict[str, Any]) -> UserDB: + user = await get_or_404(user_id) + user = dict(user) + for field in update_dict: + if field == "password" and update_dict[field]: + hashed_password = get_password_hash(update_dict[field]) + user["hashed_password"] = hashed_password + elif update_dict[field] is not None: + user[field] = update_dict[field] + updated_user = await user_db.update(UserDB(**user)) + + return updated_user + + +async def delete_user(user_id: UUID4): + user = await get_or_404(user_id) + await user_db.delete(user) + return None + + +async def create_developer(): + developer = DeveloperCreate( + email="admin@py.com", + password="123456", + is_active=True, + is_superuser=True, + is_verified=False, + name="admin", + surname="string", + patronymic="string", + avatar="string", + is_owner=False, + client_id=0, + date_reg=datetime.utcnow(), + ) + try: + created_developer = await all_users.create_user(developer, safe=False) + except Exception: + print(Exception) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ErrorCode.REGISTER_USER_ALREADY_EXISTS, + ) + return created_developer + + +async def get_user_by_email(email: EmailStr) -> Optional[UserDB]: + user = await database.fetch_one(query=users.select().where(users.c.email == email)) + if user: + return user + return None + + +async def change_pwd(user_id: UUID4, pwd: str, email: Email) -> UserDB: + if len(pwd) < 6: + raise ValueError('Password should be at least 6 characters') + updated_user = EmployeeUpdate(**dict({"password": pwd})) + updated_user = await pre_update_user(user_id, updated_user) + + await send_mail(email) + return updated_user + + +async def get_email_with_changed_pwd(pwd: str, user: UserTable) -> Email: + message = f"Добрый день!\nВы изменили свой пароль на: {pwd}\n" \ + f"Если Вы не меняли пароль обратитесь к администратору или смените его самостоятельно" + email = Email(recipient=user.email, title="Смена пароля в UDV Service Desk", message=message) + return email + + +async def get_new_password(email: EmailStr) -> UserDB: + user = await get_user_by_email(email) + if user is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=Errors.USER_NOT_FOUND) + pwd = generate_pwd() + message = f"Добрый день!\nВаш новый пароль: {pwd}\n" \ + f"Если Вы не запрашивали смену пароля обратитесь к администратору или смените его самостоятельно" + email = Email(recipient=user.email, title="Смена пароля в UDV Service Desk", message=message) + return await change_pwd(user.id, pwd, email) diff --git a/src/users/models.py b/src/users/models.py new file mode 100644 index 0000000..da150d9 --- /dev/null +++ b/src/users/models.py @@ -0,0 +1,22 @@ +from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase +from sqlalchemy import Column, Integer, Boolean, ForeignKey, DateTime, sql, String + +from .schemas import UserDB +from ..db.db import database, Base + + +class UserTable(Base, SQLAlchemyBaseUserTable): + __tablename__ = 'user' + + name = Column(String, nullable=False) + surname = Column(String, nullable=False) + patronymic = Column(String, nullable=True) + avatar = Column(String, nullable=True) + is_owner = Column(Boolean, default=False, nullable=True) + client_id = Column(Integer, ForeignKey('client.id')) + date_reg = Column(DateTime(timezone=True), server_default=sql.func.now()) + date_block = Column(DateTime, default=None, nullable=True) + + +users = UserTable.__table__ +user_db = SQLAlchemyUserDatabase(UserDB, database, users) diff --git a/src/users/routers.py b/src/users/routers.py new file mode 100644 index 0000000..4dd5cac --- /dev/null +++ b/src/users/routers.py @@ -0,0 +1,65 @@ +from fastapi import Depends, Response, HTTPException +from fastapi import APIRouter, status +from fastapi_users.router import ErrorCode +from fastapi.security import OAuth2PasswordRequestForm +from pydantic import EmailStr + +from .models import user_db +from .schemas import UserDB +from ..config import SECRET + +from src.users.logic import jwt_authentication, all_users, \ + after_verification, after_verification_request, any_user, \ + get_new_password, create_developer + +router = APIRouter() + + +@router.post("/auth/jwt/refresh") +async def refresh_jwt(response: Response, user=Depends(all_users.get_current_active_user)): + return await jwt_authentication.get_login_response(user, response) + + +@router.get("/me", response_model=UserDB) +async def me( + user: UserDB = Depends(any_user), # type: ignore +): + return user + + +@router.get("/create_developer", response_model=UserDB) +async def developer(): + return await create_developer() + + +@router.post("/forgot_password", response_model=UserDB, status_code=status.HTTP_201_CREATED) +async def forgot_password(email: EmailStr): + return await get_new_password(email) + + +@router.post("/auth/jwt/login") +async def login( + response: Response, credentials: OAuth2PasswordRequestForm = Depends() +): + user = await user_db.authenticate(credentials) + + if user is None or not user.is_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ErrorCode.LOGIN_BAD_CREDENTIALS, + ) + token = await jwt_authentication.get_login_response(user, response) + return {"token": token, "user": user} + + +router.include_router( + all_users.get_register_router(), + prefix="/auth", + tags=["auth"]) +router.include_router( + all_users.get_verify_router( + SECRET, + after_verification_request=after_verification_request, + after_verification=after_verification), + prefix="/auth", + tags=["auth"]) diff --git a/src/users/schemas.py b/src/users/schemas.py new file mode 100644 index 0000000..6d23655 --- /dev/null +++ b/src/users/schemas.py @@ -0,0 +1,100 @@ +from datetime import datetime +import random + +from fastapi_users import models +from pydantic import validator, EmailStr, BaseModel +from typing import Optional + +from src.reference_book.schemas import LicenceDB + + +def generate_pwd(): + symbols_list = "+-/*!&$#?=@<>abcdefghijklnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" + password = "" + length = random.randint(6, 10) + for i in range(length): + password += random.choice(symbols_list) + return password + + +class User(models.BaseUser): + name: str + surname: str + patronymic: Optional[str] + avatar: Optional[str] + # is_owner: bool + # client_id: int + # date_block: Optional[datetime] + + +class UserCreate(models.BaseUserCreate): + name: str + surname: str + patronymic: Optional[str] + avatar: Optional[str] + password: str = generate_pwd() + is_owner: Optional[bool] = False + client_id: Optional[int] = 0 + date_reg: datetime = datetime.utcnow() + date_block: Optional[datetime] + + @validator('password') + def valid_password(cls, v: str): + if len(v) < 6: + raise ValueError('Password should be at least 6 characters') + return v + + +class EmployeeCreate(UserCreate, models.BaseUserCreate): + is_owner: bool = False + client_id: int + date_reg: datetime = datetime.utcnow() + + +class PreEmployeeCreate(EmployeeCreate, models.BaseUserCreate): + licence_id: int + client_id: Optional[int] + + +class DeveloperCreate(UserCreate, models.BaseUserCreate): + is_superuser = True + date_reg: datetime = datetime.utcnow() + + +class OwnerCreate(UserCreate, models.BaseUserCreate): + is_owner: bool = True + client_id: int + date_reg: datetime = datetime.utcnow() + + +class UserUpdate(User, models.BaseUserUpdate): + name: Optional[str] + surname: Optional[str] + is_owner: Optional[bool] = False + is_active: Optional[bool] + date_block: Optional[datetime] + email: Optional[EmailStr] + + +class UserDB(User, models.BaseUserDB): + is_active: bool = True + is_owner: Optional[bool] + client_id: Optional[int] + date_reg: datetime = datetime.utcnow() + date_block: Optional[datetime] + + +class Employee(UserDB): + licence: Optional[LicenceDB] + + +class EmployeeUpdate(UserUpdate): + licence_id: Optional[int] + + +class EmployeeList(Employee): + count_appeals: int + + +class DeveloperList(User): + count_appeals: int diff --git a/test.db b/test.db index 56f26de..ac9a4b5 100644 Binary files a/test.db and b/test.db differ diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_home.py b/tests/test_home.py new file mode 100644 index 0000000..6c03f52 --- /dev/null +++ b/tests/test_home.py @@ -0,0 +1,12 @@ +import pytest +from httpx import AsyncClient + +from src.app import app + + +@pytest.mark.asyncio +async def test_root(): + async with AsyncClient(app=app, base_url="http://0.0.0.0") as ac: + response = await ac.get("/") + assert response.status_code == 200 + assert response.json() == {'Hello': 'World'} diff --git a/tests/tests_clients/__init__.py b/tests/tests_clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_clients/test_add.py b/tests/tests_clients/test_add.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_clients/test_delete.py b/tests/tests_clients/test_delete.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_clients/test_get.py b/tests/tests_clients/test_get.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_clients/test_update.py b/tests/tests_clients/test_update.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_desk/__init__.py b/tests/tests_desk/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_desk/test_add.py b/tests/tests_desk/test_add.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_desk/test_delete.py b/tests/tests_desk/test_delete.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_desk/test_get.py b/tests/tests_desk/test_get.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_desk/test_routers.py b/tests/tests_desk/test_routers.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_desk/test_update.py b/tests/tests_desk/test_update.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_reference_book/__init__.py b/tests/tests_reference_book/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_reference_book/test_add.py b/tests/tests_reference_book/test_add.py new file mode 100644 index 0000000..79ed55d --- /dev/null +++ b/tests/tests_reference_book/test_add.py @@ -0,0 +1,109 @@ +from datetime import datetime + +import pytest + +from src.db.db import database +from src.reference_book.models import softwares, licences, modules +from src.reference_book.schemas import SoftwareCreate, LicenceCreate, ModuleCreate +from src.reference_book.services import add_software, add_licence, add_module + + +async def clean_db(table): + query = table.delete() + await database.execute(query) + + +async def init_software(): + software = { + "name": "Windows" + } + query = softwares.insert().values(software) + await database.execute(query) + + +async def init_licence(): + licence = { + "number": 555, + "count_members": 5, + "date_end": datetime.utcnow(), + "client_id": 1, + "software_id": 1 + } + query = licences.insert().values(licence) + await database.execute(query) + + +async def init_module(): + module = { + "name": "Registration", + "software_id": 1 + } + query = modules.insert().values(module) + await database.execute(query) + + +# @pytest.fixture() +# async def prepare_db(table, method): +# await clean_db(table) +# await method() +# yield + + +@pytest.fixture() +async def prepare_db(): + await clean_db(softwares) + await clean_db(licences) + await clean_db(modules) + await init_software() + await init_licence() + await init_module() + yield + + +@pytest.mark.asyncio +@pytest.mark.usefixtures('prepare_db') +# @pytest.mark.usefixtures('prepare_db(softwares, init_software)') +@pytest.mark.parametrize("name", [ + "Adobe Photoshop", "Adobe Illustrator" +]) +async def test_add_software(name: str): + software = SoftwareCreate(name=name) + result = await add_software(software) + assert result is not None + + +@pytest.mark.asyncio +@pytest.mark.usefixtures('prepare_db') +# @pytest.mark.usefixtures('prepare_db(licences, init_licence)') +@pytest.mark.parametrize( + ("number", "count_members", "date_end", "client_id", "software_id"), [ + (-500, 5, datetime.utcnow(), 0, 1), + (545, 5, datetime.utcnow(), 1, 10), + (555, 5, datetime.utcnow(), -1, 10), + ]) +async def test_add_licence( + number: int, + count_members: int, + date_end: datetime, + client_id: int, + software_id: int): + licence = LicenceCreate(number=number, + count_members=count_members, + date_end=date_end, + client_id=client_id, + software_id=software_id) + result = await add_licence(licence) + assert result is not None + + +@pytest.mark.asyncio +@pytest.mark.usefixtures('prepare_db') +# @pytest.mark.usefixtures('prepare_db(modules, init_module)') +@pytest.mark.parametrize(("name", "software_id"), [ + ("authorization", -1), + ("", 10), +]) +async def test_add_module(name: str, software_id: int): + module = ModuleCreate(name=name, software_id=software_id) + result = await add_module(module) + assert result is not None diff --git a/tests/tests_reference_book/test_delete.py b/tests/tests_reference_book/test_delete.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_reference_book/test_get.py b/tests/tests_reference_book/test_get.py new file mode 100644 index 0000000..44c607d --- /dev/null +++ b/tests/tests_reference_book/test_get.py @@ -0,0 +1,99 @@ +from datetime import datetime + +import pytest + +from src.db.db import database +from src.reference_book.models import softwares, licences, modules +from src.reference_book.schemas import SoftwareCreate, LicenceCreate, ModuleCreate +from src.reference_book.services import add_software, add_licence, add_module + + +async def clean_db(table): + query = table.delete() + await database.execute(query) + + +async def init_software(): + software = { + "name": "Windows" + } + query = softwares.insert().values(software) + await database.execute(query) + + +async def init_licence(): + licence = { + "number": 555, + "count_members": 5, + "date_end": datetime.utcnow(), + "client_id": 1, + "software_id": 1 + } + query = licences.insert().values(licence) + await database.execute(query) + + +async def init_module(): + module = { + "name": "Registration", + "software_id": 1 + } + query = modules.insert().values(module) + await database.execute(query) + + +@pytest.fixture() +async def prepare_db(): + await clean_db(softwares) + await clean_db(licences) + await clean_db(modules) + await init_software() + await init_licence() + await init_module() + yield + + +@pytest.mark.asyncio +@pytest.mark.usefixtures('prepare_db') +@pytest.mark.parametrize("name", [ + "Adobe Photoshop", "Adobe Illustrator" +]) +async def test_add_software(name: str): + software = SoftwareCreate(name=name) + result = await add_software(software) + assert result is not None + + +@pytest.mark.asyncio +@pytest.mark.usefixtures('prepare_db') +@pytest.mark.parametrize( + ("number", "count_members", "date_end", "client_id", "software_id"), [ + (-500, 5, datetime.utcnow(), 0, 1), + (545, 5, datetime.utcnow(), 1, 10), + (555, 5, datetime.utcnow(), -1, 10), + ]) +async def test_add_licence( + number: int, + count_members: int, + date_end: datetime, + client_id: int, + software_id: int): + licence = LicenceCreate(number=number, + count_members=count_members, + date_end=date_end, + client_id=client_id, + software_id=software_id) + result = await add_licence(licence) + assert result is not None + + +@pytest.mark.asyncio +@pytest.mark.usefixtures('prepare_db') +@pytest.mark.parametrize(("name", "software_id"), [ + ("authorization", -1), + ("", 10), +]) +async def test_add_module(name: str, software_id: int): + module = ModuleCreate(name=name, software_id=software_id) + result = await add_module(module) + assert result is not None \ No newline at end of file diff --git a/tests/tests_reference_book/test_routes.py b/tests/tests_reference_book/test_routes.py new file mode 100644 index 0000000..adf0b87 --- /dev/null +++ b/tests/tests_reference_book/test_routes.py @@ -0,0 +1,127 @@ +from typing import List + +import pytest +from httpx import AsyncClient + +from src.app import app +from src.reference_book.schemas import ModuleShort, Module, ModuleDB, \ + LicenceShort, SoftwareShort, Software, Licence, LicenceDB + + +@pytest.fixture(autouse=True) +async def client(): + async with AsyncClient( + app=app, + base_url="http://0.0.0.0/reference/") as client: + yield client + + +# TODO сделать отправку данных и сравнения с моделями + +@pytest.mark.asyncio +async def test_get_modules(client): + response = await client.get("/modules/") + assert response.status_code == 200 + assert response.json() is List[ModuleShort] + + +@pytest.mark.asyncio +async def test_get_module(client): + response = await client.get("/modules/1") + assert response.status_code == 200 + assert response.json() is Module + + +@pytest.mark.asyncio +async def test_post_module(client): + response = await client.post("/modules/") + assert response.status_code == 201 + assert response.json() is ModuleDB + + +@pytest.mark.asyncio +async def test_put_module(client): + response = await client.put("/modules/1") + assert response.status_code == 201 + assert response.json() is ModuleDB + + +@pytest.mark.asyncio +async def test_delete_module(client): + response = await client.delete("/modules/1") + assert response.status_code == 204 + + +@pytest.mark.asyncio +async def test_get_licences(client): + response = await client.get("/licences") + assert response.status_code == 200 + assert response.json is List[LicenceShort] + + +@pytest.mark.asyncio +async def test_get_licence(client): + response = await client.get("/licences/1") + assert response.status_code == 200 + assert response.json() is Licence + + +@pytest.mark.asyncio +async def test_post_licence(client): + response = await client.post("/licences/") + assert response.status_code == 201 + assert response.json() is LicenceDB + + +@pytest.mark.asyncio +async def test_put_licence(client): + response = await client.put("/licences/1") + assert response.status_code == 201 + assert response.json() is LicenceDB + + +@pytest.mark.asyncio +async def test_delete_licence(client): + response = await client.delete("/licences/1") + assert response.status_code == 204 + + +@pytest.mark.asyncio +async def test_get_softwares(client): + response = await client.get("/software") + assert response.status_code == 200 + assert response.json() is List[SoftwareShort] + + +@pytest.mark.asyncio +async def test_get_software(client): + response = await client.get("/software/1") + assert response.status_code == 200 + assert response.json() is SoftwareShort + + +@pytest.mark.asyncio +async def test_get_software_with_modules(client): + response = await client.get("/software/1/modules") + assert response.status_code == 200 + assert response.json() is Software + + +@pytest.mark.asyncio +async def test_post_software(client): + response = await client.post("/software/") + assert response.status_code == 201 + assert response.json() is SoftwareShort + + +@pytest.mark.asyncio +async def test_put_software(client): + response = await client.put("/software/1") + assert response.status_code == 201 + assert response.json() is SoftwareShort + + +@pytest.mark.asyncio +async def test_delete_software(client): + response = await client.delete("/software/1") + assert response.status_code == 204 diff --git a/tests/tests_reference_book/test_update.py b/tests/tests_reference_book/test_update.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_users/__init__.py b/tests/tests_users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_users/test_add.py b/tests/tests_users/test_add.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_users/test_delete.py b/tests/tests_users/test_delete.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_users/test_get.py b/tests/tests_users/test_get.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_users/test_update.py b/tests/tests_users/test_update.py new file mode 100644 index 0000000..e69de29