From 458b6ebfc34f338a336f9a5e19c4d0a1142fb7df Mon Sep 17 00:00:00 2001 From: xds Date: Mon, 9 Feb 2026 16:06:54 +0300 Subject: [PATCH] feat: Implement project management with new models, repositories, and API endpoints, and enhance character management with project association and DTOs. --- __pycache__/main.cpython-313.pyc | Bin 8512 -> 8626 bytes api/__pycache__/dependency.cpython-313.pyc | Bin 2185 -> 2468 bytes api/dependency.py | 7 +- .../__pycache__/assets_router.cpython-313.pyc | Bin 9142 -> 10208 bytes .../__pycache__/auth.cpython-313.pyc | Bin 6114 -> 6133 bytes .../character_router.cpython-313.pyc | Bin 4174 -> 10184 bytes .../generation_router.cpython-313.pyc | Bin 6145 -> 8079 bytes api/endpoints/assets_router.py | 28 ++- api/endpoints/auth.py | 1 + api/endpoints/character_router.py | 139 ++++++++++++++- api/endpoints/generation_router.py | 54 +++++- api/endpoints/project_router.py | 167 ++++++++++++++++++ api/models/CharacterDTO.py | 18 ++ api/models/GenerationRequest.py | 1 + .../GenerationRequest.cpython-313.pyc | Bin 3207 -> 3258 bytes .../generation_service.cpython-313.pyc | Bin 21344 -> 21634 bytes api/service/generation_service.py | 13 +- main.py | 3 + models/Asset.py | 2 + models/Character.py | 10 +- models/Generation.py | 5 +- models/Project.py | 12 ++ models/__pycache__/Asset.cpython-313.pyc | Bin 3203 -> 3305 bytes models/__pycache__/Character.cpython-313.pyc | Bin 866 -> 1043 bytes models/__pycache__/Generation.cpython-313.pyc | Bin 3133 -> 3268 bytes repos/__pycache__/assets_repo.cpython-313.pyc | Bin 14470 -> 14787 bytes repos/__pycache__/char_repo.cpython-313.pyc | Bin 3027 -> 3774 bytes repos/__pycache__/dao.cpython-313.pyc | Bin 1286 -> 1466 bytes .../generation_repo.cpython-313.pyc | Bin 5147 -> 5472 bytes repos/__pycache__/user_repo.cpython-313.pyc | Bin 6208 -> 6606 bytes repos/assets_repo.py | 16 +- repos/char_repo.py | 35 ++-- repos/dao.py | 3 + repos/generation_repo.py | 15 +- repos/project_repo.py | 62 +++++++ repos/user_repo.py | 21 ++- .../__pycache__/char_router.cpython-313.pyc | Bin 10947 -> 11050 bytes .../__pycache__/gen_router.cpython-313.pyc | Bin 28182 -> 28520 bytes routers/char_router.py | 3 +- routers/gen_router.py | 8 +- tests/test_character_crud.py | 101 +++++++++++ tests/test_character_integration.py | 64 +++++++ 42 files changed, 728 insertions(+), 60 deletions(-) create mode 100644 api/endpoints/project_router.py create mode 100644 api/models/CharacterDTO.py create mode 100644 models/Project.py create mode 100644 repos/project_repo.py create mode 100644 tests/test_character_crud.py create mode 100644 tests/test_character_integration.py diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc index 7e90820705a84a094f374edfea083054b714f425..345e0897bdaa6acfc7bb3224c203d69eb2002782 100644 GIT binary patch delta 284 zcmX@$w8@$8GcPX}0}xd1>&#SCn#d=?IAf!FG8g`eXri$;k`Zeod}q z7h_DHJb~SxF=O%rc6lGB`XQd{W#24k4mZTQl5`yw4ACL*;+5|Km zWJ9s+W@*{)Oe}X9TpvufR}h=5sSrEaM}d>=g0Sip2DQm;3M&QG#RC}_E;6tMax!1! HU<8r??_EuM delta 226 zcmdnwe87qCGcPX}0}#wQ){(hcaU!1tW7bCXWVXpk?BbKxvi+KTlHG+dZ89H+ePViy zd$65V1Or2&Is-#6LolOvMo~sgfD(f#NIwuLFa+C6F%)IWF~l$gU4e)plS&LRnF!UU zj1V~mhIGbsMn#60?9J;qGY33 diff --git a/api/__pycache__/dependency.cpython-313.pyc b/api/__pycache__/dependency.cpython-313.pyc index ce1b3df8928aa8be4f590010f3d592d3fc129b28..bc2abebebe41ac5692b6250dd6d44bbc7c33707d 100644 GIT binary patch delta 380 zcmeAaTq4Z(nU|M~0SNZ)>&(<-pU5Y{*tSvKm(ebmAy~|t!ArbIT!A4*T!|rAJXj)F z(vr!9F<2@_AVz`}s3({U#xh|EmX>13V##7Gl1vxZl-XRtD8gE=$#{#+BQ-H4waBlE zCqg%%C_gJTxkT5~1t`Iqn3I`Ue2b?dz5pT|pP7=(2yzJ&FahaiApXn;B$OEx7^X6W zGN>?^Gp8{Y2?B*yGH5auX#p9krLAY diff --git a/api/dependency.py b/api/dependency.py index c0b4937..7dc90eb 100644 --- a/api/dependency.py +++ b/api/dependency.py @@ -43,4 +43,9 @@ def get_generation_service( s3_adapter: S3Adapter = Depends(get_s3_adapter), bot: Bot = Depends(get_bot_client), ) -> GenerationService: - return GenerationService(dao, gemini, s3_adapter, bot) \ No newline at end of file + return GenerationService(dao, gemini, s3_adapter, bot) + +from fastapi import Header + +async def get_project_id(x_project_id: Optional[str] = Header(None, alias="X-Project-ID")) -> Optional[str]: + return x_project_id \ No newline at end of file diff --git a/api/endpoints/__pycache__/assets_router.cpython-313.pyc b/api/endpoints/__pycache__/assets_router.cpython-313.pyc index d5936dff2af907661bdd3da21bc584e4ec0384bb..1f6c07e729e621b5a11767628bf1ded2abd69419 100644 GIT binary patch delta 4017 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 delta 2751 zcmb7GU2Gf25x%`U9w~|xCH_m4#UGI(DeKQNWrwO2z>;OLc5Ej+(Uq%98CpEaR47ut zJHc>}BC2aNerjVZkh(yD76=fe4@H{_2m%BtY$XrwTUw9U+Mw{sLC%!fub`@ z(TN2VMF+a!?CkEh-|p

