From 5e7dc19bf3dc23b3a3175ad6a0f56ad6780f52d2 Mon Sep 17 00:00:00 2001 From: xds Date: Sun, 15 Feb 2026 12:42:15 +0300 Subject: [PATCH] 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 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 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")