refactor: mixins for services

This commit is contained in:
2025-09-08 10:59:06 +04:00
parent 67634836dc
commit d73748deab
16 changed files with 207 additions and 205 deletions

View File

@ -1,25 +1,18 @@
from sqlalchemy import Select
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from models import Board from models import Board
from repositories.base import BaseRepository from repositories.base import BaseRepository
from repositories.mixins import ( from repositories.mixins import *
RepDeleteMixin,
RepCreateMixin,
GetByIdMixin,
GetAllMixin,
RepUpdateMixin,
)
from schemas.board import UpdateBoardSchema, CreateBoardSchema from schemas.board import UpdateBoardSchema, CreateBoardSchema
class BoardRepository( class BoardRepository(
BaseRepository, BaseRepository,
GetAllMixin[Board], RepGetAllMixin[Board],
RepDeleteMixin[Board], RepDeleteMixin[Board],
RepCreateMixin[Board, CreateBoardSchema], RepCreateMixin[Board, CreateBoardSchema],
RepUpdateMixin[Board, UpdateBoardSchema], RepUpdateMixin[Board, UpdateBoardSchema],
GetByIdMixin[Board], RepGetByIdMixin[Board],
): ):
entity_class = Board entity_class = Board

View File

@ -1,16 +1,8 @@
from typing import Optional
from sqlalchemy import select, Select
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from models import Deal, CardStatusHistory, Board from models import Deal, CardStatusHistory, Board
from repositories.base import BaseRepository from repositories.base import BaseRepository
from repositories.mixins import ( from repositories.mixins import *
RepDeleteMixin,
RepCreateMixin,
GetByIdMixin,
RepUpdateMixin,
)
from schemas.base import SortDir from schemas.base import SortDir
from schemas.deal import UpdateDealSchema, CreateDealSchema from schemas.deal import UpdateDealSchema, CreateDealSchema
from utils.sorting import apply_sorting from utils.sorting import apply_sorting
@ -21,7 +13,7 @@ class DealRepository(
RepDeleteMixin[Deal], RepDeleteMixin[Deal],
RepCreateMixin[Deal, CreateDealSchema], RepCreateMixin[Deal, CreateDealSchema],
RepUpdateMixin[Deal, UpdateDealSchema], RepUpdateMixin[Deal, UpdateDealSchema],
GetByIdMixin[Deal], RepGetByIdMixin[Deal],
): ):
entity_class = Deal entity_class = Deal

View File