d}Rd=8}6(r=0_tfA41HY7fW#6&o$-*f;jB-%399DpHJ- zU2^9rk-M_ImQqrAnZ6S8%(*8>bD-=|>@iM>wAvY7_9}z2PjSnB<**!3`sASE&DiDc z9&S}!B@_Gj+7MtZB9_IibmSHKd(wZjCo3F4BMc2tGNK$hfH2x(Q|e{2k}VFv_E468 z9!Yk}eKH3q$P8J96f=VFckYfi{IRdMw$kW=d1cB)?GHr`zMa2eZ zlc0a*<3kxi9#E`u@);x&mgLj{s7wp$Wcq&-&Jd&t$2oWaW#Spi;r|0==m5&G6=!;Q z>)(QpY$Yte;K%#mBs3SOv`yknyPq!*`u%81 zTIer`69Rq59S|(c<)fm*Lx1899k+P6i&mW<<#gV|mth$gv~opKQA$^V9mSe zE6(9z8;tF0^)CEa7P5StvvKE_;G5C;2w>K%%iM8cnWRPfTW30!=CvW1V741|qf{*} z)i-KJx=q7V(vG4UqOW$$Ew(jdfXAf0fGUe>k_|w00+dNgri+E9sHysTy{4EHqd@>EBz*S|GANIoUgQSlE^uyhKL1wQe?sYOXp*O%R3<~J@j37mmm}b8kGWGQEro^EUyyT zF1Mexp0U-$2>p8~4ApI6W`f5nVYe&>_F-u)8aOdUQv1y^52wk=j>TYW**p}@gR+QM znn}Ko?pi!9QViEetmJUH)oy}fd0){5ydN6uek zdJfX$F-wjvde8Sg5~oRjlD_I68IVk|R$5cp-8UUe^_rp9j3O2QZ5AkM$X4s4pZJfy zeg;jJBWKaVZqvS`sU@f^#gbtP8(KBp`*ntyHoRO6olS7E3A$0z4E<`wxMGShw`_K8 z7?r9%Qdujls6}RLIl2~j;Wd`Nj^+YImVTz78=7_wS9haAMyB;jNiP}TXVhLoKVyC# ztqY)_*q(akWr@x1bw9b8St5{_o(xJMbz$<~F$}4q6W0 zq(j}Mwq2*yDQ(U+-1PnKk6${>ISyg^o+i6~t~2vgM`qWjgSimd?jbpkxE;!X{h5p8 z{NiVwC&SSAe3az6#m~oP;?TI~C#btYZ*BK{SMbgO!1>DGAT8(9!hVUOV|{NH@Mbp4 znT%Er)jTRZJXVWL0b-!B0Qk^{TWChLfEGSJxY061aet%dCI84j4K;!mun@6sJ04yH@2>ZAM&TarpbcZxmH<$7IuMVMLHg}@Z;vTy^$kPSOko9H z&r2}W6ff87Rc)Bwh(|BtJ7)@&n$bKDalNh^W@m1G{^jEM*pcE)e)i=2>?sys#+fbl zYi&E3+D-7`KV`Z>N#ZQMkod=24sw>8N-ZwP&nr%SX$w?%izzc@@-3b`ZcQMko#6%tceDFsKi(FVP$0jU z5lAQ$sesh+7NsVp#Fql~7suzO7KH=FTsME?-NVQjF?ln;GHV=Az<2X){sYX6@ssBW z2e4)U1)?{96jo-^2PwYAQjlL~1d=Lp1QDJf0&G5qO>TZlX-=wLQQ2f?v6DQk TjM5V|h>85obEHCge2b+Zzqq6bq^u}? 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 LjeDYZKtcWoROpG! delta 2169 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 KM{Ohv0Q+w>suVN; delta 2377 zcmZ`(OKclO7~a`kKjL*B>qqU_PU1(B#%sXk%!5CVZHCt%YHJg-Ny9E6T=4+^wE-SA&eiNS0JfQp94?fgMRF zb|ziem2_jbv1d)~NP4ix&>K?TWGikpbX&@o^kcuF3#kAO$bqS$>Lpuan33BLpz<7d z(7WuZU^0Y5K}P1ojMUL6HT5C6ftrP?W_?I%j&nFHho>gUNtbi1MRrMIoHeYPt5z{2 zIb^rwlm*EpdnC8)idm%{KISri86BwHk2|XvSx*JSLN67n7_ynpytNsy*uv+eR+*K% zOcYD<$w)RC+SXnEy1TmRU6a5!6_B04R}!Q)iK%1FOYJq(4!K7P(oQic_fQ>Pw94HS zmPJV6ppsPGy-DGCUw=J?5?sJ?go_SL~*CwRf(V&GfO>n=l4|4 z$D{ufCsbt-7?LUsDgK^1{C!y=+DqQ$ z&m%-0@Lu9JeS@5Le>8QoC`g`Iq7GX9Oq9nyI$%GD0NG{D+qysn(MrCw{(^#cuQpsm zjt;sIwc-$LhCxJ#C>$tbSa1+ldO!roSs}c)o2sgXieS0nomAJwMZ7S-SV&)_LpnD< zlT~m(T@RCG;b+e*Pt|I(Qsq6`H?ELFu3qxk?&VK&QzoJ{y3D6piH~z+%;UE32f2$V zY9jWgv5Qd_LuundDx%yLQN1~<6w+BGui%+NZXvHymt$a9v&I2BRpELdYf!~6fCY#F z*qfl@cjg+iX(nfzBiVH^vL@~?iu+fNYiDFl+^>n#Yr?d~PZx#hXDa+6HwK2$wA?&UOt!4&TePSQ8a_`C{-d%kolB3jiwN}Wpypj%H@epL=pMI zDO&HPmk)kkCA?RvDpCLd0xv33B zSzS;IGleBJJ-d)mu$O%0{p%_X3c%^+j8d4%&C#N-JzZVQD050d+4d;(#$%^Wy>jI3 zS!EI42#t3wqPnGQol(K%+5<_^2?lY3cJ(L~K>9b{b~AN7r8$VjcLMd3Gro7opS})v zBUm?qXa)g44;D$#-x-Da(8YO5d`{gvQG2D>3F{5iFHETx6zn4N{xAxWPyF6-H|&}5 z!V+EA`NajbVEk7)M-}5o!H2dXL+P2N*pXa?AcQ?Odh_R-ZT`zh?iK{ zEHyE#^``H-PqVjceTi#64UIoa9MfJ(uO==4uW_`@0flk2-aA?{7S@%wfXg@noEJwC zuo AssetsResponse: +async def get_assets(request: Request, dao: DAO = Depends(get_dao), type: Optional[str] = None, limit: int = 10, offset: int = 0, current_user: dict = Depends(get_current_user), project_id: Optional[str] = Depends(get_project_id)) -> AssetsResponse: logger.info(f"get_assets called. Limit: {limit}, Offset: {offset}") - assets = await dao.assets.get_assets(type, limit, offset) + + user_id_filter = str(current_user["_id"]) + if project_id: + project = await dao.projects.get_project(project_id) + if not project or str(current_user["_id"]) not in project.members: + raise HTTPException(status_code=403, detail="Project access denied") + user_id_filter = None + + assets = await dao.assets.get_assets(type, limit, offset, created_by=user_id_filter, project_id=project_id) # assets = await dao.assets.get_assets() # This line seemed redundant/conflicting in original code - total_count = await dao.assets.get_asset_count() + total_count = await dao.assets.get_asset_count(created_by=user_id_filter, project_id=project_id) # Manually map to DTO to trigger computed fields validation if necessary, # but primarily to ensure valid Pydantic models for the response list. @@ -84,11 +93,13 @@ async def get_assets(request: Request, dao: DAO = Depends(get_dao), type: Option -@router.post("/upload", response_model=AssetResponse, status_code=status.HTTP_201_CREATED, dependencies=[Depends(get_current_user)]) +@router.post("/upload", response_model=AssetResponse, status_code=status.HTTP_201_CREATED) async def upload_asset( file: UploadFile = File(...), linked_char_id: Optional[str] = Form(None), dao: DAO = Depends(get_dao), + current_user: dict = Depends(get_current_user), + project_id: Optional[str] = Depends(get_project_id) ): logger.info(f"upload_asset called. Filename: {file.filename}, ContentType: {file.content_type}, LinkedCharId: {linked_char_id}") if not file.content_type: @@ -96,6 +107,11 @@ async def upload_asset( if not file.content_type.startswith("image/"): raise HTTPException(status_code=400, detail=f"Unsupported content type: {file.content_type}") + + if project_id: + project = await dao.projects.get_project(project_id) + if not project or str(current_user["_id"]) not in project.members: + raise HTTPException(status_code=403, detail="Project access denied") data = await file.read() if not data: @@ -111,7 +127,9 @@ async def upload_asset( content_type=AssetContentType.IMAGE, linked_char_id=linked_char_id, data=data, - thumbnail=thumbnail_bytes + thumbnail=thumbnail_bytes, + created_by=str(current_user["_id"]), + project_id=project_id, ) asset_id = await dao.assets.create_asset(asset) diff --git a/api/endpoints/auth.py b/api/endpoints/auth.py index b1b81b0..7cba1d1 100644 --- a/api/endpoints/auth.py +++ b/api/endpoints/auth.py @@ -59,6 +59,7 @@ class Token(BaseModel): class UserResponse(BaseModel): + id: str username: str full_name: str | None = None status: str diff --git a/api/endpoints/character_router.py b/api/endpoints/character_router.py index eb7b54e..da7f101 100644 --- a/api/endpoints/character_router.py +++ b/api/endpoints/character_router.py @@ -1,4 +1,4 @@ -from typing import List, Any, Coroutine +from typing import List, Any, Coroutine, Optional from fastapi import APIRouter, Depends from pydantic import BaseModel @@ -9,6 +9,7 @@ from api.models.AssetDTO import AssetsResponse, AssetResponse from api.models.GenerationRequest import GenerationRequest, GenerationResponse from models.Asset import Asset from models.Character import Character +from api.models.CharacterDTO import CharacterCreateRequest, CharacterUpdateRequest from repos.dao import DAO from api.dependency import get_dao @@ -17,25 +18,49 @@ import logging logger = logging.getLogger(__name__) from api.endpoints.auth import get_current_user +from api.dependency import get_project_id router = APIRouter(prefix="/api/characters", tags=["Characters"], dependencies=[Depends(get_current_user)]) @router.get("/", response_model=List[Character]) -async def get_characters(request: Request, dao: DAO = Depends(get_dao), ) -> List[Character]: +async def get_characters(request: Request, dao: DAO = Depends(get_dao), current_user: dict = Depends(get_current_user), project_id: Optional[str] = Depends(get_project_id)) -> List[Character]: logger.info("get_characters called") - characters = await dao.chars.get_all_characters() + + user_id_filter = str(current_user["_id"]) + if project_id: + project = await dao.projects.get_project(project_id) + if not project or str(current_user["_id"]) not in project.members: + raise HTTPException(status_code=403, detail="Project access denied") + user_id_filter = None + + characters = await dao.chars.get_all_characters(created_by=user_id_filter, project_id=project_id) return characters @router.get("/{character_id}/assets", response_model=AssetsResponse) async def get_character_assets(character_id: str, dao: DAO = Depends(get_dao), limit: int = 10, - offset: int = 0, ) -> AssetsResponse: + offset: int = 0, current_user: dict = Depends(get_current_user)) -> AssetsResponse: logger.info(f"get_character_assets called. CharacterID: {character_id}, Limit: {limit}, Offset: {offset}") character = await dao.chars.get_character(character_id) if character is None: raise HTTPException(status_code=404, detail="Character not found") + + # Access Check + is_creator = character.created_by == str(current_user["_id"]) + is_project_member = False + if character.project_id and character.project_id in current_user.get("project_ids", []): + is_project_member = True + + if not is_creator and not is_project_member: + raise HTTPException(status_code=403, detail="Access denied") + assets = await dao.assets.get_assets_by_char_id(character_id, limit, offset) + # Filter assets by user ownership as well? + # Usually if you own character, you see its assets. + # But assets also have specific created_by. + # Let's assume if you own character you can see its assets. + total_count = await dao.assets.get_asset_count(character_id) asset_responses = [AssetResponse.model_validate(a.model_dump()) for a in assets] @@ -43,12 +68,116 @@ async def get_character_assets(character_id: str, dao: DAO = Depends(get_dao), l @router.get("/{character_id}", response_model=Character) -async def get_character_by_id(character_id: str, request: Request, dao: DAO = Depends(get_dao)) -> Character: +async def get_character_by_id(character_id: str, request: Request, dao: DAO = Depends(get_dao), current_user: dict = Depends(get_current_user)) -> Character: logger.debug(f"get_character_by_id called. ID: {character_id}") character = await dao.chars.get_character(character_id) + + if not character: + raise HTTPException(status_code=404, detail="Character not found") + + if character: + is_creator = character.created_by == str(current_user["_id"]) + is_project_member = False + if character.project_id and character.project_id in current_user.get("project_ids", []): + is_project_member = True + + if not is_creator and not is_project_member: + raise HTTPException(status_code=403, detail="Access denied") + return character +@router.post("/", response_model=Character) +async def create_character( + char_req: CharacterCreateRequest, + dao: DAO = Depends(get_dao), + current_user: dict = Depends(get_current_user) +) -> Character: + logger.info("create_character called") + + char_data = char_req.model_dump() + char_data["created_by"] = str(current_user["_id"]) + if "id" not in char_data: + char_data["id"] = None + + if char_req.project_id: + project = await dao.projects.get_project(char_req.project_id) + if not project or str(current_user["_id"]) not in project.members: + raise HTTPException(status_code=403, detail="Project access denied") + + new_char = Character(**char_data) + created_char = await dao.chars.add_character(new_char) + return created_char + + +@router.put("/{character_id}", response_model=Character) +async def update_character( + character_id: str, + char_update: CharacterUpdateRequest, + dao: DAO = Depends(get_dao), + current_user: dict = Depends(get_current_user) +) -> Character: + logger.info(f"update_character called. ID: {character_id}") + + existing_char = await dao.chars.get_character(character_id) + if not existing_char: + raise HTTPException(status_code=404, detail="Character not found") + + is_creator = existing_char.created_by == str(current_user["_id"]) + is_project_member = False + if existing_char.project_id and existing_char.project_id in current_user.get("project_ids", []): + is_project_member = True + + if not is_creator and not is_project_member: + raise HTTPException(status_code=403, detail="Access denied") + + update_data = char_update.model_dump(exclude_unset=True) + + if "project_id" in update_data and update_data["project_id"]: + new_project_id = update_data["project_id"] + project = await dao.projects.get_project(new_project_id) + if not project or str(current_user["_id"]) not in project.members: + raise HTTPException(status_code=403, detail="Target project access denied") + + updated_char_data = existing_char.model_dump() + updated_char_data.update(update_data) + + updated_char = Character(**updated_char_data) + + success = await dao.chars.update_char(character_id, updated_char) + if not success: + raise HTTPException(status_code=500, detail="Failed to update character") + + return updated_char + + +@router.delete("/{character_id}", status_code=204) +async def delete_character( + character_id: str, + dao: DAO = Depends(get_dao), + current_user: dict = Depends(get_current_user) +): + logger.info(f"delete_character called. ID: {character_id}") + + existing_char = await dao.chars.get_character(character_id) + if not existing_char: + raise HTTPException(status_code=404, detail="Character not found") + + is_creator = existing_char.created_by == str(current_user["_id"]) + is_project_member = False + if existing_char.project_id and existing_char.project_id in current_user.get("project_ids", []): + is_project_member = True + + if not is_creator and not is_project_member: + raise HTTPException(status_code=403, detail="Access denied") + + success = await dao.chars.delete_character(character_id) + if not success: + raise HTTPException(status_code=500, detail="Failed to delete character") + + return + + @router.post("/{character_id}/_run", response_model=GenerationResponse) async def post_character_generation(character_id: str, generation: GenerationRequest, request: Request) -> GenerationResponse: diff --git a/api/endpoints/generation_router.py b/api/endpoints/generation_router.py index cfdf4e8..1352c92 100644 --- a/api/endpoints/generation_router.py +++ b/api/endpoints/generation_router.py @@ -5,7 +5,8 @@ from fastapi.params import Depends from starlette.requests import Request from api import service -from api.dependency import get_generation_service +from api.dependency import get_generation_service, get_project_id, get_dao +from repos.dao import DAO from api.models.GenerationRequest import GenerationResponse, GenerationRequest, GenerationsResponse, PromptResponse, PromptRequest from api.service.generation_service import GenerationService @@ -49,30 +50,65 @@ async def prompt_from_image( @router.get("", response_model=GenerationsResponse) async def get_generations(character_id: Optional[str] = None, limit: int = 10, offset: int = 0, - generation_service: GenerationService = Depends(get_generation_service)): + generation_service: GenerationService = Depends(get_generation_service), + current_user: dict = Depends(get_current_user), + project_id: Optional[str] = Depends(get_project_id), + dao: DAO = Depends(get_dao)): logger.info(f"get_generations called. CharacterId: {character_id}, Limit: {limit}, Offset: {offset}") - return await generation_service.get_generations(character_id, limit=limit, offset=offset) + + user_id_filter = str(current_user["_id"]) + if project_id: + project = await dao.projects.get_project(project_id) + if not project or str(current_user["_id"]) not in project.members: + raise HTTPException(status_code=403, detail="Project access denied") + user_id_filter = None # Show all project generations + + return await generation_service.get_generations(character_id, limit=limit, offset=offset, user_id=user_id_filter, project_id=project_id) @router.post("/_run", response_model=GenerationResponse) async def post_generation(generation: GenerationRequest, request: Request, generation_service: GenerationService = Depends(get_generation_service), - current_user: dict = Depends(get_current_user)) -> GenerationResponse: + current_user: dict = Depends(get_current_user), + project_id: Optional[str] = Depends(get_project_id), + dao: DAO = Depends(get_dao)) -> GenerationResponse: logger.info(f"post_generation (run) called. LinkedCharId: {generation.linked_character_id}, PromptLength: {len(generation.prompt)}") - return await generation_service.create_generation_task(generation, user_id=current_user.get("username")) + + if project_id: + project = await dao.projects.get_project(project_id) + if not project or str(current_user["_id"]) not in project.members: + raise HTTPException(status_code=403, detail="Project access denied") + generation.project_id = project_id + + return await generation_service.create_generation_task(generation, user_id=str(current_user.get("_id"))) @router.get("/{generation_id}", response_model=GenerationResponse) async def get_generation(generation_id: str, - generation_service: GenerationService = Depends(get_generation_service)) -> GenerationResponse: + generation_service: GenerationService = Depends(get_generation_service), + current_user: dict = Depends(get_current_user)) -> GenerationResponse: logger.debug(f"get_generation called for ID: {generation_id}") - return await generation_service.get_generation(generation_id) + gen = await generation_service.get_generation(generation_id) + if gen and gen.created_by != str(current_user["_id"]): + raise HTTPException(status_code=403, detail="Access denied") + return gen @router.get("/running") async def get_running_generations(request: Request, - generation_service: GenerationService = Depends(get_generation_service)): - return await generation_service.get_running_generations() + generation_service: GenerationService = Depends(get_generation_service), + current_user: dict = Depends(get_current_user), + project_id: Optional[str] = Depends(get_project_id), + dao: DAO = Depends(get_dao)): + + user_id_filter = str(current_user["_id"]) + if project_id: + project = await dao.projects.get_project(project_id) + if not project or str(current_user["_id"]) not in project.members: + raise HTTPException(status_code=403, detail="Project access denied") + user_id_filter = None + + return await generation_service.get_running_generations(user_id=user_id_filter, project_id=project_id) @router.delete("/{generation_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_current_user)]) diff --git a/api/endpoints/project_router.py b/api/endpoints/project_router.py new file mode 100644 index 0000000..93c2d0f --- /dev/null +++ b/api/endpoints/project_router.py @@ -0,0 +1,167 @@ +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, status +from pydantic import BaseModel +from api.dependency import get_dao +from api.endpoints.auth import get_current_user +from models.Project import Project +from repos.dao import DAO + +router = APIRouter(prefix="/api/projects", tags=["Projects"]) + +class ProjectCreate(BaseModel): + name: str + description: Optional[str] = None + +class ProjectResponse(BaseModel): + id: str + name: str + description: Optional[str] = None + owner_id: str + members: List[str] + is_owner: bool = False + +@router.post("", response_model=ProjectResponse) +async def create_project( + project_data: ProjectCreate, + dao: DAO = Depends(get_dao), + current_user: dict = Depends(get_current_user) +): + user_id = str(current_user["_id"]) + new_project = Project( + name=project_data.name, + description=project_data.description, + owner_id=user_id, + members=[user_id] + ) + project_id = await dao.projects.create_project(new_project) + + # Add project to user's project list + # Assuming user_repo has a method to add project or we do it directly? + # UserRepo doesn't have add_project method yet. + # But since UserRepo is just a wrapper around collection, lets add it here or update UserRepo later? + # Better to update UserRepo. For now, let's just return success. + # But user needs to see it in list. + # Update user in DB + await dao.users.collection.update_one( + {"_id": current_user["_id"]}, + {"$addToSet": {"project_ids": project_id}} + ) + + return ProjectResponse( + id=project_id, + name=new_project.name, + description=new_project.description, + owner_id=new_project.owner_id, + members=new_project.members, + is_owner=True + ) + +@router.get("", response_model=List[ProjectResponse]) +async def get_my_projects( + dao: DAO = Depends(get_dao), + current_user: dict = Depends(get_current_user) +): + user_id = str(current_user["_id"]) + projects = await dao.projects.get_projects_by_user(user_id) + + responses = [] + for p in projects: + responses.append(ProjectResponse( + id=p.id, + name=p.name, + description=p.description, + owner_id=p.owner_id, + members=p.members, + is_owner=(p.owner_id == user_id) + )) + return responses + +class MemberAdd(BaseModel): + username: str + +@router.post("/{project_id}/members", dependencies=[Depends(get_current_user)]) +async def add_member( + project_id: str, + member_data: MemberAdd, + dao: DAO = Depends(get_dao), + current_user: dict = Depends(get_current_user) +): + user_id = str(current_user["_id"]) + project = await dao.projects.get_project(project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + if project.owner_id != user_id: + raise HTTPException(status_code=403, detail="Only owner can add members") + + target_user = await dao.users.get_user_by_username(member_data.username) + if not target_user: + raise HTTPException(status_code=404, detail="User not found") + + target_user_id = str(target_user["_id"]) + + if target_user_id in project.members: + return {"message": "User already in project"} + + await dao.projects.add_member(project_id, target_user_id) + + # Update target user's project list + await dao.users.collection.update_one( + {"_id": target_user["_id"]}, + {"$addToSet": {"project_ids": project_id}} + ) + + return {"message": "Member added"} + +@router.post("/{project_id}/join", dependencies=[Depends(get_current_user)]) +async def join_project( + project_id: str, + dao: DAO = Depends(get_dao), + current_user: dict = Depends(get_current_user) +): + # Retrieve project to verify it exists + project = await dao.projects.get_project(project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + user_id = str(current_user["_id"]) + + # Check if user is ALREADY in project + if user_id in project.members: + return {"message": "Already a member"} + + # Add member + await dao.projects.add_member(project_id, user_id) + + # Update user's project list + await dao.users.collection.update_one( + {"_id": current_user["_id"]}, + {"$addToSet": {"project_ids": project_id}} + ) + + return {"message": "Joined project"} + + +@router.delete("/{project_id}", dependencies=[Depends(get_current_user)] ) +async def delete_project( + project_id: str, + dao: DAO = Depends(get_dao), + current_user: dict = Depends(get_current_user) +): + user_id = str(current_user["_id"]) + project = await dao.projects.get_project(project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + if project.owner_id != user_id: + raise HTTPException(status_code=403, detail="Only owner can delete project") + + await dao.projects.delete_project(project_id) + + # Remove project from user's project list + await dao.users.collection.update_one( + {"_id": current_user["_id"]}, + {"$pull": {"project_ids": project_id}} + ) + + return {"message": "Project deleted"} \ No newline at end of file diff --git a/api/models/CharacterDTO.py b/api/models/CharacterDTO.py new file mode 100644 index 0000000..ca73053 --- /dev/null +++ b/api/models/CharacterDTO.py @@ -0,0 +1,18 @@ +from typing import Optional +from pydantic import BaseModel + +class CharacterCreateRequest(BaseModel): + name: str + character_bio: str + character_image_doc_tg_id: Optional[str] = None + avatar_image: Optional[str] = None + character_image_tg_id: Optional[str] = None + project_id: Optional[str] = None + +class CharacterUpdateRequest(BaseModel): + name: Optional[str] = None + character_bio: Optional[str] = None + character_image_doc_tg_id: Optional[str] = None + avatar_image: Optional[str] = None + character_image_tg_id: Optional[str] = None + project_id: Optional[str] = None diff --git a/api/models/GenerationRequest.py b/api/models/GenerationRequest.py index 2ad06cd..40e9d18 100644 --- a/api/models/GenerationRequest.py +++ b/api/models/GenerationRequest.py @@ -16,6 +16,7 @@ class GenerationRequest(BaseModel): telegram_id: Optional[int] = None use_profile_image: bool = True assets_list: List[str] + project_id: Optional[str] = None class GenerationsResponse(BaseModel): diff --git a/api/models/__pycache__/GenerationRequest.cpython-313.pyc b/api/models/__pycache__/GenerationRequest.cpython-313.pyc index 8dbd694a79d18f4138b8fb483d20eb0880a9e41c..0fe1b17356e5451541685ef016e8fb200e61f144 100644 GIT binary patch delta 818 zcmaiy&ubGw6vuay&1RGAYC|@$X*5JksoR3qYK=dlSj3A;E){wSE-_idwlOKQQ$##O zFP2_J8IXeifSyEqQm}`DcT1s!{SQ3bqoD6Q@lptM51*OYH}k&leP`D5-}2UP%StNr zefhEBS3X;Ts&0#y>W4yvW$LlI9-~k_zL=V#anWRUJK|VkHu8KdOs=xtve)FU-}={7iL{-ZVS1+L4+#lvV*^~6qeDYK_d3`*q}ci$MBnYY%y zR%T^U)Rxtp*wE%W&>}fNDw7sv{!vj-dH<91E75l2{4sHBZ5P$VQeI#^BBlS;FXsDmSY!=XgF#uMw z6mS8M#m-@N5x7JMV~;z%250m5aT$<(R4}^&TvZg&H{#XnSVRe7wA*{iG%TyY4PaE? zXn+BQt?$M$RhN!9)L^qM)iM+ct&ZPa@mg*p2s|E0>uOHoh*obSvutLR?gFB~ym+51 z4rLeMs~99J{cDIq`oC8E2(3{HcW@j)vLH^N8#Rx=x;MN;L~4#L-I7LyI)Aq6ciU96 yc%5pUlP^NrBWsoE4nLaEs6Oen);iuzc3+gtv!9* delta 703 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 diff --git a/api/service/generation_service.py b/api/service/generation_service.py index b157c64..1cc962f 100644 --- a/api/service/generation_service.py +++ b/api/service/generation_service.py @@ -92,10 +92,10 @@ class GenerationService: return await asyncio.to_thread(self.gemini.generate_text, prompt=technical_prompt, images_list=images) - async def get_generations(self, character_id: Optional[str] = None, limit: int = 10, offset: int = 0) -> List[ + async def get_generations(self, character_id: Optional[str] = None, limit: int = 10, offset: int = 0, user_id: Optional[str] = None, project_id: Optional[str] = None) -> List[ Generation]: - generations = await self.dao.generations.get_generations(character_id = character_id,limit=limit, offset=offset) - total_count = await self.dao.generations.count_generations(character_id = character_id) + generations = await self.dao.generations.get_generations(character_id = character_id,limit=limit, offset=offset, created_by=user_id, project_id=project_id) + total_count = await self.dao.generations.count_generations(character_id = character_id, created_by=user_id, project_id=project_id) generations = [GenerationResponse(**gen.model_dump()) for gen in generations] return GenerationsResponse(generations=generations, total_count=total_count) @@ -106,8 +106,8 @@ class GenerationService: else: return GenerationResponse(**gen.model_dump()) - async def get_running_generations(self) -> List[Generation]: - return await self.dao.generations.get_generations(status=GenerationStatus.RUNNING) + async def get_running_generations(self, user_id: Optional[str] = None, project_id: Optional[str] = None) -> List[Generation]: + return await self.dao.generations.get_generations(status=GenerationStatus.RUNNING, created_by=user_id, project_id=project_id) async def create_generation_task(self, generation_request: GenerationRequest, user_id: Optional[str] = None) -> GenerationResponse: gen_id = None @@ -261,7 +261,8 @@ class GenerationService: data=None, # Not storing bytes in DB anymore minio_object_name=filename, minio_bucket=self.s3_adapter.bucket_name, - thumbnail=thumbnail_bytes + thumbnail=thumbnail_bytes, + created_by=generation.created_by ) # Сохраняем в БД diff --git a/main.py b/main.py index d97ebc5..9f70ffa 100644 --- a/main.py +++ b/main.py @@ -180,6 +180,8 @@ app.add_middleware( # Подключаем роутер API from api.endpoints.auth import router as auth_api_router from api.endpoints.admin import router as admin_api_router +from api.endpoints.project_router import router as project_api_router + app.include_router(auth_api_router) app.include_router(admin_api_router) app.include_router(api_assets_router) @@ -188,6 +190,7 @@ app.include_router(api_gen_router) app.include_router(api_album_router) app.include_router(api_admin_router) app.include_router(api_auth_router) +app.include_router(project_api_router) # --- ХЕНДЛЕРЫ БОТА (Main Router) --- diff --git a/models/Asset.py b/models/Asset.py index 81a34bd..ff4eeef 100644 --- a/models/Asset.py +++ b/models/Asset.py @@ -28,6 +28,8 @@ class Asset(BaseModel): minio_thumbnail_object_name: Optional[str] = None thumbnail: Optional[bytes] = None tags: List[str] = [] + created_by: Optional[str] = None + project_id: Optional[str] = None created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) diff --git a/models/Character.py b/models/Character.py index f112f91..80ef559 100644 --- a/models/Character.py +++ b/models/Character.py @@ -5,11 +5,13 @@ from pydantic_core.core_schema import computed_field class Character(BaseModel): - id: str | None + id: Optional[str] = None name: str avatar_image: Optional[str] = None character_image_data: Optional[bytes] = None - character_image_doc_tg_id: str - character_image_tg_id: str | None - character_bio: str + character_image_doc_tg_id: Optional[str] = None + character_image_tg_id: Optional[str] = None + character_bio: Optional[str] = None + created_by: Optional[str] = None + project_id: Optional[str] = None diff --git a/models/Generation.py b/models/Generation.py index 784d513..6c74100 100644 --- a/models/Generation.py +++ b/models/Generation.py @@ -34,8 +34,9 @@ class Generation(BaseModel): input_token_usage: Optional[int] = None output_token_usage: Optional[int] = None is_deleted: bool = False - album_id: Optional[str] = None - created_by: Optional[str] = None + album_id: Optional[str] = None + created_by: Optional[str] = None # Stores User ID (Telegram ID or Web User ObjectId) + project_id: Optional[str] = None created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) diff --git a/models/Project.py b/models/Project.py new file mode 100644 index 0000000..65bbb59 --- /dev/null +++ b/models/Project.py @@ -0,0 +1,12 @@ +from datetime import datetime +from typing import List, Optional +from pydantic import BaseModel, Field + +class Project(BaseModel): + id: Optional[str] = None + name: str + description: Optional[str] = None + owner_id: str + members: List[str] = [] # List of User IDs + is_deleted: bool = False + created_at: datetime = Field(default_factory=datetime.now) diff --git a/models/__pycache__/Asset.cpython-313.pyc b/models/__pycache__/Asset.cpython-313.pyc index f1873efe36bac9fc638283d8cfc3d72106f31559..13bfb7f10f5de5baf05fa550ec4cd736ece6ebca 100644 GIT binary patch delta 415 zcmZpcd@0HMnU|M~0SI*Wb!HZ9;h zG+_yrl48hWF$J0j0zew1l!2j0DMlHnfX##{SQ=SDkxH;kk!m`VrtIWyTbG6z$TV{hAp|0%K!h}iumllSKtc(` zu-<%u&7P4_WwQWB93!K{BJLE;UB$$=zJLWNs*^G7+-sKfIs1CRo{ F1^`sdKl%Uw diff --git a/models/__pycache__/Character.cpython-313.pyc b/models/__pycache__/Character.cpython-313.pyc index a89257adc29836d861e7d1dd736d6ddfb0048da3..4c1ed63b1b3ee1de69182e97ab339522f8756da2 100644 GIT binary patch delta 497 zcmaFFHkpI>GcPX}0}y20>dbsOk#{lUfr%T1>w}q!SYlXX*jRxwG3>$2>L8j8$YKGp zfU+D&vaC=sPM{bUR0kVWj2lTEJ5-DZNsI$3#)~AzscsChgbzuO3#yPGNsJpT##1Da z&ZNmZ*@&@0<`!3SQEFmIYD#=kKE6n3 zvH?@LNEJ{;JHrhLnFgm0jtRn@yq8&ICQoBh_S84Nz#==NVg>We+RH4)H?;LHut?6Z zSl~R<_A-n94Q2HP-w75ooF@W#>NgZr8oVZO%n+Uk)UATVyT+nYBt7{8ldr1~$TiX+ zLI&g{4x8Nkl+v73yCMZ3ml24IrGUf-W=2NFyA0A#8RYIVn0#U5WK^BN`IP}kf%O6a DIEZr! delta 293 zcmbQt@raH0GcPX}0}x#L)0mk#k#{j;&BTqu^)W2LOuFn03^A<1%<4ed7#1Ll1;h$w zDq@Rak6~d2iLwT>Ld7_MVw_MhHn$Vltf^$Rs1c3=}Tn0TQ=ZlPXJ6iziQHl3)~=yoxED-x(;{ z&TxZ+yCbEOtJ!a|D6_J-ywYVBr5n<63q)pGU1m|eAuM){MXX42awD^^lmN&mNf03g hGLOS1H$SB`C)KV#z>L6M~ zi6K}{iXoWKlCelNMhz$~4;NQ}imQXg6~W>f!AdbQCQQM~QVdxvrXY)ephz=VrARBC zNmF(63YIuVo?Bc6Mfq8&$tCfbDU?l!v>_>7DR}G2sscT4ZTd*53-kF@uZs+L7z!1t1#0C^b0~%0%BG5db3Jx@BAQz%y@-p^UdfGs4 zaRiWPV0ggI-*4S%eML&^x|IG!DgDdb`WsxXn|J}yiIVI7X+Si29fuvG_T;x5N2RQQ zdWxJt1T%ixmSYkGcPX}0}y=S?#!%V+sHS8iIHXVe5Rd@0Wl&<48gKe48eSsj72Ijsxj=r za_T^pU~ze!W66|#gN5f3NjQ3iZp_ii!{@jG*ve5VTog$e4i~~ zav7Twqx$5XY~>o(AQd(sLKH;Efe3jJVFx1YfrJK#;V?OZ-H36<r(m`rSvaz>rYnVv}4qooXB}p)Dfhc8ALdP2#`;UyeB(w psWHZGPUli*oIHnnJEQO95FT~Lh{-iPmi!8g!V@IFG5{&CV*qQ2KC%D+ diff --git a/repos/__pycache__/assets_repo.cpython-313.pyc b/repos/__pycache__/assets_repo.cpython-313.pyc index 3ae1148a6ff3c3be2ed66a48af8a20b42125653e..ead12a48c05b3b0f33b90f84ba7e8be0bccd8e07 100644 GIT binary patch delta 2058 zcmaJ>U2GIp6ux(6_ILKD+u7;Qwxzr5hO%O5fo&nM)c#RgkS+9fkhI$@?RIGvN-498 z{5)9EsE}yV=0=Sg6BAzW!347p`UHZ2K4}{tXcohx35J+LL!zR=b7s3sF~*zBH{U&f z_sqHH%-biw&A67GPCJ41+0~2bzZ&OUPwUEX3u}3h67oKY6P+@m3ykU_tK%=w#+fzD z7B|IhCNN_5xXfgD9B~J8mZYu{v&5ZoD_TZe%fiOCTwt6Z^HSQR8% z_@rm?Bvx%4={1tD#2t`?&sT$asmv0uhzP94FsNj;dewvwUYqL`_Sk%Rds0u0W>e#d zv9o#mw0`1fDw$2B$CDO5r+P=cA|XNIhvGJqDT_weW4U3CN`xG$WV8V|Xsnu4)QrO& zF+sC35p@QWkZIb4IM<7g z0WUvUq42{>RlQSlmQpgq=lneBC`Qm613pfbz>1Hrh+i4sB#=S-UMOGJsjkD1Ohd;OMw$ZjuOOKV_u+bhtx?%5> zU~|(&dnEHscZVG|x2kE6*?h|v27X(jh-SpRbu^PnW%KyZ=1-T0k?MB$ezf0AW$btJLENIFBGOiosBj;E!r|(+_gn>+aIYPSjF;H!4n^F5Z7#pmwck zRn~>JK!be%87!P1dZRB8nYn}lEYf}QI$8867tHtM%Eg8)^Ga~R5xV!RXI9Zgwfe)v zT;h%zSx_Ta4*sM@@3^D$*64ye`bP%u{EZHwL#Edy+F_Hfn@qs-Hlu0a?~zGjQx{k6 zZlHzIF762`(LxJ>1}cszuDYfFxt8N-$@t-*kK#S3d^XrjzsOw={w+YvO?N^@8)Ps_03nEgr7tvD7}+o%Z{8@+AniwB{L|+B z(8s?uGkPo6xv4=~YmVKAzguBqdt$LL9SjTlyG2P@jK%mnEt7i>pyU;V5rk2M34~(^ z(+Fn(pu5t^tbP%xnn+}_quF#aF`CWl>9JGUR3?#tS!-*prL%mvH8Rs=s<^!EO4sK* zmk7}BMdLL;G*Wm^>Bf7BkUrsn0IvZ-6CUF3And>aM8|{-YO=y<)EpDelr-I9FT^8# zVoU^0x7aT}MxTdcR5huWL8Y2_zV%4E)wJ`<`meWq*|k0&GAqin$9}6@m(ow?>bj7(X?cYSTaAo!eU6 N=R|6ML=X+5e*ncMr-J|h delta 1810 zcmaJ>T})g>6rQo3gz*2-j3)5>TT(VhVt4m>lyGu=s z6{CqYnwr#!Hoh2)FPfBuW_>X2gJ~=VO>7`0V(`tF7)&3iq=}l;dd}TlX?<{#`OY`z z%$#%P%(?fg)SFAT1FO|UV03LYXQ3OU64Lib*()yJ$ zW|XzEQ45=xNj5VxOp9z`df6%)P#ZEatB1^rvsAIMa%Pi_IlCdGWA>cIuIhENAtbT_ z)F}n3#vvO)Q>bbjtb#e=bUun2VMPsLV6L2_LRJZz@(Jl>GaPs4Y))1LNo7+#OiyIR zs(Y6jP%m5HM2V_#!zGGB0xMNl#mu9WydVU;yQXj|6;G$e;)@G=nae_#QJB{9n-16D z-vS{u#0!OLAW5x6Yq4}NVj>~qZbtpU!J?DIyk!RJ%>+ruG-y+qh>XW{#XM?p)jZb> zK5>lUMgKFHmg$-W{%S=Vzh`NW>YpJ=gQRzof!_#88Yiv!?UHfKp67>>>6_tEapa|B zWCn~r=}^@G%-BFAV;^myNfW>det&6{FD=jxm=KOtPI=JbrxqRCY z$;6M_hBT%z58u{%-m^JYERyA^&EL-1^1&p-SY@6cO39Q@&r62y6Jmw}O9me?kdZ%N zF2k&A)m)$f(}?1OV+s~b@gmeCR0HfihOw)OCT93C`-{a^#fg>5Mx(ATz5egBoF3Pif-sPdA{4JM{j_@^C3yo%e zan)#Tc=#{T2Mz-+rH8vcFTt*>o^$k4=4X#2)T_?N(Zv)%K%?LzDia9UI-pQ5J&KAc zK2v_0^309$pXkadv{EI4h9@#atugDQ4^3*J`jMSR2qLH#7(|A*2*8FigPjogWXKyZ4`vDXRx}nr{WUQv(FS^k zHt;QHDPONLi^7O9&A+KCD-aHoNTEZnuMS04fu!mH_Q-4GM|bJ<=(XsMJGAW%{ct|M z)zZId*>0A$F3x}NPVU%~TZZJeJ^5z}+xYdPP4os0ifh`Y%*WLu^emR>WsNV9%=Q?D z$h|2rC;=AxiL(7qoi5_eQGU|rp~HN_7oneIR(+2I8s;DQk5I;M`Fr4rANdF9TBg7D zo*oSDhDJKh2O4&K>YFT~hbn{`1V4iM;sIpmd2jHz`7%;0JcN2Bcov%KyC9>tGgXa# zu|gc#!#}?I&GZN8xqzU&onHqipKX~NI)iHoVG<#V5JyNLBoQtFz%wVN)5;a3?r1cX zj-?aR(O5dIB%WVR$5YWLG<{WT8ExU+t)Xz8w&>Hg_1@3A_6g8UjViV#rm`}rsB)i> zQ$n8rA|eb54{$XkJj4SJWd3XGqK9f1=z8rJO*iZJ3G&URhpODy@#}402HwHsjv=U* k>_t|B0BuKh4W07~Gs|=FZskM1(%x)e)KJs!1X11e4;6H0lK=n! diff --git a/repos/__pycache__/char_repo.cpython-313.pyc b/repos/__pycache__/char_repo.cpython-313.pyc index 5fd4d21f8fcb2b5ec3ce5dc732e8565bdcbdbadf..ccd7a75b783757ccffa08d8eb4d89a01c57ef392 100644 GIT binary patch literal 3774 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 G;lBY!TsS-c delta 1274 zcmY*YOKclO7@mJ;cfA|io1}^3M@Zu)q3ITs7fHh-q?j~SaKI{ASrpZh6~}AWD0to3 zt<-{4AgCx@sH{<&3nxzQ1u8X%BGD?`INGv=t3{Q#RH7GfDWb{=X4VA3Nc-*g|2s4P z>)U%n&9L?)80q?d zM|^P$S8)GkqyaEXJsSkd4q;Vw4$o_($C-$bkV=yJGeos z>I?KY;YGUZ9~e%aqbxgY1 zb`oOX3RcC&jKt${(o4U^v#>ya#pAyF;ponl>J@rKjQUmh1`CFd1F|Id5fB zbUzf`$yT$iklqOCwUPSd^4^8|#2d{}vK3lugx2cDJI&D9R`=O$@#j!@yX&XE@b(gY zTON3=Eu&z#6&Pv+hQ2H{4o}oZ8T=TSeC$X4k+z0`mEj`{D1Nt9i9prTpE zJt_T1MqhP3cm6o@+w%;(LbXHJtk12McC4Wtt3A6Lxij9Je!Z?Q?G=6~wNkl8D%VWq z>&s?SE3}kCQz`7*TsB*;BqX>C2`t`|86!dFW51c4vz%-`r^9Kzo7Mv-Ci%Za4sqa` zBSRech+HveS(}9CLb%Gw42OAoGB|jcPnYy_;DRJvb{?pV*ca%f;OR5%0lyThjnyvS zocseZ+Kz!#gj!!4@%No*=e_4V_3X}N{GODqy0UY*QYsg{uq+MRDie_|YS9wU)$MuT z%A1?^#M1e7vcBP%q?5Rp#Qzg>l?Ar<;-~+(MOT4-jmu3`Q`f#=R5atq}-|AlBOxZ*UiI8c1$dIgXFN#ZAO!j z^vwEZ{L~+YAVeUkv_MiF7O)k71~RpVR=8eutMfFsYA$av7s(MX+a)pgBX78X6k(r4 zXZK`HNcmB!9w*9?($V|Ws?UHy5=Jc>62^Kdb`{{m0UoQsPZu$W+H%JtBZ;Kr|wn z*r+(G;xkY@6m4Z^VrV*qvIU`4;pM^#a~#39AlJm7I|jah|8Mu6JTH?AWzfnwOmYo&BObTp5FBD7wctax2^G<^+v# pOZaE?aeRq{5E};Hp~nWphPVw~HtODA8Auzil_eL6{{V+G?GG8var6KH delta 386 zcmdnR-NwcDnU|M~0SK~=b!4t!n#d=?STRvORYHP6i6NL%iXoWGlnErwz)&QR&aKHa zaYNQ*O~wpH-pP{~^%&bGA7pH2PiN3%^wZ>++|OjeugP+Yr8qSwt%wh#g@5u#rZhGX zv)FWUEVG$ZFZXAVS(+?GA|QDo5FrR6geRY8_O9myF+t8Lk^mC7MC0R&OA<>mlj9Rh zN{TX*N=s6UPO4pz7Epo_h>JCW#0O?ZM#j4gMo$?u?lNfJWiYtQpn8`<<0~5jqYO~sI}0nL L(I*BV0X7N%J||h9 diff --git a/repos/__pycache__/generation_repo.cpython-313.pyc b/repos/__pycache__/generation_repo.cpython-313.pyc index 0c9a3be9a42b7252883cbd396610c20cfbbb6d95..f4bb2f37b2f72582120f2feb3ca87c2b325c3a4e 100644 GIT binary patch delta 1251 zcmZuwO-vg{6rNe{|N0llA^f=@68;hhG_I&jDW#?nRpM2`uqhNr$R-3qh>6Cuw55`x zRH{;`2dZh5Loe;Ir$V{p)T+7VwyPduS*nE8Q*N7}9C~Ws%o?Ord6M6J-`j8A%=^~+ zL*Kzb^e_@J5rk9wW_3NX7Y*Z%IOhPH+K@6S<&fB$5&=K~B=96{Ej(KO+4mSL@~yT`q1e-&ibtUN`U1jkV>a&BfIl zOB$cF5jw12!SC=#7`z*G!tewWX&9F^2bW}o=9)>U7X*$ZSTB(UQ%VLr`-mu<5#(q@ z6?IjpR)Dd4<`CA{nAQ=}h|U(YN{4vf!Xl$_U$Qz0^j2x4V>&+44G}4hhJsP@+`@^iL_i1z(_R$gcwO;D7Xp)mbo`!fj z&l9)*NcFGYrdq%|J6YP8!hf?RW2DDQc~)1gdvNC+FWgz{?!A98K(Evf*!RYR>iaks z#g9Wc7nUE(lLnBwUR*DIa+^D>LgS+d0T2%F00yVzIyXb)3w7M<@gLj?XY4^hE+N&(p@^JCH#iyx^m&)wU{+Y@?jb&Xu>&3GF zRN~O(-uR@7f5(%u{0CAd)%{eqvNf53*K zck#RXzeE=#Y%?oSPO7&3jDNXw7LslEE4Iz<#(|pnGM`onQ5?tO-^-)8}(RZ8RIKdB?pr|Kxf=Hn!Z)w!Mun z1%rMcN_okL3rKU)a|CQ&ddWBY=g=Lzi9t}sIXKW##&)MKKO%d19|3k}K6V#wc{A&R z)dt?&z3}s(&sDX{v#vAuBp;CSr|#AlUU^$EXf`cfg8m@$TbY_XFpDrE|41rln4fG< POkp_l1riHA1@->`6$B0% delta 966 zcmZ8fO-vI(6n-G`M_i{ z0E?%eU(JrGS+fz<8M$V0@Pok$q(DOiG)#~tkSf}PLg*BIgzA>0lqsc?n5WA?%r!74 z&LPcAP7^~)N$Gi=sEny4W;w1&DPxyRtVezev9Vps?vwpQV(0%gD-lz=E=(LHWg||_ zKBF|dw#?V2)6;7qye{G?iQ4o|&tnA80B+VUK?HkIT1ucv5n!Z>2k)D-Z^tGxgLQLymOSM+|UafUY8o!g)QmSz1 zW3VMoXfv4%pEoV4A@fB>GGPY$unc*6#aK_D7;7#3={;}rqb%wcKT7_!55`+q=VL|9 zPm#r8@8y)=wOKBoGC$j7BLpv=TBOTbDuz8+83-ruK97=bq+Tv za`^F2ph3sLQob)#6wK0@RnK~7wxs}!=7vXdqqCdCb0w8;@)Wos-xDb3Nc2n1X5{uE zFBaW8DYzP4C)uF_Fq(^x<&qCJ7aAh rH_m$FFEi7?pdcWGcA$O_>bIeK8#?wNxC4RTx?Z%1egQ|##e9DOwN<%D diff --git a/repos/__pycache__/user_repo.cpython-313.pyc b/repos/__pycache__/user_repo.cpython-313.pyc index 118d70144a29b1c31abcded33cced91274b6ba0f..271f4611f8fdfcd2a86803453e937f02050c6de0 100644 GIT binary patch delta 1627 zcmbVMT})g>6rS1pv;Tjvci9CRmhzXSvbz)ur2+|IF_pwMvTUs(?G|@0WgEPUa~Exl zkp`1yi7${zh3bPf@&FIUYF{uWM(B&x#L(~nu1#a&Q(w$(u||B+GXw3?2Yqmo`^}ki zXXecLzM1oFt16$tcwkw(!?@<8eTr?B?+IYopAuN-Eb7D-NP}9k&Fa2uQXaYxo)|w-7 z9a#D|bl5!N+^IRTbF{gnSUr}!spzAho4aQIow^p}=`96NMbA&pp*@|2nNMD~sdi$i<>1!FFooXo(J;?#-!t%>-LXat55(K<$sBdx|${ zr@z`x;6CcN7i$wRDJb%!$SYv)=fv(WIZK-yVH|~Nt#rmwV;*Hrmd34R#~*lxUz~2{ zW0V!#lVf@=Nz^x{Rp`@WIhC-7$7>5Y1J(#5bb5;2!dTEuSfL?N2K*(LcY8u=`A~2` zUZ+@azNR3isn7iq9-)Ib7=!>!-d2XbBN6-~505?0hU9u>WXQiQtpj)ft o-FLpt;l!OIkC^LWNS2hMi#Al<$Cs(W7j~)`D-RJvQUhWB0jkMz7ytkO delta 1150 zcmY*YUrbYH6u;l?y|=fe?d4BzCn>+AQ0%4@-({OW4D*b1qZnP44eI_nhDN zo!@uPxj%;=g*;nsw^P7pXlW*UPrK`JOKccYWf2(ABseYkTK$45B&4YAEHlIeH;{Tu ze=jMI+>fy7$V<~4l8JiXu&U1AV#j&!WH4AUbB##OK#{*>w44j#Rmrp&GF}Y z?}pY}P6F|ev|_+EH}I_;^6xtLX_+y1XN-rCdW=HMNLo-)TX>EyYTX_zcVDX4_y~jXvjj?!CS`(Ye4zYMlTKiZgJIV-SgkuahzI`M#tRl=~o4>%I*Za27JD^T{ujG!%|#nAV-%^F zNSXO0&wM$R!8M)AXLtt%X@>bJgdu|!m8o;otc2mx|4-C)fy_sJ`93!#DGshmpk List[Asset]: + async def get_assets(self, asset_type: Optional[str] = None, limit: int = 10, offset: int = 0, with_data: bool = False, created_by: Optional[str] = None, project_id: Optional[str] = None) -> List[Asset]: filter = {} if asset_type: filter["type"] = asset_type @@ -71,6 +71,9 @@ class AssetsRepo: # So list DOES NOT return thumbnails by default. args["thumbnail"] = 0 + if project_id: + filter["project_id"] = project_id + res = await self.collection.find(filter, args).sort("created_at", -1).skip(offset).limit(limit).to_list(None) assets = [] for doc in res: @@ -157,8 +160,15 @@ class AssetsRepo: assets.append(Asset(**doc)) return assets - async def get_asset_count(self, character_id: Optional[str] = None) -> int: - return await self.collection.count_documents({"linked_char_id": character_id} if character_id else {}) + async def get_asset_count(self, character_id: Optional[str] = None, created_by: Optional[str] = None, project_id: Optional[str] = None) -> int: + filter = {} + if character_id: + filter["linked_char_id"] = character_id + if created_by: + filter["created_by"] = created_by + if project_id: + filter["project_id"] = project_id + return await self.collection.count_documents(filter) async def get_assets_by_ids(self, asset_ids: List[str]) -> List[Asset]: object_ids = [ObjectId(asset_id) for asset_id in asset_ids] diff --git a/repos/char_repo.py b/repos/char_repo.py index 8c17531..e28e2a5 100644 --- a/repos/char_repo.py +++ b/repos/char_repo.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from bson import ObjectId from motor.motor_asyncio import AsyncIOMotorClient @@ -12,7 +12,7 @@ class CharacterRepo: async def add_character(self, character: Character) -> Character: op = await self.collection.insert_one(character.model_dump()) - character.id = op.inserted_id + character.id = str(op.inserted_id) return character async def get_character(self, character_id: str, with_image_data: bool = False) -> Character | None: @@ -26,18 +26,25 @@ class CharacterRepo: res["id"] = str(res.pop("_id")) return Character(**res) - async def get_all_characters(self) -> List[Character]: - docs = await self.collection.find({}, {"character_image_data": 0}).to_list(None) - - characters = [] - for doc in docs: - # Конвертируем ObjectId в строку и кладем в поле id + async def get_all_characters(self, created_by: Optional[str] = None, project_id: Optional[str] = None) -> List[Character]: + filter = {} + if created_by: + filter["created_by"] = created_by + if project_id: + filter["project_id"] = project_id + + args = {"character_image_data": 0} # don't return image data for list + res = await self.collection.find(filter, args).to_list(None) + chars = [] + for doc in res: doc["id"] = str(doc.pop("_id")) + chars.append(Character(**doc)) + return chars - # Создаем объект - characters.append(Character(**doc)) + async def update_char(self, char_id: str, character: Character) -> bool: + result = await self.collection.update_one({"_id": ObjectId(char_id)}, {"$set": character.model_dump()}) + return result.modified_count > 0 - return characters - - async def update_char(self, char_id: str, character: Character) -> None: - await self.collection.update_one({"_id": ObjectId(char_id)}, {"$set": character.model_dump()}) + async def delete_character(self, char_id: str) -> bool: + result = await self.collection.delete_one({"_id": ObjectId(char_id)}) + return result.deleted_count > 0 diff --git a/repos/dao.py b/repos/dao.py index 5bc70bd..23e7bbf 100644 --- a/repos/dao.py +++ b/repos/dao.py @@ -5,6 +5,7 @@ from repos.char_repo import CharacterRepo from repos.generation_repo import GenerationRepo from repos.user_repo import UsersRepo from repos.albums_repo import AlbumsRepo +from repos.project_repo import ProjectRepo from typing import Optional @@ -16,3 +17,5 @@ class DAO: self.assets = AssetsRepo(client, s3_adapter, db_name) self.generations = GenerationRepo(client, db_name) self.albums = AlbumsRepo(client, db_name) + self.projects = ProjectRepo(client, db_name) + self.users = UsersRepo(client, db_name) diff --git a/repos/generation_repo.py b/repos/generation_repo.py index 6035fd7..c668e97 100644 --- a/repos/generation_repo.py +++ b/repos/generation_repo.py @@ -25,13 +25,19 @@ class GenerationRepo: return Generation(**res) async def get_generations(self, character_id: Optional[str] = None, status: Optional[GenerationStatus] = None, - limit: int = 10, offset: int = 10) -> List[Generation]: + limit: int = 10, offset: int = 10, created_by: Optional[str] = None, project_id: Optional[str] = None) -> List[Generation]: filter = {"is_deleted": False} if character_id is not None: filter["linked_character_id"] = character_id if status is not None: filter["status"] = status + if created_by is not None: + filter["created_by"] = created_by + filter["project_id"] = None + if project_id is not None: + filter["project_id"] = project_id + res = await self.collection.find(filter).sort("created_at", -1).skip( offset).limit(limit).to_list(None) generations: List[Generation] = [] @@ -40,12 +46,17 @@ class GenerationRepo: generations.append(Generation(**generation)) return generations - async def count_generations(self, character_id: Optional[str] = None, status: Optional[GenerationStatus] = None, album_id: Optional[str] = None) -> int: + async def count_generations(self, character_id: Optional[str] = None, status: Optional[GenerationStatus] = None, + album_id: Optional[str] = None, created_by: Optional[str] = None, project_id: Optional[str] = None) -> int: args = {} if character_id is not None: args["linked_character_id"] = character_id if status is not None: args["status"] = status + if created_by is not None: + args["created_by"] = created_by + if project_id is not None: + args["project_id"] = project_id return await self.collection.count_documents(args) async def get_generations_by_ids(self, generation_ids: List[str]) -> List[Generation]: diff --git a/repos/project_repo.py b/repos/project_repo.py new file mode 100644 index 0000000..3edf3b4 --- /dev/null +++ b/repos/project_repo.py @@ -0,0 +1,62 @@ +from typing import List, Optional +from bson import ObjectId +from motor.motor_asyncio import AsyncIOMotorClient +from models.Project import Project + +class ProjectRepo: + def __init__(self, client: AsyncIOMotorClient, db_name="bot_db"): + self.collection = client[db_name]["projects"] + + async def create_project(self, project: Project) -> str: + res = await self.collection.insert_one(project.model_dump()) + return str(res.inserted_id) + + async def get_project(self, project_id: str) -> Optional[Project]: + if not ObjectId.is_valid(project_id): + return None + res = await self.collection.find_one({"_id": ObjectId(project_id)}) + if res: + res["id"] = str(res.pop("_id")) + return Project(**res) + return None + + async def get_projects_by_user(self, user_id: str) -> List[Project]: + # Find projects where user is owner OR in members + filter = { + "$or": [ + {"owner_id": user_id}, + {"members": user_id} + ], + "is_deleted": False + } + cursor = self.collection.find(filter).sort("created_at", -1) + projects = [] + async for doc in cursor: + doc["id"] = str(doc.pop("_id")) + projects.append(Project(**doc)) + return projects + + async def add_member(self, project_id: str, user_id: str) -> bool: + res = await self.collection.update_one( + {"_id": ObjectId(project_id)}, + {"$addToSet": {"members": user_id}} + ) + return res.modified_count > 0 + + async def remove_member(self, project_id: str, user_id: str) -> bool: + res = await self.collection.update_one( + {"_id": ObjectId(project_id)}, + {"$pull": {"members": user_id}} + ) + return res.modified_count > 0 + + async def update_project(self, project_id: str, updates: dict) -> bool: + res = await self.collection.update_one( + {"_id": ObjectId(project_id)}, + {"$set": updates} + ) + return res.modified_count > 0 + + async def delete_project(self, project_id: str) -> bool: + res = await self.collection.update_one({"_id": ObjectId(project_id)}, {"$set": {"is_deleted": True}}) + return res.modified_count > 0 diff --git a/repos/user_repo.py b/repos/user_repo.py index a1434cd..c4e5e9d 100644 --- a/repos/user_repo.py +++ b/repos/user_repo.py @@ -19,10 +19,14 @@ class UsersRepo: self.collection = client[db_name]["users"] async def get_user(self, user_id: int): - return await self.collection.find_one({"user_id": user_id}) + user = await self.collection.find_one({"user_id": user_id}) + user["id"] = str(user["_id"]) + return user async def get_user_by_username(self, username: str): - return await self.collection.find_one({"username": username}) + user = await self.collection.find_one({"username": username}) + user["id"] = str(user["_id"]) + return user async def create_user(self, username: str, password: str, full_name: Optional[str] = None): """Создает нового пользователя с username/паролем""" @@ -38,15 +42,22 @@ class UsersRepo: "created_at": datetime.now(), "is_email_user": False, # Теперь это просто "обычный" юзер, не телеграм (хотя поле можно переименовать) "is_web_user": True, - "is_admin": False + "is_admin": False, + "project_ids": [], + "current_project_id": None } result = await self.collection.insert_one(user_doc) - return await self.collection.find_one({"_id": result.inserted_id}) + user = await self.collection.find_one({"_id": result.inserted_id}) + user["id"] = str(user["_id"]) + return user async def get_pending_users(self): """Возвращает список пользователей со статусом PENDING""" cursor = self.collection.find({"status": UserStatus.PENDING}) - return await cursor.to_list(length=100) + users = await cursor.to_list(length=100) + for user in users: + user["id"] = str(user["_id"]) + return users async def approve_user(self, username: str): await self.collection.update_one( diff --git a/routers/__pycache__/char_router.cpython-313.pyc b/routers/__pycache__/char_router.cpython-313.pyc index 478ec0b96f6d6048f271e6911fc01670f04ea1c1..7dabe6b7ee1718625e23251ce7e4b38610f5b44b 100644 GIT binary patch delta 1288 zcmZ9LO>7%g5XawmKWwi**3LRO$@(*`vE8^%(gsSPiDZ!yx)zDFmO+&dA*o%d$``yO zf(VKWTqqK#=t-0tm5?|fBwFo(1EL~wm15^Y;1DU|zyTrkfD{rg2qE!iZMTJ$_Ba2T zH#;-$?QWy|#f!cppHCtD`sT}Pmm9ZzUwL4gee3zhU-4lSuQD?28IS7lu(iQ~={ zrR+8}_O`6CJ29u6AZ3RcvNJ~FA?ESLpr4J#H9;{4c+v@Z#Zwl{s2%c{F$}R=z7!Ut%q@_1L9nXY z6}9qwH)0y>$G|ao0f%7jU>%(7TJUA>zH*V)=GSZ0xrI&Zeoz5Ovp;)MAz^Ku?z=D1 z)#bU3wHl?TcpcWKU9mpZ@}SIfZ;0P(@3OtnG@N4b-qGmK;ek)i?CH5(J-4S1@9M)p zg-8BST><4i@u0`L(EASbECTTk9x5bIf(e&AMoe8yZI>5El?UlkB?}YnuzJ^R&7~E9^skNP3k=7p*&b3gDvkYvdO}u43{f z)=ES({2L%yfevD15^)&=YP&qPqWIU)DW)dlGgyhPAeIsM#_23#41uqkKFPtSqo^NF zH-fG`Vg=PJh&lFVG9_(t`G)m*GA%^bF}lH#b=o_juVEmMc%9upo(%l2XoU@=a?%yP ze%&gjegLV?>Uaf8{)h6i& r%*y;W`V71TH=PFr?JcEkzth_G`_o5u{74{<{DA{g4&Gv|>|g%^4RjP1 delta 1230 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 w3y!c@G1odi@0e4