diff --git a/src/repo_registry/core/service.py b/src/repo_registry/core/service.py index 989793f..5f4eea8 100644 --- a/src/repo_registry/core/service.py +++ b/src/repo_registry/core/service.py @@ -200,6 +200,72 @@ class RegistryService: self.store.update_repository_status(repository_id, "reviewing") return self.store.get_candidate_graph(repository_id, analysis_run_id) + def reject_candidate_capability( + self, + repository_id: int, + analysis_run_id: int, + candidate_capability_id: int, + *, + notes: str = "", + ) -> CandidateGraph: + self.store.reject_candidate_capability( + repository_id, + analysis_run_id, + candidate_capability_id, + ) + self.store.create_review_decision( + repository_id, + analysis_run_id, + action="reject_candidate_capability", + notes=notes, + ) + self.store.update_repository_status(repository_id, "reviewing") + return self.store.get_candidate_graph(repository_id, analysis_run_id) + + def reject_candidate_feature( + self, + repository_id: int, + analysis_run_id: int, + candidate_feature_id: int, + *, + notes: str = "", + ) -> CandidateGraph: + self.store.reject_candidate_feature( + repository_id, + analysis_run_id, + candidate_feature_id, + ) + self.store.create_review_decision( + repository_id, + analysis_run_id, + action="reject_candidate_feature", + notes=notes, + ) + self.store.update_repository_status(repository_id, "reviewing") + return self.store.get_candidate_graph(repository_id, analysis_run_id) + + def reject_candidate_evidence( + self, + repository_id: int, + analysis_run_id: int, + candidate_evidence_id: int, + *, + notes: str = "", + ) -> CandidateGraph: + self.store.reject_candidate_evidence( + repository_id, + analysis_run_id, + candidate_evidence_id, + ) + self.store.create_review_decision( + repository_id, + analysis_run_id, + action="reject_candidate_evidence", + notes=notes, + ) + 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, diff --git a/src/repo_registry/storage/sqlite.py b/src/repo_registry/storage/sqlite.py index 7118f9b..b2969e0 100644 --- a/src/repo_registry/storage/sqlite.py +++ b/src/repo_registry/storage/sqlite.py @@ -441,6 +441,75 @@ class RegistryStore: (capability_id, repository_id, analysis_run_id), ) + def reject_candidate_capability( + self, + repository_id: int, + analysis_run_id: int, + candidate_capability_id: int, + ) -> None: + with self.connect() as connection: + cursor = connection.execute( + """ + UPDATE candidate_capabilities + SET status = 'rejected' + WHERE id = ? + AND repository_id = ? + AND analysis_run_id = ? + AND status = 'candidate' + """, + (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}" + ) + connection.execute( + """ + UPDATE candidate_features + SET status = 'rejected' + WHERE capability_id = ? AND repository_id = ? AND analysis_run_id = ? + """, + (candidate_capability_id, repository_id, analysis_run_id), + ) + connection.execute( + """ + UPDATE candidate_evidence + SET status = 'rejected' + WHERE capability_id = ? AND repository_id = ? AND analysis_run_id = ? + """, + (candidate_capability_id, repository_id, analysis_run_id), + ) + + def reject_candidate_feature( + self, + repository_id: int, + analysis_run_id: int, + candidate_feature_id: int, + ) -> None: + self._reject_candidate_leaf( + table="candidate_features", + label="candidate feature", + repository_id=repository_id, + analysis_run_id=analysis_run_id, + candidate_id=candidate_feature_id, + ) + + def reject_candidate_evidence( + self, + repository_id: int, + analysis_run_id: int, + candidate_evidence_id: int, + ) -> None: + self._reject_candidate_leaf( + table="candidate_evidence", + label="candidate evidence", + repository_id=repository_id, + analysis_run_id=analysis_run_id, + candidate_id=candidate_evidence_id, + ) + def update_candidate_ability( self, repository_id: int, @@ -507,6 +576,33 @@ class RegistryStore: f"{repository_id} analysis run {analysis_run_id}" ) + def _reject_candidate_leaf( + self, + *, + table: str, + label: str, + repository_id: int, + analysis_run_id: int, + candidate_id: int, + ) -> None: + with self.connect() as connection: + cursor = connection.execute( + f""" + UPDATE {table} + SET status = 'rejected' + WHERE id = ? + AND repository_id = ? + AND analysis_run_id = ? + AND status = 'candidate' + """, + (candidate_id, repository_id, analysis_run_id), + ) + if cursor.rowcount == 0: + raise NotFoundError( + f"{label} {candidate_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 6071278..9f369d7 100644 --- a/src/repo_registry/web_api/app.py +++ b/src/repo_registry/web_api/app.py @@ -224,6 +224,78 @@ def reject_candidate_ability( raise HTTPException(status_code=404, detail=str(exc)) from exc +@app.post( + "/repos/{repository_id}/analysis-runs/{analysis_run_id}" + "/candidate-capabilities/{candidate_capability_id}/reject" +) +def reject_candidate_capability( + repository_id: int, + analysis_run_id: int, + candidate_capability_id: int, + payload: CandidateRejection, + service: RegistryService = Depends(get_service), +) -> dict[str, object]: + try: + return asdict( + service.reject_candidate_capability( + repository_id, + analysis_run_id, + candidate_capability_id, + notes=payload.notes, + ) + ) + except NotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + +@app.post( + "/repos/{repository_id}/analysis-runs/{analysis_run_id}" + "/candidate-features/{candidate_feature_id}/reject" +) +def reject_candidate_feature( + repository_id: int, + analysis_run_id: int, + candidate_feature_id: int, + payload: CandidateRejection, + service: RegistryService = Depends(get_service), +) -> dict[str, object]: + try: + return asdict( + service.reject_candidate_feature( + repository_id, + analysis_run_id, + candidate_feature_id, + notes=payload.notes, + ) + ) + except NotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + +@app.post( + "/repos/{repository_id}/analysis-runs/{analysis_run_id}" + "/candidate-evidence/{candidate_evidence_id}/reject" +) +def reject_candidate_evidence( + repository_id: int, + analysis_run_id: int, + candidate_evidence_id: int, + payload: CandidateRejection, + service: RegistryService = Depends(get_service), +) -> dict[str, object]: + try: + return asdict( + service.reject_candidate_evidence( + repository_id, + analysis_run_id, + candidate_evidence_id, + notes=payload.notes, + ) + ) + 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-abilities/{candidate_ability_id}" diff --git a/src/repo_registry/web_ui/views.py b/src/repo_registry/web_ui/views.py index 3bdfbf3..92cac0f 100644 --- a/src/repo_registry/web_ui/views.py +++ b/src/repo_registry/web_ui/views.py @@ -330,6 +330,72 @@ def reject_candidate_ability_from_form( ) +@router.post( + "/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}" + "/candidate-capabilities/{candidate_capability_id}/reject" +) +def reject_candidate_capability_from_form( + repository_id: int, + analysis_run_id: int, + candidate_capability_id: int, + service: RegistryService = Depends(get_service), +) -> RedirectResponse: + service.reject_candidate_capability( + repository_id, + analysis_run_id, + candidate_capability_id, + notes="Rejected 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-features/{candidate_feature_id}/reject" +) +def reject_candidate_feature_from_form( + repository_id: int, + analysis_run_id: int, + candidate_feature_id: int, + service: RegistryService = Depends(get_service), +) -> RedirectResponse: + service.reject_candidate_feature( + repository_id, + analysis_run_id, + candidate_feature_id, + notes="Rejected 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-evidence/{candidate_evidence_id}/reject" +) +def reject_candidate_evidence_from_form( + repository_id: int, + analysis_run_id: int, + candidate_evidence_id: int, + service: RegistryService = Depends(get_service), +) -> RedirectResponse: + service.reject_candidate_evidence( + repository_id, + analysis_run_id, + candidate_evidence_id, + notes="Rejected 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-abilities/{candidate_ability_id}/edit" @@ -460,11 +526,11 @@ def render_candidate_capability( analysis_run_id: int, ) -> str: features = "".join( - f'
{escape(capability['description'])}
{render_candidate_edit_form('candidate-capabilities', capability, repository_id, analysis_run_id)} {render_sources(capability['source_refs'])} @@ -483,6 +550,57 @@ def render_candidate_capability( """ +def render_candidate_feature( + feature: dict, + repository_id: int, + analysis_run_id: int, +) -> str: + return f""" +