@ -1,18 +1,14 @@
from typing import Generic, TypeVar, Type, Optional from typing import Type, Optional
from sqlalchemy import select, Select from sqlalchemy import select, Select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
EntityType = TypeVar("EntityType")
CreateType = TypeVar("CreateType")
UpdateType = TypeVar("UpdateType")
class RepBaseMixin[EntityType]:
class RepBaseMixin(Generic[EntityType]):
session: AsyncSession session: AsyncSession
class RepDeleteMixin(RepBaseMixin[EntityType]): class RepDeleteMixin[EntityType](RepBaseMixin[EntityType]):
async def delete(self, obj: EntityType, is_soft: bool) -> None: async def delete(self, obj: EntityType, is_soft: bool) -> None:
if not is_soft: if not is_soft:
await self.session.delete(obj) await self.session.delete(obj)
@ -28,7 +24,7 @@ class RepDeleteMixin(RepBaseMixin[EntityType]):
await self.session.commit() await self.session.commit()
class RepCreateMixin(RepBaseMixin[EntityType], Generic[EntityType, CreateType]): class RepCreateMixin[EntityType, CreateType](RepBaseMixin[EntityType]):
entity_class: Type[EntityType] entity_class: Type[EntityType]
async def create(self, data: CreateType) -> int: async def create(self, data: CreateType) -> int:
@ -39,7 +35,7 @@ class RepCreateMixin(RepBaseMixin[EntityType], Generic[EntityType, CreateType]):
return obj.id return obj.id
class RepUpdateMixin(RepBaseMixin[EntityType], Generic[EntityType, UpdateType]): class RepUpdateMixin[EntityType, UpdateType](RepBaseMixin[EntityType]):
async def _apply_update_data_to_model( async def _apply_update_data_to_model(
self, self,
model: EntityType, model: EntityType,
@ -61,8 +57,11 @@ class RepUpdateMixin(RepBaseMixin[EntityType], Generic[EntityType, UpdateType]):
await self.session.refresh(model) await self.session.refresh(model)
return model return model
async def update(self, entity: EntityType, data: UpdateType) -> EntityType:
pass
class GetByIdMixin(RepBaseMixin[EntityType]):
class RepGetByIdMixin[EntityType](RepBaseMixin[EntityType]):
entity_class: Type[EntityType] entity_class: Type[EntityType]
def _process_get_by_id_stmt(self, stmt: Select) -> Select: def _process_get_by_id_stmt(self, stmt: Select) -> Select:
@ -78,7 +77,7 @@ class GetByIdMixin(RepBaseMixin[EntityType]):
return result.scalar_one_or_none() return result.scalar_one_or_none()
class GetAllMixin(RepBaseMixin[EntityType]): class RepGetAllMixin[EntityType](RepBaseMixin[EntityType]):
entity_class: Type[EntityType] entity_class: Type[EntityType]
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select: def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:

View File

@ -1,25 +1,18 @@
from sqlalchemy import Select
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from models.project import Project from models.project import Project
from repositories.base import BaseRepository from repositories.base import BaseRepository
from repositories.mixins import ( from repositories.mixins import *
RepDeleteMixin,
RepCreateMixin,
GetByIdMixin,
GetAllMixin,
RepUpdateMixin,
)
from schemas.project import CreateProjectSchema, UpdateProjectSchema from schemas.project import CreateProjectSchema, UpdateProjectSchema
class ProjectRepository( class ProjectRepository(
BaseRepository, BaseRepository,
GetAllMixin[Project], RepGetAllMixin[Project],
RepDeleteMixin[Project], RepDeleteMixin[Project],
RepCreateMixin[Project, CreateProjectSchema], RepCreateMixin[Project, CreateProjectSchema],
RepUpdateMixin[Project, UpdateProjectSchema], RepUpdateMixin[Project, UpdateProjectSchema],
GetByIdMixin[Project], RepGetByIdMixin[Project],
): ):
entity_class = Project entity_class = Project

View File

@ -1,24 +1,18 @@
from sqlalchemy import select, func, Select from sqlalchemy import func
from models import Status, Deal from models import Status, Deal
from repositories.base import BaseRepository from repositories.base import BaseRepository
from repositories.mixins import ( from repositories.mixins import *
RepDeleteMixin,
RepCreateMixin,
GetByIdMixin,
GetAllMixin,
RepUpdateMixin,
)
from schemas.status import UpdateStatusSchema, CreateStatusSchema from schemas.status import UpdateStatusSchema, CreateStatusSchema
class StatusRepository( class StatusRepository(
BaseRepository, BaseRepository,
GetAllMixin[Status], RepGetAllMixin[Status],
RepDeleteMixin[Status], RepDeleteMixin[Status],
RepCreateMixin[Status, CreateStatusSchema], RepCreateMixin[Status, CreateStatusSchema],
RepUpdateMixin[Status, UpdateStatusSchema], RepUpdateMixin[Status, UpdateStatusSchema],
GetByIdMixin[Status], RepGetByIdMixin[Status],
): ):
entity_class = Status entity_class = Status

View File

@ -18,7 +18,7 @@ async def get_boards(
session: SessionDependency, session: SessionDependency,
project_id: int = Path(alias="projectId"), project_id: int = Path(alias="projectId"),
): ):
return await BoardService(session).get_boards(project_id) return await BoardService(session).get_all(project_id)
@board_router.post( @board_router.post(
@ -30,7 +30,7 @@ async def create_board(
session: SessionDependency, session: SessionDependency,
request: CreateBoardRequest, request: CreateBoardRequest,
): ):
return await BoardService(session).create_board(request) return await BoardService(session).create(request)
@board_router.patch( @board_router.patch(
@ -43,7 +43,7 @@ async def update_board(
request: UpdateBoardRequest, request: UpdateBoardRequest,
pk: int = Path(), pk: int = Path(),
): ):
return await BoardService(session).update_board(pk, request) return await BoardService(session).update(pk, request)
@board_router.delete( @board_router.delete(
@ -55,4 +55,4 @@ async def delete_board(
session: SessionDependency, session: SessionDependency,
pk: int = Path(), pk: int = Path(),
): ):
return await BoardService(session).delete_board(pk) return await BoardService(session).delete(pk)

View File

@ -28,7 +28,7 @@ async def get_deals(
id: Optional[int] = Query(default=None), id: Optional[int] = Query(default=None),
name: Optional[str] = Query(default=None), name: Optional[str] = Query(default=None),
): ):
return await DealService(session).get_deals( return await DealService(session).get_all(
pagination, pagination,
sorting, sorting,
project_id, project_id,
@ -48,7 +48,7 @@ async def create_deal(
session: SessionDependency, session: SessionDependency,
request: CreateDealRequest, request: CreateDealRequest,
): ):
return await DealService(session).create_deal(request) return await DealService(session).create(request)
@deal_router.patch( @deal_router.patch(
@ -61,7 +61,7 @@ async def update_deal(
request: UpdateDealRequest, request: UpdateDealRequest,
pk: int = Path(), pk: int = Path(),
): ):
return await DealService(session).update_deal(pk, request) return await DealService(session).update(pk, request)
@deal_router.delete( @deal_router.delete(
@ -73,4 +73,4 @@ async def delete_deal(
session: SessionDependency, session: SessionDependency,
pk: int = Path(), pk: int = Path(),
): ):
return await DealService(session).delete_deal(pk) return await DealService(session).delete(pk)

View File

@ -17,7 +17,7 @@ project_router = APIRouter(
async def get_projects( async def get_projects(
session: SessionDependency, session: SessionDependency,
): ):
return await ProjectService(session).get_projects() return await ProjectService(session).get_all()
@project_router.post( @project_router.post(
@ -29,7 +29,7 @@ async def create_project(
session: SessionDependency, session: SessionDependency,
request: CreateProjectRequest, request: CreateProjectRequest,
): ):
return await ProjectService(session).create_project(request) return await ProjectService(session).create(request)
@project_router.patch( @project_router.patch(
@ -42,7 +42,7 @@ async def update_project(
request: UpdateProjectRequest, request: UpdateProjectRequest,
pk: int = Path(), pk: int = Path(),
): ):
return await ProjectService(session).update_project(pk, request) return await ProjectService(session).update(pk, request)
@project_router.delete( @project_router.delete(
@ -54,4 +54,4 @@ async def delete_project(
session: SessionDependency, session: SessionDependency,
pk: int = Path(), pk: int = Path(),
): ):
return await ProjectService(session).delete_project(pk) return await ProjectService(session).delete(pk)

View File

@ -18,7 +18,7 @@ async def get_statuses(
session: SessionDependency, session: SessionDependency,
board_id: int = Path(alias="boardId"), board_id: int = Path(alias="boardId"),
): ):
return await StatusService(session).get_statuses(board_id) return await StatusService(session).get_all(board_id)
@status_router.post( @status_router.post(
@ -30,7 +30,7 @@ async def create_status(
session: SessionDependency, session: SessionDependency,
request: CreateStatusRequest, request: CreateStatusRequest,
): ):
return await StatusService(session).create_status(request) return await StatusService(session).create(request)
@status_router.patch( @status_router.patch(
@ -43,7 +43,7 @@ async def update_status(
request: UpdateStatusRequest, request: UpdateStatusRequest,
pk: int = Path(), pk: int = Path(),
): ):
return await StatusService(session).update_status(pk, request) return await StatusService(session).update(pk, request)
@status_router.delete( @status_router.delete(
@ -55,4 +55,4 @@ async def delete_status(
session: SessionDependency, session: SessionDependency,
pk: int = Path(), pk: int = Path(),
): ):
return await StatusService(session).delete_status(pk) return await StatusService(session).delete(pk)

View File

@ -14,6 +14,7 @@ class BaseSchema(BaseModel):
from_attributes = True from_attributes = True
alias_generator = to_camel alias_generator = to_camel
populate_by_name = True populate_by_name = True
arbitrary_types_allowed = True
@classmethod @classmethod
def from_sql_model(cls, model, fields: dict): def from_sql_model(cls, model, fields: dict):

21
schemas/base_crud.py Normal file
View File

@ -0,0 +1,21 @@
from typing import TypeVar
from schemas.base import BaseResponse, BaseSchema
SchemaType = TypeVar("SchemaType", bound=BaseSchema)
class BaseGetAllResponse[SchemaType](BaseSchema):
items: list[SchemaType]
class BaseCreateResponse[SchemaType](BaseResponse):
entity: SchemaType
class BaseUpdateResponse(BaseResponse):
pass
class BaseDeleteResponse(BaseResponse):
pass

View File

@ -1,43 +1,23 @@
from fastapi import HTTPException from models import Board
from sqlalchemy.ext.asyncio import AsyncSession
from repositories import BoardRepository from repositories import BoardRepository
from schemas.board import * from schemas.board import *
from services.mixins import *
class BoardService: class BoardService(
ServiceGetAllMixin[Board, BoardSchema],
ServiceCreateMixin[Board, CreateBoardRequest, BoardSchema],
ServiceUpdateMixin[Board, UpdateBoardRequest],
ServiceDeleteMixin[Board],
):
schema_class = BoardSchema
entity_not_found_msg = "Доска не найдена"
entity_deleted_msg = "Доска успешно удалена"
entity_updated_msg = "Доска успешно обновлена"
entity_created_msg = "Доска успешно создана"
def __init__(self, session: AsyncSession): def __init__(self, session: AsyncSession):
self.repository = BoardRepository(session) self.repository = BoardRepository(session)
async def get_boards(self, project_id: int) -> GetBoardsResponse: async def is_soft_delete(self, board: BoardSchema) -> bool:
boards = await self.repository.get_all(project_id) return len(board.deals) > 0
return GetBoardsResponse(
items=[BoardSchema.model_validate(board) for board in boards]
)
async def create_board(self, request: CreateBoardRequest) -> CreateBoardResponse:
board_id = await self.repository.create(request.entity)
board = await self.repository.get_by_id(board_id)
return CreateBoardResponse(
entity=BoardSchema.model_validate(board),
message="Доска успешно создана",
)
async def update_board(
self, board_id: int, request: UpdateBoardRequest
) -> UpdateBoardResponse:
board = await self.repository.get_by_id(board_id)
if not board:
raise HTTPException(status_code=404, detail="Доска не найдена")
await self.repository.update(board, request.entity)
return UpdateBoardResponse(message="Доска успешно обновлена")
async def delete_board(self, board_id: int) -> DeleteBoardResponse:
board = await self.repository.get_by_id(board_id)
if not board:
raise HTTPException(status_code=404, detail="Доска не найдена")
is_soft_needed: bool = len(board.deals) > 0
await self.repository.delete(board, is_soft_needed)
return DeleteBoardResponse(message="Доска успешно удалена")

View File

@ -1,18 +1,27 @@
import math import math
from fastapi import HTTPException from models import Deal
from sqlalchemy.ext.asyncio import AsyncSession
from repositories import DealRepository from repositories import DealRepository
from schemas.base import PaginationSchema, SortingSchema from schemas.base import PaginationSchema, SortingSchema
from schemas.deal import * from schemas.deal import *
from services.mixins import *
class DealService: class DealService(
ServiceCreateMixin[Deal, CreateDealRequest, DealSchema],
ServiceUpdateMixin[Deal, UpdateDealRequest],
ServiceDeleteMixin[Deal],
):
schema_class = DealSchema
entity_not_found_msg = "Сделка не найдена"
entity_deleted_msg = "Сделка успешно удалена"
entity_updated_msg = "Сделка успешно обновлена"
entity_created_msg = "Сделка успешно создана"
def __init__(self, session: AsyncSession): def __init__(self, session: AsyncSession):
self.repository = DealRepository(session) self.repository = DealRepository(session)
async def get_deals( async def get_all(
self, self,
pagination: PaginationSchema, pagination: PaginationSchema,
sorting: SortingSchema, sorting: SortingSchema,
@ -36,27 +45,3 @@ class DealService:
total_pages=total_pages, total_items=total_items total_pages=total_pages, total_items=total_items
), ),
) )
async def create_deal(self, request: CreateDealRequest) -> CreateDealResponse:
deal_id = await self.repository.create(request.entity)
deal = await self.repository.get_by_id(deal_id)
return CreateDealResponse(
entity=DealSchema.model_validate(deal),
message="Сделка успешно создана",
)
async def update_deal(self, deal_id: int, request: UpdateDealRequest):
deal = await self.repository.get_by_id(deal_id)
if not deal:
raise HTTPException(status_code=404, detail="Сделка не найдена")
await self.repository.update(deal, request.entity)
return UpdateDealResponse(message="Сделка успешно обновлена")
async def delete_deal(self, deal_id: int) -> DeleteDealResponse:
deal = await self.repository.get_by_id(deal_id)
if not deal:
raise HTTPException(status_code=404, detail="Сделка не найдена")
await self.repository.delete(deal, True)
return DeleteDealResponse(message="Сделка успешно удалена")

84
services/mixins.py Normal file
View File

@ -0,0 +1,84 @@
from typing import Generic
from fastapi import HTTPException
from repositories.mixins import *
from schemas.base_crud import *
RepositoryMixin = TypeVar("RepositoryMixin")
EntityType = TypeVar("EntityType")
SchemaType = TypeVar("SchemaType", bound=BaseSchema)
UpdateRequestType = TypeVar("UpdateRequestType", bound=BaseSchema)
CreateRequestType = TypeVar("CreateRequestType", bound=BaseSchema)
class ServiceBaseMixin(Generic[RepositoryMixin]):
repository: RepositoryMixin
class ServiceCreateMixin(
Generic[EntityType, CreateRequestType, SchemaType],
ServiceBaseMixin[RepCreateMixin | RepGetByIdMixin],
):
entity_created_msg = "Entity created"
schema_class: type[SchemaType]
async def create(self, request: CreateRequestType) -> BaseCreateResponse:
entity_id = await self.repository.create(request.entity)
entity = await self.repository.get_by_id(entity_id)
return BaseCreateResponse(
entity=self.schema_class.model_validate(entity),
message=self.entity_created_msg,
)
class ServiceGetAllMixin(
ServiceBaseMixin[RepGetAllMixin],
Generic[EntityType, SchemaType],
):
schema_class: type[SchemaType]
async def get_all(self, *args) -> BaseGetAllResponse[SchemaType]:
entities = await self.repository.get_all(*args)
return BaseGetAllResponse[SchemaType](
items=[self.schema_class.model_validate(entity) for entity in entities]
)
class ServiceUpdateMixin(
ServiceBaseMixin[RepUpdateMixin | RepGetByIdMixin],
Generic[EntityType, UpdateRequestType],
):
entity_not_found_msg = "Entity not found"
entity_updated_msg = "Entity updated"
async def update(
self, entity_id: int, request: UpdateRequestType
) -> BaseUpdateResponse:
entity = await self.repository.get_by_id(entity_id)
if not entity:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
await self.repository.update(entity, request.entity)
return BaseUpdateResponse(message=self.entity_updated_msg)
class ServiceDeleteMixin(
ServiceBaseMixin[RepDeleteMixin | RepGetByIdMixin],
Generic[EntityType],
):
entity_not_found_msg = "Entity not found"
entity_deleted_msg = "Entity deleted"
async def is_soft_delete(self, entity: EntityType) -> bool:
return hasattr(entity, "is_deleted")
async def delete(self, entity_id: int) -> BaseDeleteResponse:
entity = await self.repository.get_by_id(entity_id)
if not entity:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
is_soft = await self.is_soft_delete(entity)
await self.repository.delete(entity, is_soft)
return BaseDeleteResponse(message=self.entity_deleted_msg)

View File

@ -1,45 +1,23 @@
from fastapi import HTTPException from models import Project
from sqlalchemy.ext.asyncio import AsyncSession
from repositories import ProjectRepository from repositories import ProjectRepository
from schemas.project import * from schemas.project import *
from services.mixins import *
class ProjectService: class ProjectService(
ServiceGetAllMixin[Project, ProjectSchema],
ServiceCreateMixin[Project, CreateProjectRequest, ProjectSchema],
ServiceUpdateMixin[Project, UpdateProjectRequest],
ServiceDeleteMixin[Project],
):
schema_class = ProjectSchema
entity_not_found_msg = "Проект не найден"
entity_deleted_msg = "Проект успешно удален"
entity_updated_msg = "Проект успешно обновлен"
entity_created_msg = "Проект успешно создан"
def __init__(self, session: AsyncSession): def __init__(self, session: AsyncSession):
self.repository = ProjectRepository(session) self.repository = ProjectRepository(session)
async def get_projects(self) -> GetProjectsResponse: async def is_soft_delete(self, project: ProjectSchema) -> bool:
projects = await self.repository.get_all() return len(project.boards) > 0
return GetProjectsResponse(
items=[ProjectSchema.model_validate(project) for project in projects]
)
async def create_project(
self, request: CreateProjectRequest
) -> CreateProjectResponse:
project_id = await self.repository.create(request.entity)
project = await self.repository.get_by_id(project_id)
return CreateProjectResponse(
entity=ProjectSchema.model_validate(project),
message="Проект успешно создан",
)
async def update_project(
self, project_id: int, request: UpdateProjectRequest
) -> UpdateProjectResponse:
project = await self.repository.get_by_id(project_id)
if not project:
raise HTTPException(status_code=404, detail="Проект не найден")
await self.repository.update(project, request.entity)
return UpdateProjectResponse(message="Проект успешно обновлен")
async def delete_project(self, project_id: int) -> DeleteProjectResponse:
project = await self.repository.get_by_id(project_id)
if not project:
raise HTTPException(status_code=404, detail="Проект не найден")
is_soft_needed: bool = len(project.boards) > 0
await self.repository.delete(project, is_soft_needed)
return DeleteProjectResponse(message="Проект успешно удален")

View File

@ -1,43 +1,25 @@
from fastapi import HTTPException from models import Status
from sqlalchemy.ext.asyncio import AsyncSession
from repositories import StatusRepository from repositories import StatusRepository
from schemas.board import UpdateBoardResponse
from schemas.status import * from schemas.status import *
from services.mixins import *
class StatusService: class StatusService(
ServiceGetAllMixin[Status, StatusSchema],
ServiceCreateMixin[Status, CreateStatusRequest, StatusSchema],
ServiceUpdateMixin[Status, UpdateStatusRequest],
ServiceDeleteMixin[Status],
):
schema_class = StatusSchema
entity_not_found_msg = "Статус не найден"
entity_deleted_msg = "Статус успешно удален"
entity_updated_msg = "Статус успешно обновлен"
entity_created_msg = "Статус успешно создан"
def __init__(self, session: AsyncSession): def __init__(self, session: AsyncSession):
self.repository = StatusRepository(session) self.repository = StatusRepository(session)
async def get_statuses(self, board_id: int) -> GetStatusesResponse: async def is_soft_delete(self, status: StatusSchema) -> bool:
statuses = await self.repository.get_all(board_id) deals_count = await self.repository.get_deals_count(status.id)
return GetStatusesResponse(
items=[StatusSchema.model_validate(status) for status in statuses]
)
async def create_status(self, request: CreateStatusRequest) -> CreateStatusResponse:
status_id = await self.repository.create(request.entity)
status = await self.repository.get_by_id(status_id)
return CreateStatusResponse(
entity=StatusSchema.model_validate(status),
message="Статус успешно создан",
)
async def update_status(self, status_id: int, request: UpdateStatusRequest):
status = await self.repository.get_by_id(status_id)
if not status:
raise HTTPException(status_code=404, detail="Статус не найден")
await self.repository.update(status, request.entity)
return UpdateBoardResponse(message="Статус успешно обновлен")
async def delete_status(self, status_id: int) -> DeleteStatusResponse:
board = await self.repository.get_by_id(status_id)
if not board:
raise HTTPException(status_code=404, detail="Статус не найден")
deals_count = await self.repository.get_deals_count(status_id)
is_soft_needed: bool = deals_count > 0 is_soft_needed: bool = deals_count > 0
await self.repository.delete(board, is_soft_needed) return is_soft_needed
return DeleteStatusResponse(message="Статус успешно удален")