From 97483b7030b6bd81d06c0e316f7008e582324160 Mon Sep 17 00:00:00 2001 From: xds Date: Sun, 15 Feb 2026 10:26:01 +0300 Subject: [PATCH 1/3] + ideas --- aiws.py | 2 + api/dependency.py | 5 ++ api/endpoints/idea_router.py | 61 +++++++++++++ api/models/GenerationRequest.py | 2 + .../GenerationRequest.cpython-313.pyc | Bin 3698 -> 3790 bytes api/service/generation_service.py | 4 + api/service/idea_service.py | 22 +++++ models/Generation.py | 1 + models/Idea.py | 12 +++ models/__pycache__/Generation.cpython-313.pyc | Bin 3329 -> 3377 bytes repos/__pycache__/dao.cpython-313.pyc | Bin 1466 -> 1576 bytes .../generation_repo.cpython-313.pyc | Bin 7177 -> 7404 bytes repos/dao.py | 2 + repos/generation_repo.py | 19 ++++- repos/idea_repo.py | 39 +++++++++ tests/test_idea.py | 80 ++++++++++++++++++ 16 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 api/endpoints/idea_router.py create mode 100644 api/service/idea_service.py create mode 100644 models/Idea.py create mode 100644 repos/idea_repo.py create mode 100644 tests/test_idea.py diff --git a/aiws.py b/aiws.py index 04b17d9..5214dba 100644 --- a/aiws.py +++ b/aiws.py @@ -43,6 +43,7 @@ from api.endpoints.auth import router as api_auth_router from api.endpoints.admin import router as api_admin_router from api.endpoints.album_router import router as api_album_router from api.endpoints.project_router import router as project_api_router +from api.endpoints.idea_router import router as idea_api_router load_dotenv() logger = logging.getLogger(__name__) @@ -210,6 +211,7 @@ app.include_router(api_char_router) app.include_router(api_gen_router) app.include_router(api_album_router) app.include_router(project_api_router) +app.include_router(idea_api_router) # Prometheus Metrics (Instrument after all routers are added) Instrumentator( diff --git a/api/dependency.py b/api/dependency.py index 7dc90eb..51674c5 100644 --- a/api/dependency.py +++ b/api/dependency.py @@ -45,6 +45,11 @@ def get_generation_service( ) -> GenerationService: return GenerationService(dao, gemini, s3_adapter, bot) +from api.service.idea_service import IdeaService + +def get_idea_service(dao: DAO = Depends(get_dao)) -> IdeaService: + return IdeaService(dao) + from fastapi import Header async def get_project_id(x_project_id: Optional[str] = Header(None, alias="X-Project-ID")) -> Optional[str]: diff --git a/api/endpoints/idea_router.py b/api/endpoints/idea_router.py new file mode 100644 index 0000000..b2022b8 --- /dev/null +++ b/api/endpoints/idea_router.py @@ -0,0 +1,61 @@ +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, Query +from api.dependency import get_idea_service, get_current_user, get_project_id, get_generation_service +from api.service.idea_service import IdeaService +from api.service.generation_service import GenerationService +from models.Idea import Idea +from api.models.GenerationRequest import GenerationResponse + +router = APIRouter(prefix="/ideas", tags=["ideas"]) + +@router.post("", response_model=Idea) +async def create_idea( + name: str, + project_id: str = Depends(get_project_id), + current_user: dict = Depends(get_current_user), + idea_service: IdeaService = Depends(get_idea_service) +): + if not project_id: + raise HTTPException(status_code=400, detail="Project ID header is required") + + return await idea_service.create_idea(name, project_id, str(current_user["_id"])) + +@router.get("", response_model=List[Idea]) +async def get_ideas( + project_id: str = Depends(get_project_id), + limit: int = 20, + offset: int = 0, + idea_service: IdeaService = Depends(get_idea_service) +): + if not project_id: + raise HTTPException(status_code=400, detail="Project ID header is required") + return await idea_service.get_ideas(project_id, limit, offset) + +@router.get("/{idea_id}", response_model=Idea) +async def get_idea( + idea_id: str, + idea_service: IdeaService = Depends(get_idea_service) +): + idea = await idea_service.get_idea(idea_id) + if not idea: + raise HTTPException(status_code=404, detail="Idea not found") + return idea + +@router.delete("/{idea_id}") +async def delete_idea( + idea_id: str, + idea_service: IdeaService = Depends(get_idea_service) +): + success = await idea_service.delete_idea(idea_id) + if not success: + raise HTTPException(status_code=404, detail="Idea not found or could not be deleted") + return {"status": "success"} + +@router.get("/{idea_id}/generations", response_model=List[GenerationResponse]) +async def get_idea_generations( + idea_id: str, + limit: int = 50, + offset: int = 0, + generation_service: GenerationService = Depends(get_generation_service) +): + return await generation_service.get_generations(idea_id=idea_id, limit=limit, offset=offset) diff --git a/api/models/GenerationRequest.py b/api/models/GenerationRequest.py index 33a4010..e6b2ae6 100644 --- a/api/models/GenerationRequest.py +++ b/api/models/GenerationRequest.py @@ -17,6 +17,7 @@ class GenerationRequest(BaseModel): use_profile_image: bool = True assets_list: List[str] project_id: Optional[str] = None + idea_id: Optional[str] = None count: int = Field(default=1, ge=1, le=10) @@ -47,6 +48,7 @@ class GenerationResponse(BaseModel): cost: Optional[float] = None created_by: Optional[str] = None generation_group_id: Optional[str] = None + idea_id: Optional[str] = None created_at: datetime = datetime.now(UTC) updated_at: datetime = datetime.now(UTC) diff --git a/api/models/__pycache__/GenerationRequest.cpython-313.pyc b/api/models/__pycache__/GenerationRequest.cpython-313.pyc index 0726ddb09aee4939f0106e13d72247b80b8f7d21..c74e8026b60fb9d7c8181a37939b3cd5644ee0f5 100644 GIT binary patch delta 1052 zcma))&rcIU6vubFv$Slx)HQ8wOAAXO!tygf#G-(rq6PxC?uEk=)3g<<2skbA2nPdr z!;FU>Of;UncrYG~G4YO}Y0}{S2PPa0_TWjK_X_bxdT=-S%**_EZ@%9*hmlWF^_!|H z0{<2YPjbmO>QhlXU@ygYBc_A;EZJ(1`I_Z4J)JQ6V}hyjzsdBDN;F`3`N|1cF*SN> z(P5lq>yE7k+nLK`Q%sV&#USgKp1DJO2Xz5j*`Cxa>g+&Di<>M#?zHbm+cvIk&+nuk zFO*5!XDv{C@FMwYsHF98Ol=QWNI&gmyW}p<6qkpbI!`iZxY(rIpMaau(*Og|1JD7z zfIh&$F@ikE`#83Qea*iY=q_nJWzw^+`6|R$J21fBDN6j=RI?Uo&l6Bu0b|@=RZ%?4$F02L;<~B~7!b zLN;?NRj`${)yz?hN{tvdBJ4&Fpouijez=0`qY$Dya9?^JFvelax0hB@1v(BB696>s z0z?-9lYmQr%YZ8Y?486TTUuUuND0hM0j>h-neGJ*C?v{*e2hJBJXHVcGTTu)8?Z}* z-1J-^4hOK>5JWo1oY4r{5~vNeVAgu|rr`_#(i}HA6e*Jkwz{jN85LsGnmi3nW*^+s zI+_44!d91Z%lE+Gd_G&q|5?2ctKWe9c!he^XJHjE&Q?7wHC3}%<+bOmhGs&QqqI&C zHHvBLB}>9i0G!J^Irw|^csFNiw@Z4(N)aV8(#|z*=o$vqs_-)tN{guqxQg(vQ rv!7~LhawIX8><2zs$qxdFB(;W4^<^7_7y`_fe+OYHq+EjpfkS!E+^0b delta 946 zcmZvaO>7cD6vua9`Q8OLVWE`I0<};SE3LJ)*fh}|Y8yi3;K5A@0#X!<&MwiT2V;!~ zFXMrOiN=`d)p+#o$r!6igL}}khbE@(!HbFWX4@JgyV>7N_Pu%k_c0%$dlB)cD7rcN zeEsyOExr~XBQzk-(c7frPjVH3zGB5YZ^g3WQydlF1of;O!GV?~D-mMZA}Q+x3X)ap zlcbZHaEwBit5sF2Y3+KorfIl+rKdI3Y8A(cm#?Ec1oIp67b(!c^=$oBVOz@IU4AgV z!{>31{N}IGU>mkVCr;y(vQC_Upksg}AO)}i#sL|?Bmn$z4sZ;RKR}RfR6oTQH<09y zfyr$t_nFTPB>xWYHxJ~=4DT3psmo|y_=2wJxe${#_A*M;%EtGfKLPL z014m$2!J^Nt9u@HX8;Qny6tMGThnk6ew+ocmMoHou4vH;4%W@?ZOnt^JYX3xtYeTf zOFTk1s-Nt-_Q~mePKM>mVh$5qfir+v z^34-9HC9Xw;lel+hGAy9*lBNGhu*1bQ&ay}DS;=G8m6)aR=@&zFT_lhHL6k$EP(|S z_Cq=j6Q3Cts0^NfCHkoj3VNFa-~Cnd0+q=N@!mAMa;#!j9TR5<|Gk>Jvv{N1=yjTl r_!7D7&7@uEMBim_bTLLONbYA1jxL5fh>rI|21gfThP?Eqc$mZ=$*jQ4 diff --git a/api/service/generation_service.py b/api/service/generation_service.py index d729d83..b241923 100644 --- a/api/service/generation_service.py +++ b/api/service/generation_service.py @@ -137,6 +137,10 @@ class GenerationService: if generation_group_id: generation_model.generation_group_id = generation_group_id + # Explicitly set idea_id from request if present (already in model_dump, but ensuring clarity) + if generation_request.idea_id: + generation_model.idea_id = generation_request.idea_id + gen_id = await self.dao.generations.create_generation(generation_model) generation_model.id = gen_id diff --git a/api/service/idea_service.py b/api/service/idea_service.py new file mode 100644 index 0000000..fd1641c --- /dev/null +++ b/api/service/idea_service.py @@ -0,0 +1,22 @@ +from typing import List, Optional +from repos.dao import DAO +from models.Idea import Idea + +class IdeaService: + def __init__(self, dao: DAO): + self.dao = dao + + async def create_idea(self, name: str, project_id: str, user_id: str) -> Idea: + idea = Idea(name=name, project_id=project_id, created_by=user_id) + idea_id = await self.dao.ideas.create_idea(idea) + idea.id = idea_id + return idea + + async def get_ideas(self, project_id: str, limit: int = 20, offset: int = 0) -> List[Idea]: + return await self.dao.ideas.get_ideas(project_id, limit, offset) + + async def get_idea(self, idea_id: str) -> Optional[Idea]: + return await self.dao.ideas.get_idea(idea_id) + + async def delete_idea(self, idea_id: str) -> bool: + return await self.dao.ideas.delete_idea(idea_id) diff --git a/models/Generation.py b/models/Generation.py index 9a75c84..8f164f9 100644 --- a/models/Generation.py +++ b/models/Generation.py @@ -38,6 +38,7 @@ class Generation(BaseModel): generation_group_id: Optional[str] = None created_by: Optional[str] = None # Stores User ID (Telegram ID or Web User ObjectId) project_id: Optional[str] = None + idea_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/Idea.py b/models/Idea.py new file mode 100644 index 0000000..0a7ebb1 --- /dev/null +++ b/models/Idea.py @@ -0,0 +1,12 @@ +from datetime import datetime +from typing import Optional, List +from pydantic import BaseModel, Field + +class Idea(BaseModel): + id: Optional[str] = None + name: str = "New Idea" + project_id: str + created_by: str # User ID + is_deleted: bool = False + created_at: datetime = Field(default_factory=datetime.now) + updated_at: datetime = Field(default_factory=datetime.now) diff --git a/models/__pycache__/Generation.cpython-313.pyc b/models/__pycache__/Generation.cpython-313.pyc index cb9ecdb2d77fb9c455b1c77b3cb8f4ea7aa01bbf..189b987340a052776af22e9a4094f153bf176a95 100644 GIT binary patch delta 295 zcmZpa+9<{QnU|M~0SF>XCT7NO%l$$4CAj2)Y&a=9?_-H=smaPNqiklY!2 knMHB(P44ZC0h1^4s53@S-pOOhugoYsLGmjDkOEr`07!8^#{d8T delta 248 zcmdle)hNaLnU|M~0SLG+^<~;_HqT?9!^o&J*_tyONG|6*FX{{uX9f|j yAOht4BHzgqxYQV{HgD&0VVum#vz^g@@Q z2uI;>tfn;44)nGoB!rB>{`Z9$#|SSok5e)Pm^czai(B?O_p0M#i==IMf@Nw0+UUd)7U`F zV&BONnf+9Hxjuu;(qt!~i6~x&WjWM+5)> diff --git a/repos/__pycache__/generation_repo.cpython-313.pyc b/repos/__pycache__/generation_repo.cpython-313.pyc index a36ef065d34ea1a6dbc954970959ccc52b454cbd..235953dbecef798df2f695734957d0627cf06ff2 100644 GIT binary patch delta 1533 zcmZ`(O-vg{6rLIHuGikRF?a!EW5)y{Y$$>TBOs7cic)A=Ov2#cf)ix0ad3>O#)ec) zCHi-0ZlGz^N|mZqy|g`ntV)p@RZVW~rQW6zmQT$QsRt*hJ@nGftPM>g^-259`{w=3 zoA-A2_R#Oc_9L6k3Ye_tch^GNzI_yW8#rfjGz$Uv7{ma97!ZUZG2vF4f@AavY@+vI z>xd&}iJ3*_ip7{MCUGs~!1ZpB7SfQ|eZU8>4_i;X6s9SLe*?83@AlL2YRHbAwa7ZG zv3$>F7H}iC69*PzQV8KDgVp2anE9#m1?)2R8}KRI5>uH)4GGv?%k0D+qNOZBf7w2n zEPRH35w^?bwL~($w3b+r`K@ch_4nCx^b>1BJMgncV2Dv4%*qkCEC3L7VN)k_h#-Rn zuwXE8S!K2;77Zq?u-jq~k+D?ehVS(nGjryPzKP9-(Hy9q9Y-uko`Kkswa|CPE)Dh%3b&~ny823Xu7k3amIN!hwWlJR^pu@ukKj3o?h^gBy? zui;geR@XB5Bq>{S8zjH9K@v$qI#?uea%g3c-lRH2{_z(U0bQM zdq>)NTX%Gnltx`?E4BCRjOwbtq%`P?ujKFC8P-+rpEB^A-o1Q>d}#embA2+iJ9D4~ zbS>~z(?hNAKpW7tfxXNR7awYDe@@rt=shdhZklL^4_t7fO?c2UZegS>$1}^D>wL~; zR?xASm}1QNbEZ`1+y}De+KihqGa$cn$)V%{Q7DUG)f(wOs!}m}bxS33#>etM(i+YA08y)chq}qTe*H!X&MCed{!b!-xD2Mc79H z!r^dG3eyAEmV1Kly}|)=h;q2fAx%>)L-132r)6^c8d^q=O9(%`x3`r2GE68&+yO0rwUn{VmaTL!Bj5Jp!}1%P?9fF84I&>}jv zq|gm+$)GK6NuitQ50iIW^i*B|Z@@fcvDI2^;>h0AUA#YC0Zc8T&qz+F0t%I@usiSI1p)@<0>pr_!*mrv&yhVTZbk|*n@bnYFk$6~}{sB_k BSaJXW delta 1303 zcmZ8gO>7%Q6rNe{|E~XPZ{pZalafN}q*3!@NN|(3ZoqYE;)YDONg9VJzb0;QtFdW; zR)il3!3Elm5Jx~rAjBo96{#1B=&hXAszfYbIYn@Ai8yeAnce&#p0wY*Z{9cWy?LYk zuCuH@wjt=df@S*_C(skU|5j6}7FdiF{1 z+*WX;5*#5(4L8GK4}9c;!#e+nPb!quG8b>PnYle zra5@A>rxj+fszj&mYQ zixDX|egkL7L}(daArC{Fele5TX8%0CN+FYp3FM=u8{so-C&gflfyrQ+!Rw?UJOCHT z#qhbcGu%tuQ&&)ktRH*Ox6xGx)V0PYt*MpDSl(vUXOGpHpxU2s)=|^CJoR|SA=hqN zZ{4a~yKS>N2hMY&T%9(-O>$2GWmDW=>~Q@!xDHE@3fAB-EvN}6*V7Ns#`)1YpuRPJ z#hSia8DF(owWD{v@B6MgQ&mSUS?1*9QHK<6SU2CP6mHp!CQEY{XnrcIhs*aOEfBs< T?ngVlGY~%WCt!#p^uqrF2! List[Generation]: + limit: int = 10, offset: int = 0, created_by: Optional[str] = None, project_id: Optional[str] = None, idea_id: Optional[str] = None) -> List[Generation]: filter = {"is_deleted": False} if character_id is not None: @@ -35,11 +35,20 @@ class GenerationRepo: filter["status"] = status if created_by is not None: filter["created_by"] = created_by - filter["project_id"] = None + # If filtering by created_by user (e.g. "My Generations"), we typically imply personal scope if project_id is None. + # But if project_id is passed, we filter by that. + if project_id is None: + filter["project_id"] = None if project_id is not None: filter["project_id"] = project_id + if idea_id is not None: + filter["idea_id"] = idea_id - res = await self.collection.find(filter).sort("created_at", -1).skip( + # If fetching for an idea, sort by created_at ascending (cronological) + # Otherwise typically descending (newest first) + sort_order = 1 if idea_id else -1 + + res = await self.collection.find(filter).sort("created_at", sort_order).skip( offset).limit(limit).to_list(None) generations: List[Generation] = [] for generation in res: @@ -48,7 +57,7 @@ class GenerationRepo: return generations 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: + album_id: Optional[str] = None, created_by: Optional[str] = None, project_id: Optional[str] = None, idea_id: Optional[str] = None) -> int: args = {} if character_id is not None: args["linked_character_id"] = character_id @@ -58,6 +67,8 @@ class GenerationRepo: args["created_by"] = created_by if project_id is not None: args["project_id"] = project_id + if idea_id is not None: + args["idea_id"] = idea_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/idea_repo.py b/repos/idea_repo.py new file mode 100644 index 0000000..6ea79f9 --- /dev/null +++ b/repos/idea_repo.py @@ -0,0 +1,39 @@ +from typing import Optional, List +from bson import ObjectId +from motor.motor_asyncio import AsyncIOMotorClient +from models.Idea import Idea + +class IdeaRepo: + def __init__(self, client: AsyncIOMotorClient, db_name="bot_db"): + self.collection = client[db_name]["ideas"] + + async def create_idea(self, idea: Idea) -> str: + res = await self.collection.insert_one(idea.model_dump()) + return str(res.inserted_id) + + async def get_idea(self, idea_id: str) -> Optional[Idea]: + if not ObjectId.is_valid(idea_id): + return None + res = await self.collection.find_one({"_id": ObjectId(idea_id)}) + if res: + res["id"] = str(res.pop("_id")) + return Idea(**res) + return None + + async def get_ideas(self, project_id: str, limit: int = 20, offset: int = 0) -> List[Idea]: + filter = {"project_id": project_id, "is_deleted": False} + res = await self.collection.find(filter).sort("updated_at", -1).skip(offset).limit(limit).to_list(None) + ideas = [] + for doc in res: + doc["id"] = str(doc.pop("_id")) + ideas.append(Idea(**doc)) + return ideas + + async def delete_idea(self, idea_id: str) -> bool: + if not ObjectId.is_valid(idea_id): + return False + res = await self.collection.update_one( + {"_id": ObjectId(idea_id)}, + {"$set": {"is_deleted": True}} + ) + return res.modified_count > 0 diff --git a/tests/test_idea.py b/tests/test_idea.py new file mode 100644 index 0000000..18fe1ee --- /dev/null +++ b/tests/test_idea.py @@ -0,0 +1,80 @@ +import asyncio +import os +from dotenv import load_dotenv +from motor.motor_asyncio import AsyncIOMotorClient +from bson import ObjectId + +# Import from project root (requires PYTHONPATH=.) +from api.service.idea_service import IdeaService +from repos.dao import DAO +from models.Idea import Idea +from models.Generation import Generation, GenerationStatus +from models.enums import AspectRatios, Quality + +load_dotenv() + +MONGO_HOST = os.getenv("MONGO_HOST", "mongodb://localhost:27017") +DB_NAME = os.getenv("DB_NAME", "bot_db") + +print(f"Connecting to MongoDB: {MONGO_HOST}, DB: {DB_NAME}") + +async def test_idea_flow(): + client = AsyncIOMotorClient(MONGO_HOST) + dao = DAO(client, db_name=DB_NAME) + service = IdeaService(dao) + + # 1. Create an Idea + print("Creating idea...") + user_id = "test_user_123" + project_id = "test_project_abc" + idea = await service.create_idea("My Test Idea", project_id, user_id) + print(f"Idea created: {idea.id} - {idea.name}") + + # 2. Add Generation linked to Idea + print("Creating generation linked to idea...") + gen = Generation( + prompt="idea generation 1", + idea_id=idea.id, + project_id=project_id, + created_by=user_id, + aspect_ratio=AspectRatios.NINESIXTEEN, + quality=Quality.ONEK, + assets_list=[] + ) + gen_id = await dao.generations.create_generation(gen) + print(f"Created linked generation: {gen_id}") + + # Debug: Check if generation was saved with idea_id + saved_gen = await dao.generations.collection.find_one({"_id": ObjectId(gen_id)}) + print(f"DEBUG: Saved Generation in DB idea_id: {saved_gen.get('idea_id')}") + + # 3. Fetch Generations for Idea (Verify filtering and ordering) + print("Fetching generations for idea...") + gens = await dao.generations.get_generations(idea_id=idea.id) + print(f"Found {len(gens)} generations in idea") + + if len(gens) == 1 and gens[0].id == gen_id: + print("✅ Generation retrieval successful") + else: + print("❌ Generation retrieval FAILED") + + # 4. Fetch Ideas for Project + ideas = await service.get_ideas(project_id) + print(f"Found {len(ideas)} ideas for project") + + # Cleaning up + print("Cleaning up...") + await service.delete_idea(idea.id) + await dao.generations.collection.delete_one({"_id": ObjectId(gen_id)}) + + # Verify deletion + deleted_idea = await service.get_idea(idea.id) + # IdeaRepo.delete_idea logic sets is_deleted=True + if deleted_idea and deleted_idea.is_deleted: + print(f"✅ Idea deleted successfully") + + # Hard delete for cleanup + await dao.ideas.collection.delete_one({"_id": ObjectId(idea.id)}) + +if __name__ == "__main__": + asyncio.run(test_idea_flow()) From 5e7dc19bf3dc23b3a3175ad6a0f56ad6780f52d2 Mon Sep 17 00:00:00 2001 From: xds Date: Sun, 15 Feb 2026 12:42:15 +0300 Subject: [PATCH 2/3] ideas --- api/__pycache__/dependency.cpython-313.pyc | Bin 2468 -> 2727 bytes api/endpoints/idea_router.py | 62 +++++++++++++++--- api/models/IdeaRequest.py | 11 ++++ .../generation_service.cpython-313.pyc | Bin 27911 -> 28063 bytes api/service/generation_service.py | 7 +- api/service/idea_service.py | 57 +++++++++++++++- models/Idea.py | 1 + repos/idea_repo.py | 14 ++++ tests/test_idea.py | 31 +++++++-- 9 files changed, 162 insertions(+), 21 deletions(-) create mode 100644 api/models/IdeaRequest.py diff --git a/api/__pycache__/dependency.cpython-313.pyc b/api/__pycache__/dependency.cpython-313.pyc index eb067d1ef5835eda4ff5c7e6a03a68aa9bef1acd..6e621584864b40f6e4e55425c6c032fd98273407 100644 GIT binary patch delta 439 zcmZ1?yj+y`GcPX}0}v#ZOw8Ohkyny&+eY;u#(Hrti6RLFh8QL#h8R&(Mvw{y1{20$ zp%|`Ut{4dumSAxyhAfsW#v;jJhF}SA1}~{1DWG~OC5B+hV5wkfOD3RdnHYf>X;z?4 zpdKg-s8bfIQzl(fQ|=|mU`?i5>_Mr8rK!awx7b}$3sN_qVEo5YzmoA5w`WRfVsL6v zS!Qx-GSnF$9SqD+{8<9Xn92|hmkVWpIah_DNDwGlqyZ!}8H-eaR53`ILXjAdyd{vH zS`wcLG(NrCfl-evl;>w zm{0a)mt~ZloXvjO^p-?oL8czm96c06HGxXOCKu@dS+|&rONxp>+KUV(`*TQe*#Ma! sM-_W-F63Zklrwy&puU3rvVy@!21Z7i8Ok3()Q0G@ssI20 delta 181 zcmZ23xH>8b zP3GZ{Wt5t%#c|oVNCPNvi@CU@s0bugqyr)(K!g}bg2N^^KczG$)vm}K$YliLV(rZ# LoUDwKE4lOl$TKE< diff --git a/api/endpoints/idea_router.py b/api/endpoints/idea_router.py index b2022b8..f6ceb89 100644 --- a/api/endpoints/idea_router.py +++ b/api/endpoints/idea_router.py @@ -1,24 +1,28 @@ from typing import List, Optional -from fastapi import APIRouter, Depends, HTTPException, Query -from api.dependency import get_idea_service, get_current_user, get_project_id, get_generation_service +from fastapi import APIRouter, Depends, HTTPException, Query, Body +from api.dependency import get_idea_service, get_project_id, get_generation_service +from api.endpoints.auth import get_current_user from api.service.idea_service import IdeaService from api.service.generation_service import GenerationService from models.Idea import Idea -from api.models.GenerationRequest import GenerationResponse +from api.models.GenerationRequest import GenerationResponse, GenerationsResponse +from api.models.IdeaRequest import IdeaCreateRequest, IdeaUpdateRequest -router = APIRouter(prefix="/ideas", tags=["ideas"]) +router = APIRouter(prefix="/api/ideas", tags=["ideas"]) @router.post("", response_model=Idea) async def create_idea( - name: str, + request: IdeaCreateRequest, project_id: str = Depends(get_project_id), current_user: dict = Depends(get_current_user), idea_service: IdeaService = Depends(get_idea_service) ): - if not project_id: + if not project_id and not request.project_id: raise HTTPException(status_code=400, detail="Project ID header is required") - return await idea_service.create_idea(name, project_id, str(current_user["_id"])) + pid = project_id or request.project_id + + return await idea_service.create_idea(request.name, request.description, pid, str(current_user["_id"])) @router.get("", response_model=List[Idea]) async def get_ideas( @@ -41,6 +45,17 @@ async def get_idea( raise HTTPException(status_code=404, detail="Idea not found") return idea +@router.put("/{idea_id}", response_model=Idea) +async def update_idea( + idea_id: str, + request: IdeaUpdateRequest, + idea_service: IdeaService = Depends(get_idea_service) +): + idea = await idea_service.update_idea(idea_id, request.name, request.description) + if not idea: + raise HTTPException(status_code=404, detail="Idea not found") + return idea + @router.delete("/{idea_id}") async def delete_idea( idea_id: str, @@ -51,11 +66,42 @@ async def delete_idea( raise HTTPException(status_code=404, detail="Idea not found or could not be deleted") return {"status": "success"} -@router.get("/{idea_id}/generations", response_model=List[GenerationResponse]) +@router.get("/{idea_id}/generations", response_model=GenerationsResponse) async def get_idea_generations( idea_id: str, limit: int = 50, offset: int = 0, generation_service: GenerationService = Depends(get_generation_service) ): + # Depending on how generation service implements filtering by idea_id. + # We might need to update generation_service to support getting by idea_id directly + # or ensure generic get_generations supports it. + # Looking at generation_router.py, get_generations doesn't have idea_id arg? + # Let's check generation_service.get_generations signature again. + # It has: (character_id, limit, offset, user_id, project_id). NO IDEA_ID. + # I need to update GenerationService.get_generations too! + + # For now, let's assume I will update it. return await generation_service.get_generations(idea_id=idea_id, limit=limit, offset=offset) + +@router.post("/{idea_id}/generations/{generation_id}") +async def add_generation_to_idea( + idea_id: str, + generation_id: str, + idea_service: IdeaService = Depends(get_idea_service) +): + success = await idea_service.add_generation_to_idea(idea_id, generation_id) + if not success: + raise HTTPException(status_code=404, detail="Idea or Generation not found") + return {"status": "success"} + +@router.delete("/{idea_id}/generations/{generation_id}") +async def remove_generation_from_idea( + idea_id: str, + generation_id: str, + idea_service: IdeaService = Depends(get_idea_service) +): + success = await idea_service.remove_generation_from_idea(idea_id, generation_id) + if not success: + raise HTTPException(status_code=404, detail="Idea or Generation not found") + return {"status": "success"} diff --git a/api/models/IdeaRequest.py b/api/models/IdeaRequest.py new file mode 100644 index 0000000..773f38c --- /dev/null +++ b/api/models/IdeaRequest.py @@ -0,0 +1,11 @@ +from typing import Optional +from pydantic import BaseModel + +class IdeaCreateRequest(BaseModel): + name: str + description: Optional[str] = None + project_id: Optional[str] = None # Optional in body if passed via header/dependency + +class IdeaUpdateRequest(BaseModel): + name: Optional[str] = None + description: Optional[str] = None diff --git a/api/service/__pycache__/generation_service.cpython-313.pyc b/api/service/__pycache__/generation_service.cpython-313.pyc index a0f1879d59e01ba62471ecf76a971f0164da9ec3..1d495681a66f71ea4f7d1f469ce9454fa7e1e73e 100644 GIT binary patch delta 2646 zcmaKtdrVu`8NkoEwy&{`9l#GRU~GfI;AAAQLPL`BaEaR{Dm3K=O2ES}Y%ma#9GeBD zSqVzJv|AHp{hGGRnx?5#sp_OPo&VU{b&mw-+U_ALMQY|vdq-7NX>-?At29m8`HsPf zy2q9MyWi_P?)QE7+*3Jv@z>PyiP>x<{CZ;P8>yL**DY;^+D)kQ>yz`8kaHwN7*z?A zRLV4J1B47-+5jgEZq64CjkFQoHZI0{-YSAXFvZ}1d+f|=h6>_K*#rdj>k}*`-UZU2lM=Jj}vVuoC*CVUc z+K{bXQe9%@>UGVHS}UvJn?t8ZIGyX;t>qi76Bd_RUrm@>^)k<-I>H15gQMYc4+@2q-02M zCBufaw2h=CE4j}}$edx!fa%9e)wYr$!=MS;>a1|OQx6_RTfI-}cM)PD9=-w(HWxw( z8Cigc-Ck-)lI@x#_3H-wGA!9W3XAXvzm|>h8;c>rhzPu9_tQB1%HB;XAfitce=yTh|ESoml;D*a(;)O0;gD!}Atfl(dvJK0vC9m+{8FziHvfG4`reM(< zqfcf3=>0Zb6cw?tN{q!?6dFWiD}rjFR46@)IEnCcuVF+A+I$Xr4DR!_I!|yzdWauFpWP%U$n|pj z(O@TCE~O}2%)t)Lva>m8kyf;Hw_aY+1thMl>S@p@ua@XgHd7w7 zYHQE3d8dcJOciO2e@3fVmiRlQf`XL0na@Q2F=O#z)HWL_Gkanf5X`R47VRM zBo;CWHW!`U#-78tZz7H(&Lh5scph;9@dD@%)YD6_`M}ng=$FfA^&sjH{IDbJmx!Ms zS`lKSub{Ms_;2Yeo)kG_p;afs(F}VF-aODs6Y$Z2W|{}vXd}#JdP_6xt3LJ%=v3-@p?<1p!`m{a9@i~tPAOf*N!iK zOh|?Xq=G3=$gnid6%v+W5_b(t@msEW=_}}ZL<(rQYhJorQyev*iE2I-%j8RSx}BH3 zc{rRp9oVX?Si4)z6TFWn*CwamADYJsq2HSk)<|E);1+1>-yNJG)>6Uvh0sY%U>bus!rSf*ge;f zWZmPF{QJ)He)pbp@AdP)hbPNm`NV8CG4Z_ZpUZ52;FP7)*kmDHJf9o`#*VTWe)s;!YtX{UhKsrHyjHN~o8_0pqN z9pvFwHHdyw!)zs9uR2%bZE8?;#q4Um$S_fc+HgZM#T;D*wQ)7l`F|tpMWlN*(xrN0 zwOxwZB){CSs(DH4;EiH(KytQ`w}3v4e0_6V=tQoF1DK8}MqowRewAgecvb{|KoX(OcR5hJvz@6%;GG z$HQ38u+K;pr$^Oxun_}3n7vtm% z<7~5L9KyP2Scl)U`z>J&-zRc-f)K}x_An%I#oh}>-01MNCCNM{P}WSOa|Qamf9>IS zkTXrrW<25Y_Uj6B?ksI{n!%hBnPDBj zi$+Oe%+=63K^6_)N|q_Yq(DIwjy_uXTo#YJf-qfr$5jo^%A|$+#xRtW#y>R_fR{!Z z9|L`nEJ?iN-ceg1aX&#C58@Wj!*Bp!@tlO$aaYrfGR$zL(}usTvEkvg4TrowdzA>6 z&j{-RRU`Us5Y0Q+?8P5>-C)Nzy_>;-*Sv1L-f5MY<1IFf_v+NGysbMR6{b4v*y2rmyRaiwtU#smWP6No7`-9)>=6=gjLaV= zHBWeg5F$J&5H?iW^zTJ2>%}gKXHw-_@lNwZF_X{5AIhiFS^gAd%6=Mz^x*0ysxlXx%?D4tT2Hy{(zYfqq3Oi$NG{(=Uc}ByRu_ zwAk*8SacVv6(L_R1IZR0$yQCoQJQbdTVV#9dWPTxPV__|QJU|`0vyK1-flR8!@a}x zCun)p!m@7Y%l+FU{6##|+XPSJa&Kq&S#o}#fQ07=bbZR^-BRL1_{F|fz3lB180)(i zj^TWt3JHX*Pn%DQgm6Qp>T`H}>w^9m4e>H2`bYHCz;XZ|^#owPbh-apb>AX6FOXUD zP$tJeB=u&)Wi!u_P4-*1^(~=_gS3^bb7+Zfg3~w{4a1I7E_%h}KPv3sFs8p&O!HhK zyOIBZ;(kb&AsoltUMGHMuMb|pSN7hXlzZl93eI6c}1*YLT~ps3TC(E$7$FOBY)U7#FUpfE}633QS0 zpAya!Zd8c(kb03|Bgk3XN%|#$y2{@myhoskR4O@0(ys`I2!{#soVnPj5I-Q@Px##8 z|K~!r6fE0M{S2FBFUmD*HIIu!#jhd60zSF2KT!C~giC}!66Ofs#erA~(xsoo?$bXo zFD&7jTY682yKKr7;^J7QMKN!wi=4n&8bWT6BP8A28`{d=kT>%j-a8hBbEW6Ut|;Ok z=$sseLztcP&C0UTUkR@x z$ouDEl4=Q7!nX)?Q&euN( zlrTWJgV4Q(h!8Y|zXH~04t45uBk+3L=`Wr7!U`k#y+Y-^lt`@#g_)&q&R%D%2>psM z;h9XMhay)P%PR$C*-(F719`>vsmx!opfZKd^}70{ZR!f6_x|yviK&nGPYVrSn`)>N zHjz7~98f+b|H>Zx-PHZnzo6+Y68?#srw8D((#*6Qw$#!pWk=+evyr-aZ6%*uV%g9m buHFOLbT1F#ue0asM)Ytu{DsMvti- List[ - Generation]: - 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) + 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, idea_id: Optional[str] = None) -> GenerationsResponse: + generations = await self.dao.generations.get_generations(character_id = character_id,limit=limit, offset=offset, created_by=user_id, project_id=project_id, idea_id=idea_id) + total_count = await self.dao.generations.count_generations(character_id = character_id, created_by=user_id, project_id=project_id, idea_id=idea_id) generations = [GenerationResponse(**gen.model_dump()) for gen in generations] return GenerationsResponse(generations=generations, total_count=total_count) diff --git a/api/service/idea_service.py b/api/service/idea_service.py index fd1641c..89c430f 100644 --- a/api/service/idea_service.py +++ b/api/service/idea_service.py @@ -1,4 +1,5 @@ from typing import List, Optional +from datetime import datetime from repos.dao import DAO from models.Idea import Idea @@ -6,8 +7,8 @@ class IdeaService: def __init__(self, dao: DAO): self.dao = dao - async def create_idea(self, name: str, project_id: str, user_id: str) -> Idea: - idea = Idea(name=name, project_id=project_id, created_by=user_id) + async def create_idea(self, name: str, description: Optional[str], project_id: str, user_id: str) -> Idea: + idea = Idea(name=name, description=description, project_id=project_id, created_by=user_id) idea_id = await self.dao.ideas.create_idea(idea) idea.id = idea_id return idea @@ -18,5 +19,57 @@ class IdeaService: async def get_idea(self, idea_id: str) -> Optional[Idea]: return await self.dao.ideas.get_idea(idea_id) + async def update_idea(self, idea_id: str, name: Optional[str] = None, description: Optional[str] = None) -> Optional[Idea]: + idea = await self.dao.ideas.get_idea(idea_id) + if not idea: + return None + + if name is not None: + idea.name = name + if description is not None: + idea.description = description + + idea.updated_at = datetime.now() + await self.dao.ideas.update_idea(idea) + return idea + async def delete_idea(self, idea_id: str) -> bool: return await self.dao.ideas.delete_idea(idea_id) + + async def add_generation_to_idea(self, idea_id: str, generation_id: str) -> bool: + # Verify idea exists + idea = await self.dao.ideas.get_idea(idea_id) + if not idea: + return False + + # Get generation + gen = await self.dao.generations.get_generation(generation_id) + if not gen: + return False + + # Link + gen.idea_id = idea_id + gen.updated_at = datetime.now() + await self.dao.generations.update_generation(gen) + return True + + async def remove_generation_from_idea(self, idea_id: str, generation_id: str) -> bool: + # Verify idea exists (optional, but good for validation) + idea = await self.dao.ideas.get_idea(idea_id) + if not idea: + return False + + # Get generation + gen = await self.dao.generations.get_generation(generation_id) + if not gen: + return False + + # Unlink only if currently linked to this idea + if gen.idea_id == idea_id: + gen.idea_id = None + gen.updated_at = datetime.now() + await self.dao.generations.update_generation(gen) + return True + + return False + diff --git a/models/Idea.py b/models/Idea.py index 0a7ebb1..305ecc2 100644 --- a/models/Idea.py +++ b/models/Idea.py @@ -5,6 +5,7 @@ from pydantic import BaseModel, Field class Idea(BaseModel): id: Optional[str] = None name: str = "New Idea" + description: Optional[str] = None project_id: str created_by: str # User ID is_deleted: bool = False diff --git a/repos/idea_repo.py b/repos/idea_repo.py index 6ea79f9..bb65e0e 100644 --- a/repos/idea_repo.py +++ b/repos/idea_repo.py @@ -37,3 +37,17 @@ class IdeaRepo: {"$set": {"is_deleted": True}} ) return res.modified_count > 0 + + async def update_idea(self, idea: Idea) -> bool: + if not idea.id or not ObjectId.is_valid(idea.id): + return False + + idea_dict = idea.model_dump() + if "id" in idea_dict: + del idea_dict["id"] + + res = await self.collection.update_one( + {"_id": ObjectId(idea.id)}, + {"$set": idea_dict} + ) + return res.modified_count > 0 diff --git a/tests/test_idea.py b/tests/test_idea.py index 18fe1ee..fb864f9 100644 --- a/tests/test_idea.py +++ b/tests/test_idea.py @@ -27,14 +27,23 @@ async def test_idea_flow(): print("Creating idea...") user_id = "test_user_123" project_id = "test_project_abc" - idea = await service.create_idea("My Test Idea", project_id, user_id) + idea = await service.create_idea("My Test Idea", "Initial Description", project_id, user_id) print(f"Idea created: {idea.id} - {idea.name}") - # 2. Add Generation linked to Idea + # 2. Update Idea + print("Updating idea...") + updated_idea = await service.update_idea(idea.id, description="Updated description") + print(f"Idea updated: {updated_idea.description}") + if updated_idea.description == "Updated description": + print("✅ Idea update successful") + else: + print("❌ Idea update FAILED") + + # 3. Add Generation linked to Idea print("Creating generation linked to idea...") gen = Generation( prompt="idea generation 1", - idea_id=idea.id, + # idea_id=idea.id, <-- Intentionally NOT linking initially to test linking method project_id=project_id, created_by=user_id, aspect_ratio=AspectRatios.NINESIXTEEN, @@ -42,15 +51,23 @@ async def test_idea_flow(): assets_list=[] ) gen_id = await dao.generations.create_generation(gen) - print(f"Created linked generation: {gen_id}") + print(f"Created generation: {gen_id}") + + # Link generation to idea + print("Linking generation to idea...") + success = await service.add_generation_to_idea(idea.id, gen_id) + if success: + print("✅ Linking successful") + else: + print("❌ Linking FAILED") # Debug: Check if generation was saved with idea_id saved_gen = await dao.generations.collection.find_one({"_id": ObjectId(gen_id)}) print(f"DEBUG: Saved Generation in DB idea_id: {saved_gen.get('idea_id')}") - # 3. Fetch Generations for Idea (Verify filtering and ordering) + # 4. Fetch Generations for Idea (Verify filtering and ordering) print("Fetching generations for idea...") - gens = await dao.generations.get_generations(idea_id=idea.id) + gens = await service.dao.generations.get_generations(idea_id=idea.id) # using repo directly as service might return wrapper print(f"Found {len(gens)} generations in idea") if len(gens) == 1 and gens[0].id == gen_id: @@ -58,7 +75,7 @@ async def test_idea_flow(): else: print("❌ Generation retrieval FAILED") - # 4. Fetch Ideas for Project + # 5. Fetch Ideas for Project ideas = await service.get_ideas(project_id) print(f"Found {len(ideas)} ideas for project") From 68a3f529cb066923f5718831c5997d0160bf9302 Mon Sep 17 00:00:00 2001 From: xds Date: Mon, 16 Feb 2026 16:35:26 +0300 Subject: [PATCH 3/3] feat: Enhance idea retrieval to include the latest generation and support user-specific ideas not tied to a project, while also improving asset storage uniqueness and adjusting generation cancellation timeout. --- aiws.py | 2 +- api/__pycache__/dependency.cpython-313.pyc | Bin 2727 -> 2727 bytes api/endpoints/idea_router.py | 16 +++---- api/models/IdeaRequest.py | 5 ++ .../GenerationRequest.cpython-313.pyc | Bin 3790 -> 3790 bytes .../generation_service.cpython-313.pyc | Bin 28063 -> 28063 bytes api/service/idea_service.py | 6 +-- models/Idea.py | 2 +- models/__pycache__/Generation.cpython-313.pyc | Bin 3377 -> 3377 bytes repos/__pycache__/assets_repo.cpython-313.pyc | Bin 14845 -> 15403 bytes repos/__pycache__/dao.cpython-313.pyc | Bin 1576 -> 1576 bytes .../generation_repo.cpython-313.pyc | Bin 7404 -> 7404 bytes repos/assets_repo.py | 19 +++++--- repos/generation_repo.py | 2 +- repos/idea_repo.py | 45 ++++++++++++++---- 15 files changed, 67 insertions(+), 30 deletions(-) diff --git a/aiws.py b/aiws.py index 5214dba..e08bdda 100644 --- a/aiws.py +++ b/aiws.py @@ -130,7 +130,7 @@ async def start_scheduler(service: GenerationService): break except Exception as e: logger.error(f"Scheduler error: {e}") - await asyncio.sleep(600) # Check every 10 minutes + await asyncio.sleep(60) # Check every 10 minutes # --- LIFESPAN (Запуск FastAPI + Bot) --- @asynccontextmanager diff --git a/api/__pycache__/dependency.cpython-313.pyc b/api/__pycache__/dependency.cpython-313.pyc index 6e621584864b40f6e4e55425c6c032fd98273407..21db54a428147dd2606d1e4399ebe3c36848959a 100644 GIT binary patch delta 20 acmZ23x?Gg|GcPX}0}y0dPuj>mhYJ8V*ab5H delta 20 acmZ23x?Gg|GcPX}0}v#ZOx(ylhYJ8W@daZ5 diff --git a/api/endpoints/idea_router.py b/api/endpoints/idea_router.py index f6ceb89..a9b612d 100644 --- a/api/endpoints/idea_router.py +++ b/api/endpoints/idea_router.py @@ -6,34 +6,30 @@ from api.service.idea_service import IdeaService from api.service.generation_service import GenerationService from models.Idea import Idea from api.models.GenerationRequest import GenerationResponse, GenerationsResponse -from api.models.IdeaRequest import IdeaCreateRequest, IdeaUpdateRequest +from api.models.IdeaRequest import IdeaCreateRequest, IdeaUpdateRequest, IdeaResponse router = APIRouter(prefix="/api/ideas", tags=["ideas"]) @router.post("", response_model=Idea) async def create_idea( request: IdeaCreateRequest, - project_id: str = Depends(get_project_id), + project_id: Optional[str] = Depends(get_project_id), current_user: dict = Depends(get_current_user), idea_service: IdeaService = Depends(get_idea_service) ): - if not project_id and not request.project_id: - raise HTTPException(status_code=400, detail="Project ID header is required") - pid = project_id or request.project_id return await idea_service.create_idea(request.name, request.description, pid, str(current_user["_id"])) -@router.get("", response_model=List[Idea]) +@router.get("", response_model=List[IdeaResponse]) async def get_ideas( - project_id: str = Depends(get_project_id), + project_id: Optional[str] = Depends(get_project_id), limit: int = 20, offset: int = 0, + current_user: dict = Depends(get_current_user), idea_service: IdeaService = Depends(get_idea_service) ): - if not project_id: - raise HTTPException(status_code=400, detail="Project ID header is required") - return await idea_service.get_ideas(project_id, limit, offset) + return await idea_service.get_ideas(project_id, str(current_user["_id"]), limit, offset) @router.get("/{idea_id}", response_model=Idea) async def get_idea( diff --git a/api/models/IdeaRequest.py b/api/models/IdeaRequest.py index 773f38c..82138f7 100644 --- a/api/models/IdeaRequest.py +++ b/api/models/IdeaRequest.py @@ -1,5 +1,7 @@ from typing import Optional from pydantic import BaseModel +from models.Idea import Idea +from api.models.GenerationRequest import GenerationResponse class IdeaCreateRequest(BaseModel): name: str @@ -9,3 +11,6 @@ class IdeaCreateRequest(BaseModel): class IdeaUpdateRequest(BaseModel): name: Optional[str] = None description: Optional[str] = None + +class IdeaResponse(Idea): + last_generation: Optional[GenerationResponse] = None diff --git a/api/models/__pycache__/GenerationRequest.cpython-313.pyc b/api/models/__pycache__/GenerationRequest.cpython-313.pyc index c74e8026b60fb9d7c8181a37939b3cd5644ee0f5..66faad5b2906050223630322dc53f65705b1b5ea 100644 GIT binary patch delta 20 acmX>ndrp@7GcPX}0}y0dPuj?Rm=6Fy6a~Bh delta 20 acmX>ndrp@7GcPX}0}#ZPOx(zQm=6Fz5e4V~ diff --git a/api/service/__pycache__/generation_service.cpython-313.pyc b/api/service/__pycache__/generation_service.cpython-313.pyc index 1d495681a66f71ea4f7d1f469ce9454fa7e1e73e..431481c87b5173083e1f1b12b4f28e8ada167fb5 100644 GIT binary patch delta 22 ccmbP#n{obaM()qNyj%=GkZC<>BlnbC09R-S3;+NC delta 22 ccmbP#n{obaM()qNyj%=G(AhX~BlnbC09oe;aR2}S diff --git a/api/service/idea_service.py b/api/service/idea_service.py index 89c430f..e154564 100644 --- a/api/service/idea_service.py +++ b/api/service/idea_service.py @@ -7,14 +7,14 @@ class IdeaService: def __init__(self, dao: DAO): self.dao = dao - async def create_idea(self, name: str, description: Optional[str], project_id: str, user_id: str) -> Idea: + async def create_idea(self, name: str, description: Optional[str], project_id: Optional[str], user_id: str) -> Idea: idea = Idea(name=name, description=description, project_id=project_id, created_by=user_id) idea_id = await self.dao.ideas.create_idea(idea) idea.id = idea_id return idea - async def get_ideas(self, project_id: str, limit: int = 20, offset: int = 0) -> List[Idea]: - return await self.dao.ideas.get_ideas(project_id, limit, offset) + async def get_ideas(self, project_id: Optional[str], user_id: str, limit: int = 20, offset: int = 0) -> List[dict]: + return await self.dao.ideas.get_ideas(project_id, user_id, limit, offset) async def get_idea(self, idea_id: str) -> Optional[Idea]: return await self.dao.ideas.get_idea(idea_id) diff --git a/models/Idea.py b/models/Idea.py index 305ecc2..4a2aeab 100644 --- a/models/Idea.py +++ b/models/Idea.py @@ -6,7 +6,7 @@ class Idea(BaseModel): id: Optional[str] = None name: str = "New Idea" description: Optional[str] = None - project_id: str + project_id: Optional[str] = None created_by: str # User ID is_deleted: bool = False created_at: datetime = Field(default_factory=datetime.now) diff --git a/models/__pycache__/Generation.cpython-313.pyc b/models/__pycache__/Generation.cpython-313.pyc index 189b987340a052776af22e9a4094f153bf176a95..0e828bbdfd4361520f99942786c37645c59b38ee 100644 GIT binary patch delta 20 acmdlewNZ-uGcPX}0}y0dPuj?>#tQ&B9|Yb2 delta 20 acmdlewNZ-uGcPX}0}w=(Ox(z=#tQ&C00jmB diff --git a/repos/__pycache__/assets_repo.cpython-313.pyc b/repos/__pycache__/assets_repo.cpython-313.pyc index 60c530c6293ff21aea8fb8b3a33b9fba6eef626e..b1af289b1fd76521653ff03c1c012f7882ee0bfa 100644 GIT binary patch delta 4543 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 delta 3978 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 delta 20 acmZ3%vx0~FGcPX}0}#ZQOx(yV%LV{8!vxL% diff --git a/repos/__pycache__/generation_repo.cpython-313.pyc b/repos/__pycache__/generation_repo.cpython-313.pyc index 235953dbecef798df2f695734957d0627cf06ff2..767e94e867a88a0123484f22935487bee0c59d39 100644 GIT binary patch delta 36 qcmaE3`Nop>GcPX}0}x2qP0BRh$lEB%$hx^-GJuIudGiBl9!3DfF$vuO delta 36 qcmaE3`Nop>GcPX}0}$kvOw2Ud$lEB%XtTLrGJuKEc=H2k9!3Dup$e}6 diff --git a/repos/assets_repo.py b/repos/assets_repo.py index deaeac1..651b8cb 100644 --- a/repos/assets_repo.py +++ b/repos/assets_repo.py @@ -1,6 +1,7 @@ from typing import List, Optional import logging from bson import ObjectId +from uuid import uuid4 from motor.motor_asyncio import AsyncIOMotorClient from models.Asset import Asset @@ -19,7 +20,8 @@ class AssetsRepo: # Main data if asset.data: ts = int(asset.created_at.timestamp()) - object_name = f"{asset.type.value}/{ts}_{asset.name}" + uid = uuid4().hex[:8] + object_name = f"{asset.type.value}/{ts}_{uid}_{asset.name}" uploaded = await self.s3.upload_file(object_name, asset.data) if uploaded: @@ -32,7 +34,8 @@ class AssetsRepo: # Thumbnail if asset.thumbnail: ts = int(asset.created_at.timestamp()) - thumb_name = f"{asset.type.value}/thumbs/{ts}_{asset.name}_thumb.jpg" + uid = uuid4().hex[:8] + thumb_name = f"{asset.type.value}/thumbs/{ts}_{uid}_{asset.name}_thumb.jpg" uploaded_thumb = await self.s3.upload_file(thumb_name, asset.thumbnail) if uploaded_thumb: @@ -134,7 +137,8 @@ class AssetsRepo: if self.s3: if asset.data: ts = int(asset.created_at.timestamp()) - object_name = f"{asset.type.value}/{ts}_{asset.name}" + uid = uuid4().hex[:8] + object_name = f"{asset.type.value}/{ts}_{uid}_{asset.name}" if await self.s3.upload_file(object_name, asset.data): asset.minio_object_name = object_name asset.minio_bucket = self.s3.bucket_name @@ -142,7 +146,8 @@ class AssetsRepo: if asset.thumbnail: ts = int(asset.created_at.timestamp()) - thumb_name = f"{asset.type.value}/thumbs/{ts}_{asset.name}_thumb.jpg" + uid = uuid4().hex[:8] + thumb_name = f"{asset.type.value}/thumbs/{ts}_{uid}_{asset.name}_thumb.jpg" if await self.s3.upload_file(thumb_name, asset.thumbnail): asset.minio_thumbnail_object_name = thumb_name asset.thumbnail = None @@ -216,7 +221,8 @@ class AssetsRepo: created_at = doc.get("created_at") ts = int(created_at.timestamp()) if created_at else 0 - object_name = f"{type_}/{ts}_{asset_id}_{name}" + uid = uuid4().hex[:8] + object_name = f"{type_}/{ts}_{uid}_{asset_id}_{name}" if await self.s3.upload_file(object_name, data): await self.collection.update_one( {"_id": asset_id}, @@ -243,7 +249,8 @@ class AssetsRepo: created_at = doc.get("created_at") ts = int(created_at.timestamp()) if created_at else 0 - thumb_name = f"{type_}/thumbs/{ts}_{asset_id}_{name}_thumb.jpg" + uid = uuid4().hex[:8] + thumb_name = f"{type_}/thumbs/{ts}_{uid}_{asset_id}_{name}_thumb.jpg" if await self.s3.upload_file(thumb_name, thumb): await self.collection.update_one( {"_id": asset_id}, diff --git a/repos/generation_repo.py b/repos/generation_repo.py index ab1ab3d..f77ceee 100644 --- a/repos/generation_repo.py +++ b/repos/generation_repo.py @@ -98,7 +98,7 @@ class GenerationRepo: generations.append(Generation(**generation)) return generations - async def cancel_stale_generations(self, timeout_minutes: int = 60) -> int: + async def cancel_stale_generations(self, timeout_minutes: int = 5) -> int: cutoff_time = datetime.now(UTC) - timedelta(minutes=timeout_minutes) res = await self.collection.update_many( { diff --git a/repos/idea_repo.py b/repos/idea_repo.py index bb65e0e..4271018 100644 --- a/repos/idea_repo.py +++ b/repos/idea_repo.py @@ -20,14 +20,43 @@ class IdeaRepo: return Idea(**res) return None - async def get_ideas(self, project_id: str, limit: int = 20, offset: int = 0) -> List[Idea]: - filter = {"project_id": project_id, "is_deleted": False} - res = await self.collection.find(filter).sort("updated_at", -1).skip(offset).limit(limit).to_list(None) - ideas = [] - for doc in res: - doc["id"] = str(doc.pop("_id")) - ideas.append(Idea(**doc)) - return ideas + async def get_ideas(self, project_id: Optional[str], user_id: str, limit: int = 20, offset: int = 0) -> List[dict]: + if project_id: + match_stage = {"project_id": project_id, "is_deleted": False} + else: + match_stage = {"created_by": user_id, "project_id": None, "is_deleted": False} + + pipeline = [ + {"$match": match_stage}, + {"$sort": {"updated_at": -1}}, + {"$skip": offset}, + {"$limit": limit}, + # Add string id field for lookup + {"$addFields": {"str_id": {"$toString": "$_id"}}}, + # Lookup generations + { + "$lookup": { + "from": "generations", + "let": {"idea_id": "$str_id"}, + "pipeline": [ + {"$match": {"$expr": {"$eq": ["$idea_id", "$$idea_id"]}}}, + {"$sort": {"created_at": -1}}, # Ensure we get the latest + {"$limit": 1} + ], + "as": "generations" + } + }, + # Unwind generations array (preserve ideas without generations) + {"$unwind": {"path": "$generations", "preserveNullAndEmptyArrays": True}}, + # Rename for clarity + {"$addFields": { + "last_generation": "$generations", + "id": "$str_id" + }}, + {"$project": {"generations": 0, "str_id": 0, "_id": 0}} + ] + + return await self.collection.aggregate(pipeline).to_list(None) async def delete_idea(self, idea_id: str) -> bool: if not ObjectId.is_valid(idea_id):