diff --git a/src/repo_registry/core/service.py b/src/repo_registry/core/service.py index a0cf827..989793f 100644 --- a/src/repo_registry/core/service.py +++ b/src/repo_registry/core/service.py @@ -200,6 +200,62 @@ class RegistryService: self.store.update_repository_status(repository_id, "reviewing") return self.store.get_candidate_graph(repository_id, analysis_run_id) + def edit_candidate_ability( + self, + repository_id: int, + analysis_run_id: int, + candidate_ability_id: int, + *, + name: str, + description: str, + confidence: float, + notes: str = "", + ) -> CandidateGraph: + self.store.update_candidate_ability( + repository_id, + analysis_run_id, + candidate_ability_id, + name=name, + description=description, + confidence=confidence, + ) + self.store.create_review_decision( + repository_id, + analysis_run_id, + action="edit_candidate_ability", + notes=notes, + ) + self.store.update_repository_status(repository_id, "reviewing") + return self.store.get_candidate_graph(repository_id, analysis_run_id) + + def edit_candidate_capability( + self, + repository_id: int, + analysis_run_id: int, + candidate_capability_id: int, + *, + name: str, + description: str, + confidence: float, + notes: str = "", + ) -> CandidateGraph: + self.store.update_candidate_capability( + repository_id, + analysis_run_id, + candidate_capability_id, + name=name, + description=description, + confidence=confidence, + ) + self.store.create_review_decision( + repository_id, + analysis_run_id, + action="edit_candidate_capability", + notes=notes, + ) + self.store.update_repository_status(repository_id, "reviewing") + return self.store.get_candidate_graph(repository_id, analysis_run_id) + def add_ability( self, repository_id: int, diff --git a/src/repo_registry/storage/sqlite.py b/src/repo_registry/storage/sqlite.py index 0038d77..7118f9b 100644 --- a/src/repo_registry/storage/sqlite.py +++ b/src/repo_registry/storage/sqlite.py @@ -441,6 +441,72 @@ class RegistryStore: (capability_id, repository_id, analysis_run_id), ) + def update_candidate_ability( + self, + repository_id: int, + analysis_run_id: int, + candidate_ability_id: int, + *, + name: str, + description: str, + confidence: float, + ) -> None: + with self.connect() as connection: + cursor = connection.execute( + """ + UPDATE candidate_abilities + SET name = ?, description = ?, confidence = ? + WHERE id = ? AND repository_id = ? AND analysis_run_id = ? + """, + ( + name, + description, + confidence, + candidate_ability_id, + repository_id, + analysis_run_id, + ), + ) + if cursor.rowcount == 0: + raise NotFoundError( + "candidate ability " + f"{candidate_ability_id} was not found for repository " + f"{repository_id} analysis run {analysis_run_id}" + ) + + def update_candidate_capability( + self, + repository_id: int, + analysis_run_id: int, + candidate_capability_id: int, + *, + name: str, + description: str, + confidence: float, + ) -> None: + with self.connect() as connection: + cursor = connection.execute( + """ + UPDATE candidate_capabilities + SET name = ?, description = ?, confidence = ? + WHERE id = ? AND repository_id = ? AND analysis_run_id = ? + """, + ( + name, + description, + confidence, + candidate_capability_id, + repository_id, + analysis_run_id, + ), + ) + if cursor.rowcount == 0: + raise NotFoundError( + "candidate capability " + f"{candidate_capability_id} was not found for repository " + f"{repository_id} analysis run {analysis_run_id}" + ) + def create_review_decision( self, repository_id: int, diff --git a/src/repo_registry/web_api/app.py b/src/repo_registry/web_api/app.py index 1d25000..6071278 100644 --- a/src/repo_registry/web_api/app.py +++ b/src/repo_registry/web_api/app.py @@ -77,6 +77,13 @@ class CandidateRejection(BaseModel): notes: str = "" +class CandidateEdit(BaseModel): + name: str + description: str = "" + confidence: float = Field(default=0.5, ge=0.0, le=1.0) + notes: str = "" + + app = FastAPI(title="Repository Ability Registry", version="0.1.0") @@ -217,6 +224,54 @@ def reject_candidate_ability( raise HTTPException(status_code=404, detail=str(exc)) from exc +@app.patch( + "/repos/{repository_id}/analysis-runs/{analysis_run_id}" + "/candidate-abilities/{candidate_ability_id}" +) +def edit_candidate_ability( + repository_id: int, + analysis_run_id: int, + candidate_ability_id: int, + payload: CandidateEdit, + service: RegistryService = Depends(get_service), +) -> dict[str, object]: + try: + return asdict( + service.edit_candidate_ability( + repository_id, + analysis_run_id, + candidate_ability_id, + **payload.model_dump(), + ) + ) + except NotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + +@app.patch( + "/repos/{repository_id}/analysis-runs/{analysis_run_id}" + "/candidate-capabilities/{candidate_capability_id}" +) +def edit_candidate_capability( + repository_id: int, + analysis_run_id: int, + candidate_capability_id: int, + payload: CandidateEdit, + service: RegistryService = Depends(get_service), +) -> dict[str, object]: + try: + return asdict( + service.edit_candidate_capability( + repository_id, + analysis_run_id, + candidate_capability_id, + **payload.model_dump(), + ) + ) + except NotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + @app.post("/repos/{repository_id}/abilities", status_code=201) def create_ability( repository_id: int, diff --git a/src/repo_registry/web_ui/views.py b/src/repo_registry/web_ui/views.py index 9618d2c..3bdfbf3 100644 --- a/src/repo_registry/web_ui/views.py +++ b/src/repo_registry/web_ui/views.py @@ -330,13 +330,72 @@ def reject_candidate_ability_from_form( ) +@router.post( + "/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}" + "/candidate-abilities/{candidate_ability_id}/edit" +) +def edit_candidate_ability_from_form( + repository_id: int, + analysis_run_id: int, + candidate_ability_id: int, + name: str = Form(...), + description: str = Form(""), + confidence: float = Form(...), + service: RegistryService = Depends(get_service), +) -> RedirectResponse: + service.edit_candidate_ability( + repository_id, + analysis_run_id, + candidate_ability_id, + name=name, + description=description, + confidence=confidence, + notes="Edited from web UI", + ) + return RedirectResponse( + f"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}", + status_code=303, + ) + + +@router.post( + "/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}" + "/candidate-capabilities/{candidate_capability_id}/edit" +) +def edit_candidate_capability_from_form( + repository_id: int, + analysis_run_id: int, + candidate_capability_id: int, + name: str = Form(...), + description: str = Form(""), + confidence: float = Form(...), + service: RegistryService = Depends(get_service), +) -> RedirectResponse: + service.edit_candidate_capability( + repository_id, + analysis_run_id, + candidate_capability_id, + name=name, + description=description, + confidence=confidence, + notes="Edited from web UI", + ) + return RedirectResponse( + f"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}", + status_code=303, + ) + + def render_candidate_graph(graph: dict, repository_id: int, analysis_run_id: int) -> str: abilities = graph.get("abilities", []) if not abilities: return '
No candidates generated.
' items = [] for ability in abilities: - capabilities = "".join(render_candidate_capability(capability) for capability in ability["capabilities"]) + capabilities = "".join( + render_candidate_capability(capability, repository_id, analysis_run_id) + for capability in ability["capabilities"] + ) items.append( f"""{escape(ability['description'])}
+ {render_candidate_edit_form('candidate-abilities', ability, repository_id, analysis_run_id)} {render_sources(ability['source_refs'])}{escape(capability['description'])}
+ {render_candidate_edit_form('candidate-capabilities', capability, repository_id, analysis_run_id)} {render_sources(capability['source_refs'])}