diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..140a744 --- /dev/null +++ b/constants.py @@ -0,0 +1,3 @@ +allowed_telegram_ids = [ + 454777987, +] diff --git a/models/deal.py b/models/deal.py index 820d730..17753c4 100644 --- a/models/deal.py +++ b/models/deal.py @@ -37,10 +37,11 @@ class Deal(BaseModel): products = relationship('DealProduct', back_populates='deal') # TODO remake with sequence - rank = Column(String, nullable=False, comment='Lexorank') + lexorank = Column(String, nullable=False, comment='Lexorank', index=True) comment = Column(String, nullable=False, server_default='', comment='Коментарий к заданию') + class DealStatusHistory(BaseModel): __tablename__ = 'deals_status_history' id = Column(Integer, autoincrement=True, primary_key=True, index=True) diff --git a/routers/client.py b/routers/client.py index ac43a5b..0f335b0 100644 --- a/routers/client.py +++ b/routers/client.py @@ -11,7 +11,8 @@ from services.client import ClientService client_router = APIRouter( prefix="/client", - tags=['client'] + tags=['client'], + dependencies=[Depends(get_current_user)] ) diff --git a/routers/deal.py b/routers/deal.py index f76aa45..da11d16 100644 --- a/routers/deal.py +++ b/routers/deal.py @@ -11,7 +11,8 @@ from services.deal import DealService deal_router = APIRouter( prefix='/deal', - tags=['deal'] + tags=['deal'], + dependencies=[Depends(get_current_user)] ) @@ -24,6 +25,18 @@ async def create( return await DealService(session).create(request, user) +@deal_router.post( + '/delete', + response_model=DealDeleteResponse, + operation_id='deleteDeal' +) +async def delete( + request: DealDeleteRequest, + session: Annotated[AsyncSession, Depends(get_session)] +): + return await DealService(session).delete(request) + + @deal_router.post('/quickCreate', response_model=DealQuickCreateResponse) async def quick_create( request: DealQuickCreateRequest, @@ -39,7 +52,7 @@ async def change_status( session: Annotated[AsyncSession, Depends(get_session)], user: Annotated[User, Depends(get_current_user)] ): - return await DealService(session).change_status(request, user) + return await DealService(session).change_status_manual(request, user) @deal_router.get('/summaries', @@ -51,9 +64,10 @@ async def get_summary( ): return await DealService(session).get_summary() + @deal_router.post( '/summaries/reorder', - response_model=DealSummaryReorderResponse, + response_model=DealSummaryResponse, operation_id='reorderDealSummaries' ) async def reorder( diff --git a/routers/product.py b/routers/product.py index cdf9508..ea6828e 100644 --- a/routers/product.py +++ b/routers/product.py @@ -7,11 +7,13 @@ import utils.dependecies from backend.session import get_session from schemas.base import PaginationSchema from schemas.product import * +from services.auth import get_current_user from services.product import ProductService product_router = APIRouter( prefix="/product", - tags=["product"] + tags=["product"], + dependencies=[Depends(get_current_user)] ) @@ -98,6 +100,7 @@ async def exists_product_barcode( ): return await ProductService(session).exists_barcode(product_id, barcode) + @product_router.post( '/barcode/generate', response_model=ProductGenerateBarcodeResponse, diff --git a/routers/service.py b/routers/service.py index 64c0823..3a0f8f8 100644 --- a/routers/service.py +++ b/routers/service.py @@ -5,11 +5,13 @@ from sqlalchemy.ext.asyncio import AsyncSession from backend.session import get_session from schemas.service import * +from services.auth import get_current_user from services.service import ServiceService service_router = APIRouter( prefix="/service", - tags=['service'] + tags=['service'], + dependencies=[Depends(get_current_user)] ) diff --git a/schemas/deal.py b/schemas/deal.py index 08b12f1..9850666 100644 --- a/schemas/deal.py +++ b/schemas/deal.py @@ -21,6 +21,7 @@ class DealSummary(CustomModelCamel): name: str client_name: str changed_at: datetime.datetime + deadline: datetime.datetime status: int total_price: int rank: int @@ -41,7 +42,8 @@ class DealStatusHistorySchema(CustomModelCamel): changed_at: datetime.datetime from_status: int to_status: int - next_status_deadline: datetime.datetime + next_status_deadline: datetime.datetime | None + comment: str | None = None class DealSchema(CustomModelCamel): @@ -146,12 +148,16 @@ class DealUpdateGeneralInfoRequest(CustomModelCamel): class DealSummaryReorderRequest(CustomModelCamel): deal_id: int - new_status: int - rank: int - next_status_deadline: datetime.datetime + status: int + index: int + deadline: datetime.datetime comment: str +class DealDeleteRequest(CustomModelCamel): + deal_id: int + + # endregion Requests # region Responses @@ -219,4 +225,8 @@ class DealUpdateGeneralInfoResponse(OkMessageSchema): class DealSummaryReorderResponse(OkMessageSchema): pass + + +class DealDeleteResponse(OkMessageSchema): + pass # endregion Responses diff --git a/services/auth.py b/services/auth.py index aebf5e2..a14e769 100644 --- a/services/auth.py +++ b/services/auth.py @@ -8,6 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from starlette import status import backend.config +import constants from backend.session import get_session from models import User from schemas.auth import * @@ -46,6 +47,9 @@ class AuthService(BaseService): return jwt.encode(payload, backend.config.SECRET_KEY, algorithm=algorithm) async def authenticate(self, request: AuthLoginRequest): + if request.id not in constants.allowed_telegram_ids: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid credentials') + user: Union[User, None] = await self.session.scalar(select(User).where(User.telegram_id == request.id)) if not user: user = User(telegram_id=request.id, diff --git a/services/deal.py b/services/deal.py index db475ee..3b8d824 100644 --- a/services/deal.py +++ b/services/deal.py @@ -21,38 +21,47 @@ class DealService(BaseService): async def _get_deal_by_id(self, deal_id) -> Union[Deal, None]: return await self.session.get(Deal, deal_id) - async def _get_rank(self): - query = await self.session.execute(select(Deal.rank).order_by(Deal.rank.desc())) - result = query.scalar_one_or_none() - if not result: - return str(lexorank.Bucket.BUCEKT_0.next()) - rank = lexorank.parse(result) - return str(rank.next()) + async def _get_rank_for_deal(self, deal_status: DealStatus) -> int: + deal_query = await self.session.execute( + select(Deal).where(Deal.current_status == deal_status).order_by(Deal.lexorank.desc()).limit(1)) + deal = deal_query.scalar_one_or_none() + if not deal: + prev = lexorank.middle(lexorank.Bucket.BUCEKT_0) + return str(prev.next()) + return str(lexorank.parse(deal.lexorank).next()) async def change_status(self, deal: Deal, status: DealStatus, user: User, - deadline: datetime.datetime = None) -> DealStatusHistory: - deadline = deadline - status_change = DealStatusHistory( - deal_id=deal.id, - user_id=user.id, - changed_at=datetime.datetime.now(), - from_status=deal.current_status, - to_status=status, - next_status_deadline=deadline - ) - self.session.add(status_change) + deadline: datetime.datetime = None, + rank=None, + comment: str = '') -> DealStatusHistory: + if not deal.current_status == status: + deadline = deadline + status_change = DealStatusHistory( + deal_id=deal.id, + user_id=user.id, + changed_at=datetime.datetime.now(), + from_status=deal.current_status, + to_status=status, + next_status_deadline=deadline, + comment=comment + ) + self.session.add(status_change) + deal.current_status = status + if not rank: + rank = await self._get_rank_for_deal(status) + if rank: + deal.lexorank = rank await self.session.flush() - return status_change async def create(self, request: DealCreateRequest, user: User) -> DealCreateResponse: - rank = await self._get_rank() + rank = self._get_rank_for_deal(DealStatus.CREATED) deal = Deal( name=request.name, created_at=datetime.datetime.now(), current_status=DealStatus.CREATED, - rank=rank + lexorank=rank ) self.session.add(deal) await self.session.flush() @@ -63,6 +72,14 @@ class DealService(BaseService): await self.session.commit() return DealCreateResponse(ok=True) + async def delete(self, request: DealDeleteRequest) -> DealDeleteResponse: + deal = await self._get_deal_by_id(request.deal_id) + if not deal: + return DealDeleteResponse(ok=False, message="Сделка не найдена") + deal.is_deleted = True + await self.session.commit() + return DealDeleteResponse(ok=True, message="Сделка успешно удалена") + async def quick_create(self, request: DealQuickCreateRequest, user: User) -> DealQuickCreateResponse: client_service = ClientService(self.session) client = await client_service.get_by_name(request.client_name) @@ -72,13 +89,13 @@ class DealService(BaseService): request.client_name, ClientDetailsSchema(address=request.client_address)) await client_service.update_details(user, client, ClientDetailsSchema(address=request.client_address)) - rank = await self._get_rank() + rank = await self._get_rank_for_deal(DealStatus.CREATED) deal = Deal( name=request.name, created_at=datetime.datetime.now(), client_id=client.id, current_status=DealStatus.CREATED, - rank=rank + lexorank=rank ) self.session.add(deal) await self.session.flush() @@ -108,10 +125,11 @@ class DealService(BaseService): q = ( select( Deal, - func.coalesce(service_subquery.c.total_price, 0) - ) - .order_by( - Deal.rank.desc() + func.coalesce(service_subquery.c.total_price, 0), + func.row_number().over( + partition_by=Deal.current_status, + order_by=Deal.lexorank + ).label('rank') ) .options( selectinload(Deal.status_history), @@ -126,18 +144,20 @@ class DealService(BaseService): ) deals_query = await self.session.execute(q) summaries = [] - for deal, total_price in deals_query.all(): + for deal, total_price, rank in deals_query.all(): deal: Deal last_status: DealStatusHistory = max(deal.status_history, key=lambda status: status.changed_at) + deadline = last_status.next_status_deadline summaries.append( DealSummary( id=deal.id, client_name=deal.client.name, name=deal.name, changed_at=last_status.changed_at, + deadline=deadline, status=last_status.to_status, total_price=total_price, - rank=deal.rank + rank=rank ) ) return DealSummaryResponse(summaries=summaries) @@ -190,8 +210,51 @@ class DealService(BaseService): await self.session.rollback() return DealUpdateGeneralInfoResponse(ok=False, message=str(e)) - async def reorder(self, request: DealSummaryReorderRequest, user: User) -> DealSummaryReorderResponse: - pass + async def reorder(self, request: DealSummaryReorderRequest, user: User) -> DealSummaryResponse: + deal: Deal = await self.session.scalar(select(Deal).where(Deal.id == request.deal_id)) + if request.index == 1: + request.index = 0 + is_first = request.index == 0 + stmt = ( + select(Deal) + .where(Deal.current_status == request.status, + Deal.id != request.deal_id) + .order_by(Deal.lexorank) + .offset(max([request.index - 2, 0])) + .limit(2 if not is_first else 1) + + ) + query = await self.session.execute(stmt) + boundaries = query.scalars().all() + top_boundary: Union[Deal, None] = boundaries[0] if not is_first else None + bottom_boundary: Union[Deal, None] = boundaries[1] if len(boundaries) == 2 else None + if top_boundary: + print(top_boundary.name) + if bottom_boundary: + print(bottom_boundary.name) + # working when between two elements + if top_boundary and bottom_boundary: + top_lexorank = lexorank.parse(top_boundary.lexorank) + bottom_lexorank = lexorank.parse(bottom_boundary.lexorank) + new_rank = lexorank.between(top_lexorank, bottom_lexorank) + + # working when at the bottom + elif top_boundary and not bottom_boundary: + new_rank = lexorank.parse(top_boundary.lexorank).next() + # working when at the top + elif bottom_boundary and not top_boundary: + new_rank = lexorank.parse(bottom_boundary.lexorank).prev() + elif not top_boundary and not bottom_boundary and len(boundaries) > 0: + new_rank = lexorank.parse(boundaries[0].lexorank).prev() + else: + new_rank = lexorank.middle(lexorank.Bucket.BUCEKT_0) + + await self.change_status(deal, request.status, user, + deadline=request.deadline, + comment=request.comment, + rank=str(new_rank)) + await self.session.commit() + return await self.get_summary() # endregion diff --git a/test/ockho.py b/test/ockho.py index ead2b70..4fd112b 100644 --- a/test/ockho.py +++ b/test/ockho.py @@ -1,17 +1,16 @@ -import string +from pprint import pprint import lexorank +from lexorank import Bucket, middle - -def main(): - prev = lexorank.middle(lexorank.Bucket.BUCEKT_0) - ranks = [prev] - for _ in range(1_000_000): - ranks.append(prev.next()) - prev = ranks[-1] - print('generated') - sorted_ranks = list(sorted(ranks)) - for idx in range(len(sorted_ranks)): - print(sorted_ranks[idx], ' ', ranks[idx]) -if __name__ == '__main__': - main() +prev = middle(Bucket.BUCEKT_0) +ranks = [prev] +for _ in range(9): + ranks.append(prev.next()) + prev = ranks[-1] +middle_ranks = lexorank.between(ranks[1], ranks[2]) +pprint(ranks) +print('----------') +ranks.append(middle_ranks) +ranks = sorted(ranks) +pprint(ranks)