Skip to content

Add Candidate Claim Management Dashboard#5006

Open
rudransh-shrivastava wants to merge 37 commits into
OWASP:feature/bod-candidate-transparencyfrom
rudransh-shrivastava:feature/bod-claim-dash-cherry-claim
Open

Add Candidate Claim Management Dashboard#5006
rudransh-shrivastava wants to merge 37 commits into
OWASP:feature/bod-candidate-transparencyfrom
rudransh-shrivastava:feature/bod-claim-dash-cherry-claim

Conversation

@rudransh-shrivastava

Copy link
Copy Markdown
Collaborator

Proposed change

Resolves #5004

This PR adds a dashboard for managing claims. The redirects to evidence pages return a 404 as this is the first (1/2) PR, the second PR should fill those gaps.

Checklist

  • Required: I followed the contributing workflow
  • Required: I verified that my code works as intended and resolves the issue as described
  • Required: I ran make check-test locally: all warnings addressed, tests passed
  • I used AI for code, documentation, tests, or communication related to this PR

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a full-stack candidate claim management dashboard. Backend mutations now return the affected claim node in ClaimResult, a has_evidence GraphQL field and evidence_exists queryset annotation are introduced. Frontend adds CandidateClaimsPage, CreateClaimPage, EditClaimPage, and ClaimDetailsPage pages, shared ClaimForm, DropdownActions, and ClaimActions components, GraphQL query/mutation documents, a Claim TypeScript type, E2E auth helpers, and comprehensive unit and E2E tests.

Changes

Candidate Claim Management Dashboard

Layer / File(s) Summary
Backend ClaimResult.claim, has_evidence field, and evidence annotation
backend/apps/owasp/api/internal/mutations/board_candidate_claim.py, backend/apps/owasp/api/internal/nodes/board_candidate_claim.py, backend/apps/owasp/api/internal/queries/board_candidate_claim.py, backend/tests/unit/apps/owasp/api/internal/mutations/board_candidate_claim_test.py, backend/tests/unit/apps/owasp/api/internal/nodes/board_candidate_claim_test.py, backend/tests/unit/apps/owasp/api/internal/queries/board_candidate_claim_test.py
ClaimResult gains a nullable claim field populated by all five mutation success paths. BoardCandidateClaimNode gains a has_evidence resolver using a precomputed evidence_exists annotation or a fallback .exists() query. The board_candidate_claims resolver annotates the queryset with an Exists subquery over BoardCandidateClaimEvidence. All corresponding backend tests are updated.
Frontend GraphQL queries, mutations, and Claim type
frontend/src/types/claim.ts, frontend/src/server/mutations/claimMutations.ts, frontend/src/server/queries/boardQueries.ts, frontend/src/server/queries/claimQueries.ts, frontend/src/server/queries/evidenceQueries.ts
Six mutation documents (create/update/discard/submit/withdraw/reorder), four claim query documents (single claim, claims list, candidate+claims, claim+evidences), a GET_CLAIM_EVIDENCES query, a GET_BOARD_CANDIDATE query, and the Claim TypeScript type with ClaimStatusEnum are defined.
ClaimForm, DropdownActions, ClaimActions, and file validators
frontend/src/components/ClaimForm.tsx, frontend/src/components/DropdownActions.tsx, frontend/src/components/ClaimActions.tsx, frontend/src/components/forms/shared/formValidationUtils.ts
ClaimForm provides controlled name/description inputs with client-side and backend validation error display. DropdownActions renders a keyboard-navigable, ARIA-compliant dropdown. ClaimActions wires submit/discard/withdraw mutations with Apollo cache updates and a confirmation modal. Two new file validators (validateFileExtension, validateFileSize) are added.
CandidateCard Manage Claims link and CandidateClaimsPage
frontend/src/app/board/[year]/candidates/page.tsx, frontend/src/app/board/[year]/candidates/[login]/claims/page.tsx
CandidateCard uses useDjangoSession to derive isOwnProfile and conditionally renders a Manage claims link. CandidateClaimsPage fetches claims, maintains local Draft/Approved ordering state, implements handleReorder/handleSave with Apollo cache writes for the reorder mutation, and renders status-grouped sections with reorder controls and an Evidence chip.
CreateClaimPage, EditClaimPage, and ClaimDetailsPage
frontend/src/app/board/[year]/candidates/[login]/claims/create/page.tsx, frontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/edit/page.tsx, frontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/page.tsx
CreateClaimPage queries the candidate, submits the create mutation, and appends the new claim to Apollo cache. EditClaimPage fetches the existing claim, pre-populates ClaimForm, and writes the updated claim back to cache via writeQuery. ClaimDetailsPage fetches a specific claim and its evidences, renders metadata, an Add Evidence action for Draft claims, and embeds ClaimActions.
Frontend unit tests and mock data
frontend/__tests__/mockData/mockClaimData.ts, frontend/__tests__/unit/pages/*, frontend/__tests__/unit/components/ClaimActions.test.tsx, frontend/__tests__/unit/components/DropdownActions.test.tsx, frontend/__tests__/unit/components/ActionButton.test.tsx, frontend/__tests__/unit/components/ProgramForm.test.tsx
Shared mock claim/evidence/candidate fixtures are added. Unit tests for all claim pages assert loading, access-denied, form rendering, mutation invocation, navigation, and toast behavior. ClaimActions, DropdownActions, and ActionButton component tests cover rendering, keyboard navigation, mutation flows, modal behavior, and cache update logic. ProgramForm date tests are refactored to use fireEvent.
E2E auth helpers and claim page E2E tests
e2e/helpers/mockAuthCookies.ts, e2e/helpers/mockDashboardCookies.ts, e2e/helpers/mockClaimAuth.ts, e2e/pages/BoardCandidateClaims.spec.ts, e2e/pages/BoardCandidateClaimDetails.spec.ts
authCookies constant is extracted into mockAuthCookies.ts and reused by mockDashboardCookies and the new mockClaimAuth helper. mockClaimAuth intercepts auth session and GraphQL endpoints with operation-level filtering. E2E test suites cover claims list UI (status sections, evidence badge, empty state, access control, breadcrumbs) and claim details (evidences, add-evidence button, not-found, access-denied, breadcrumbs).

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

Suggested reviewers

  • kasya
  • arkid15r
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main objective of the changeset: adding a candidate claim management dashboard, which is directly reflected in the code additions across backend and frontend components.
Description check ✅ Passed The PR description is clearly related to the changeset, mentioning the dashboard for managing claims (issue #5004) and acknowledging this is part 1 of 2, which aligns with the implementation details provided.
Linked Issues check ✅ Passed The PR successfully implements the core requirement from issue #5004: a dashboard enabling candidates to manage their claims. Backend mutations, GraphQL queries, and comprehensive frontend pages/components for viewing, creating, editing, and managing claims are all present.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the candidate claim management dashboard. Supporting infrastructure (validation utilities, form components, test mocks, auth helpers, and test suites) are all in scope for the feature requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/tests/unit/apps/owasp/api/internal/mutations/board_candidate_claim_test.py (1)

528-555: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Set up return value for objects.create() to match other success tests.

The test verifies result.claim is not None, but unlike test_update_claim_success, test_discard_claim_success, etc., it doesn't set up mock_claim_model.objects.create.return_value. This means when the mutation calls objects.create(), it returns a default MagicMock, not a configured claim. The assertion passes because the MagicMock is not None, but doesn't verify the returned claim is the one that was created. If the mutation code is later changed to not return the claim, this test would still pass.

Align this test with the pattern used in other success tests by setting up the return value and checking object identity.

🔧 Proposed fix to set up return value and check object identity
 `@patch`("apps.owasp.api.internal.mutations.board_candidate_claim.BoardOfDirectors")
 `@patch`("apps.owasp.api.internal.mutations.board_candidate_claim.BoardCandidateClaim")
 def test_create_claim_success(self, mock_claim_model, mock_board_model):
     mock_claim_model.Status = BoardCandidateClaim.Status
     user = MagicMock()
     user.is_authenticated = True
     mock_github_user = MagicMock()
     user.github_user = mock_github_user
     info = _make_info(user)
     input_data = self._make_input_data()

     mock_board = MagicMock()
     mock_candidate = MagicMock()
     mock_board.get_candidate.return_value = mock_candidate
     mock_board_model.objects.get.return_value = mock_board
+
+    claim = MagicMock()
+    mock_claim_model.objects.create.return_value = claim

     mutation = BoardCandidateClaimMutations()
     result = mutation.create_board_candidate_claim(info, input_data)

     mock_board_model.objects.get.assert_called_once_with(year=2025)
     mock_board.get_candidate.assert_called_once_with(login=mock_github_user.login)
     mock_claim_model.objects.create.assert_called_once_with(
         board=mock_board,
         candidate=mock_candidate,
         description=input_data.description,
         name=input_data.name,
     )
     assert result.ok
     assert result.code == "SUCCESS"
-    assert result.claim is not None
+    assert result.claim is claim
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/tests/unit/apps/owasp/api/internal/mutations/board_candidate_claim_test.py`
around lines 528 - 555, In the test_create_claim_success method, set up the
return value for mock_claim_model.objects.create before calling the mutation to
ensure it returns a configured mock claim object. Create a mock claim object,
assign it to mock_claim_model.objects.create.return_value, then verify that
result.claim is the same object (using identity comparison, not just truthiness
check). This aligns the test with the pattern used in other success tests like
test_update_claim_success and test_discard_claim_success, ensuring the test
would fail if the mutation code is later changed to not return the created
claim.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/apps/owasp/api/internal/nodes/board_candidate_claim.py`:
- Around line 32-35: The has_evidence resolver method executes a database query
per claim by calling root.evidences.filter(is_removed=False).exists() each time
it's resolved, causing N+1 query problems when fetching multiple claims. Instead
of querying inside the field resolver, pre-annotate the has_evidence value on
the queryset at the point where claims are fetched (e.g., using Django's
annotate with a Count or Exists expression on the evidences relation filtered by
is_removed=False), then read that pre-computed annotated value directly from the
root object in the has_evidence method instead of executing the filter and
exists call.

In `@e2e/helpers/mockClaimAuth.ts`:
- Around line 15-35: The route mock handler for GraphQL requests currently has a
catch-all fallback that returns success for any operation that is not
SyncDjangoSession, which can mask missing or broken mocks. Instead of the else
block returning success with mockData for all unknown operations, add explicit
handling for each expected operationName and fail fast on unhandled operations
by using route.abort() or throwing an error when an unknown operationName is
encountered. This ensures that any test requesting an unmocked GraphQL operation
will fail rather than silently succeed with default data.

In `@e2e/pages/BoardCandidateClaimDetails.spec.ts`:
- Around line 23-25: The assertion for `Web security experience` on line 24 uses
lowercase casing that does not match the fixture and actual UI output which uses
title case. Update the getByText assertion from `Web security experience` to
`Web Security Experience` to match the expected fixture rendering and ensure the
test passes against the actual page content.

In
`@frontend/src/app/board/`[year]/candidates/[login]/claims/[claimKey]/edit/page.tsx:
- Around line 23-27: The useQuery hook call for GetBoardCandidateClaimDocument
currently only skips the query when claimKey is missing, but does not verify
user ownership before fetching. Update the skip condition to include an
ownership check that compares session?.user?.login with the login parameter, so
the query is skipped if either claimKey is missing or the current user is not
authorized to view this claim.

In `@frontend/src/app/board/`[year]/candidates/[login]/claims/[claimKey]/page.tsx:
- Around line 83-87: The handleAddEvidence function routes to a non-existent
destination which results in a 404 error, breaking the primary call-to-action
for adding evidence. Locate the UI button or element that triggers
handleAddEvidence and apply a conditional to either hide it completely or
disable it (using the disabled prop) until the destination route is properly
implemented. This same approach should also be applied to the button or element
triggering handleEvidenceClick as noted in the comment, ensuring both navigation
paths are protected from routing to undefined destinations.
- Around line 33-37: The useQuery call for GetClaimAndEvidencesDocument
currently skips execution only when claimKey is missing, but it should also
verify session ownership before making the backend request. Modify the skip
condition in the useQuery hook to include additional checks for session
ownership validation (session?.user?.login === login) and syncing status
(!!isSyncing) alongside the existing !claimKey check, so the query only executes
when all conditions are satisfied. Apply the same optimization pattern to any
other similar query calls as indicated in the comment (also at lines 70-74).

In `@frontend/src/app/board/`[year]/candidates/[login]/claims/create/page.tsx:
- Around line 28-33: The useQuery call for GetBoardCandidateDocument is
executing unconditionally and errors are being masked by the isCandidate check,
making network failures indistinguishable from access-denied cases.
Additionally, the query runs before the ownership verification check that occurs
later in the component. Refactor the code to conditionally execute the
GetBoardCandidateDocument query only after confirming route ownership (defer the
useQuery or gate it with a condition based on the ownership check), and add
explicit error handling to capture and display query errors separately from the
access control logic.

In `@frontend/src/app/board/`[year]/candidates/[login]/claims/page.tsx:
- Around line 88-93: The swap operation in this state setter does not validate
that the key actually exists in the prev array before attempting to swap. When
indexOf returns -1 because the key is not found, the down-direction swap will
still execute with invalid indices (swapping position -1 with position 0). Add a
guard check immediately after calculating idx to verify that idx is not -1, and
return prev unchanged if the key is not found in the array.
- Around line 100-129: The reorderClaims mutation call currently shows a success
toast without checking if the mutation actually succeeded at the business logic
level. Modify the code to check the response data from the reorderClaims
mutation for a success indicator (such as an ok field) before displaying the
success toast. If the mutation response indicates failure, either skip the toast
or show an error toast instead to properly reflect the actual operation result.
- Around line 37-40: Add an ownership verification check to the skip condition
of the useQuery call for GetBoardCandidateAndClaimsDocument to gate the claim
fetching on confirmed viewer ownership of the login parameter using the
available session object. Additionally, modify the reorder mutation handling to
check the ok field in the mutation response before conditionally displaying the
success toast instead of showing it unconditionally. Finally, add a guard
condition to check that indexOf does not return -1 before performing array index
operations in the reorder swap logic for down-direction moves to prevent
undefined behavior when a key is missing from the array.

In `@frontend/src/app/board/`[year]/candidates/page.tsx:
- Line 95: Remove the useDjangoSession hook call from the CandidateCard
component and instead call it once at the page level where candidates are
rendered. Pass the session (or the derived isOwnProfile boolean value) as a prop
to each CandidateCard instance. Update the CandidateCard component to accept
this session or isOwnProfile prop and remove the hook invocation, replacing the
hook-based session usage with the prop value. This ensures the hook's side
effects run only once per page instead of once per candidate card.

In `@frontend/src/components/ClaimActions.tsx`:
- Around line 185-193: The textarea input with placeholder="Reason for
withdrawal..." in the withdraw confirmation section lacks an accessible label
for assistive technologies. Add either a proper label element associated with
the textarea (using htmlFor/id) or add an aria-label attribute to the textarea
element to provide a descriptive label that screen readers can announce.

In `@frontend/src/components/ClaimForm.tsx`:
- Around line 89-92: The form inputs are only displaying errors from local
validators but not from the backendErrors state that gets populated when the
extractGraphQLErrors call is executed. Update all form input components to merge
and display both local validation errors and backend errors from the
backendErrors state. This means modifying the error prop or className binding
for each input field to check both the local validators' errors and the
corresponding backendErrors values, ensuring users see inline validation
feedback for both client-side and server-side validation failures. Apply this
fix to all form inputs referenced in the diff and at the additional lines
mentioned (110-110, 122-122).
- Line 121: The onChange handler for the description field at line 121 calls
handleInputChange to update the value but does not mark the field as touched,
causing inconsistent validation feedback compared to the name field. Update the
onChange callback for the description input to also invoke a function that marks
the description field as touched (similar to how the name field is handled),
ensuring both value updates and touched state are properly synchronized when the
user modifies the description.

In `@frontend/src/components/forms/shared/formValidationUtils.ts`:
- Around line 35-41: The validateFileExtension function normalizes the extracted
file extension to lowercase with a leading dot, but does not normalize the
allowedExtensions array before comparison. This causes valid files to be
rejected if callers pass extensions in different formats like 'PDF' (uppercase)
or 'pdf' (without dot). Normalize each element in the allowedExtensions array to
lowercase and ensure each has a leading dot prefix before checking if the ext
value is included in the normalized array.

---

Outside diff comments:
In
`@backend/tests/unit/apps/owasp/api/internal/mutations/board_candidate_claim_test.py`:
- Around line 528-555: In the test_create_claim_success method, set up the
return value for mock_claim_model.objects.create before calling the mutation to
ensure it returns a configured mock claim object. Create a mock claim object,
assign it to mock_claim_model.objects.create.return_value, then verify that
result.claim is the same object (using identity comparison, not just truthiness
check). This aligns the test with the pattern used in other success tests like
test_update_claim_success and test_discard_claim_success, ensuring the test
would fail if the mutation code is later changed to not return the created
claim.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 902c050e-dc42-4614-a490-a026ce318471

📥 Commits

Reviewing files that changed from the base of the PR and between 2eae0aa and 00ae269.

⛔ Files ignored due to path filters (5)
  • frontend/src/types/__generated__/boardQueries.generated.ts is excluded by !**/__generated__/**
  • frontend/src/types/__generated__/claimMutations.generated.ts is excluded by !**/__generated__/**
  • frontend/src/types/__generated__/claimQueries.generated.ts is excluded by !**/__generated__/**
  • frontend/src/types/__generated__/evidenceQueries.generated.ts is excluded by !**/__generated__/**
  • frontend/src/types/__generated__/graphql.ts is excluded by !**/__generated__/**
📒 Files selected for processing (27)
  • backend/apps/owasp/api/internal/mutations/board_candidate_claim.py
  • backend/apps/owasp/api/internal/nodes/board_candidate_claim.py
  • backend/tests/unit/apps/owasp/api/internal/mutations/board_candidate_claim_test.py
  • backend/tests/unit/apps/owasp/api/internal/nodes/board_candidate_claim_test.py
  • e2e/helpers/mockClaimAuth.ts
  • e2e/pages/BoardCandidateClaimDetails.spec.ts
  • e2e/pages/BoardCandidateClaims.spec.ts
  • frontend/__tests__/mockData/mockClaimData.ts
  • frontend/__tests__/unit/pages/BoardCandidatesPage.test.tsx
  • frontend/__tests__/unit/pages/CandidateClaimsPage.test.tsx
  • frontend/__tests__/unit/pages/ClaimDetailsPage.test.tsx
  • frontend/__tests__/unit/pages/CreateClaimPage.test.tsx
  • frontend/__tests__/unit/pages/EditClaimPage.test.tsx
  • frontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/edit/page.tsx
  • frontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/page.tsx
  • frontend/src/app/board/[year]/candidates/[login]/claims/create/page.tsx
  • frontend/src/app/board/[year]/candidates/[login]/claims/page.tsx
  • frontend/src/app/board/[year]/candidates/page.tsx
  • frontend/src/components/ClaimActions.tsx
  • frontend/src/components/ClaimForm.tsx
  • frontend/src/components/DropdownActions.tsx
  • frontend/src/components/forms/shared/formValidationUtils.ts
  • frontend/src/server/mutations/claimMutations.ts
  • frontend/src/server/queries/boardQueries.ts
  • frontend/src/server/queries/claimQueries.ts
  • frontend/src/server/queries/evidenceQueries.ts
  • frontend/src/types/claim.ts

Comment thread backend/apps/owasp/api/internal/nodes/board_candidate_claim.py
Comment thread e2e/helpers/mockClaimAuth.ts
Comment thread e2e/pages/BoardCandidateClaimDetails.spec.ts
Comment thread frontend/src/app/board/[year]/candidates/page.tsx Outdated
Comment thread frontend/src/components/ClaimActions.tsx
Comment thread frontend/src/components/ClaimForm.tsx
Comment thread frontend/src/components/ClaimForm.tsx Outdated
Comment thread frontend/src/components/forms/shared/formValidationUtils.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

19 issues found across 32 files

Confidence score: 2/5

  • In frontend/src/components/ClaimForm.tsx, backend validation errors are stored but not surfaced (and validateName is called without backendErrors.name), so users can hit server-side uniqueness failures like “name already exists” without seeing actionable feedback; this is the highest user-impact risk and should be fixed by wiring backend errors into field validation/rendering before merge.
  • In frontend/src/app/board/[year]/candidates/[login]/claims/page.tsx, save/reorder flows can mislead users: handleSave can report success when mutation ok is false, and reorder logic can swap with next[-1] when indexOf returns -1, risking corrupted ordering state; guard missing keys and gate success messaging on ok/message before merging.
  • In frontend/src/app/board/[year]/candidates/[login]/claims/create/page.tsx, query/network failures are treated as authorization failures, so real backend outages or GraphQL errors appear as Access Denied and mask root cause for users/support; split auth failures from transport/query errors before merge.
  • In frontend/__tests__/mockData/mockClaimData.ts, mock claims/evidences are missing required id fields, reducing test realism and potentially hiding runtime/keying bugs, and backend/apps/owasp/api/internal/nodes/board_candidate_claim.py introduces N+1 query behavior that may degrade list performance; align mocks with GraphQL types and add queryset annotation/prefetch for has_evidence to de-risk correctness and scalability.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="frontend/src/server/queries/evidenceQueries.ts">

<violation number="1" location="frontend/src/server/queries/evidenceQueries.ts:3">
P2: Duplicates existing `GET_CLAIM_AND_EVIDENCES` from `claimQueries.ts`. The evidence sub-query is identical — same fields, same `boardCandidateClaimEvidences` resolver. Two queries fetching the same data creates a maintenance risk: schema or field changes must be synced across both files.</violation>
</file>

<file name="frontend/src/components/DropdownActions.tsx">

<violation number="1" location="frontend/src/components/DropdownActions.tsx:29">
P2: Focus is lost after selecting a menu item — the trigger button should be refocused. Add `triggerButtonRef.current?.focus()` after closing the dropdown.</violation>

<violation number="2" location="frontend/src/components/DropdownActions.tsx:105">
P2: Menu container has `tabIndex={0}`, creating an extra Tab stop. Use `tabIndex={-1}` — focus is managed via roving tabindex on menuitems.</violation>
</file>

<file name="frontend/__tests__/mockData/mockClaimData.ts">

<violation number="1" location="frontend/__tests__/mockData/mockClaimData.ts:1">
P2: Mock claim objects missing `id` field that is part of the GraphQL query response. The generated type `GetBoardCandidateClaimsQuery` includes `id: string` for every claim. If components use `claim.id` (e.g., React key, navigation), tests won't reflect real behavior.</violation>

<violation number="2" location="frontend/__tests__/mockData/mockClaimData.ts:72">
P2: mockSingleClaim missing required `id` field. The GraphQL query response includes `id: string` for the claim — without it, tests using this mock won't accurately represent real API data.</violation>

<violation number="3" location="frontend/__tests__/mockData/mockClaimData.ts:81">
P2: mockEvidences missing required `id` field. The GraphQL query response includes `id: string` for every evidence item. Tests referencing evidence IDs or using them as keys won't match real behavior.</violation>
</file>

<file name="backend/apps/owasp/api/internal/nodes/board_candidate_claim.py">

<violation number="1" location="backend/apps/owasp/api/internal/nodes/board_candidate_claim.py:33">
P2: This resolver executes a DB query per claim node. On list queries (e.g., `boardCandidateClaims`), this introduces N+1 database hits. Consider annotating `has_evidence` on the queryset (e.g., via `strawberry_django.field(annotate={...})`) and reading the pre-computed value here instead of issuing a per-node `exists()` call.</violation>
</file>

<file name="frontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/edit/page.tsx">

<violation number="1" location="frontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/edit/page.tsx:25">
P2: The query executes when `claimKey` is present, before verifying `session?.user?.login === login`. This allows unauthorized sessions to trigger GraphQL requests for claims belonging to other users. Add ownership verification to the `skip` condition: `skip: !claimKey || session?.user?.login !== login`.</violation>
</file>

<file name="frontend/src/app/board/[year]/candidates/page.tsx">

<violation number="1" location="frontend/src/app/board/[year]/candidates/page.tsx:95">
P2: Calling `useDjangoSession()` inside `CandidateCard` creates N hook instances when rendering many candidates. This hook contains session sync logic with side effects (mutations, toasts, sign-out calls) that should only execute once. Consider lifting it to the parent page and passing `isOwnProfile` as a prop.</violation>
</file>

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread frontend/src/components/ClaimForm.tsx
Comment thread frontend/src/app/board/[year]/candidates/[login]/claims/page.tsx Outdated
Comment thread frontend/src/server/queries/evidenceQueries.ts
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setDropdownOpen(false)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Focus is lost after selecting a menu item — the trigger button should be refocused. Add triggerButtonRef.current?.focus() after closing the dropdown.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/components/DropdownActions.tsx, line 29:

<comment>Focus is lost after selecting a menu item — the trigger button should be refocused. Add `triggerButtonRef.current?.focus()` after closing the dropdown.</comment>

<file context>
@@ -0,0 +1,141 @@
+  useEffect(() => {
+    const handleClickOutside = (event: MouseEvent) => {
+      if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
+        setDropdownOpen(false)
+        setFocusIndex(-1)
+      }
</file context>

Comment thread frontend/src/components/ClaimActions.tsx
Comment thread frontend/src/components/ClaimForm.tsx Outdated
Comment thread frontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/edit/page.tsx Outdated
Comment thread frontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/edit/page.tsx Outdated
Comment thread e2e/pages/BoardCandidateClaims.spec.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/src/components/forms/shared/formValidationUtils.ts (1)

35-44: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix extension comparison to handle dot-prefix inconsistency.

The current logic extracts ext without a leading dot (Line 39: file.name.split('.').pop()?.toLowerCase()), but compares it directly against allowedExtensions items (Line 40), which may include a leading dot (e.g., ['.pdf', '.png']). This mismatch causes valid files to be rejected when allowedExtensions uses the common dot-prefixed convention.

🐛 Proposed fix to normalize both sides
 export const validateFileExtension = (
   file: File,
   allowedExtensions: string[]
 ): string | undefined => {
   const ext = file.name.split('.').pop()?.toLowerCase()
-  if (!allowedExtensions.some((e) => e.toLowerCase() === ext)) {
+  const normalizedAllowed = allowedExtensions.map((e) =>
+    e.toLowerCase().replace(/^\./, '')
+  )
+  if (!ext || !normalizedAllowed.includes(ext)) {
     return `File extension .${ext} is not supported. Allowed: ${allowedExtensions.join(', ')}`
   }
   return undefined
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/forms/shared/formValidationUtils.ts` around lines 35
- 44, The validateFileExtension function extracts the file extension without a
leading dot (ext variable from split and pop operations), but compares it
against allowedExtensions items which may include leading dots (e.g., ['.pdf',
'.png']). Normalize both sides of the comparison by stripping any leading dot
from each allowedExtensions item before comparing it with ext, or add a dot
prefix to ext before comparison. This ensures the logic correctly validates
files regardless of whether allowedExtensions uses dot-prefixed or non-prefixed
conventions.
frontend/src/app/board/[year]/candidates/[login]/claims/create/page.tsx (1)

38-50: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Distinguish query errors from access denial for clearer UX.

The current flow shows a dismissible error toast via handleAppError (line 40), then continues to line 50 where query errors (which leave isCandidate false) display the same "Access Denied" message as legitimate permission failures. Users experiencing network issues will see the generic access message after the toast dismisses, making it unclear whether they lack permission or encountered a transient error.

💡 Suggested improvement

Return early with a specific error state when the query fails:

   useEffect(() => {
     if (candidateQueryError) {
       handleAppError(candidateQueryError)
     }
   }, [candidateQueryError])

+  if (candidateQueryError) {
+    return (
+      <AccessDeniedDisplay
+        title="Error Loading Form"
+        message="Unable to load candidate data. Please try again."
+      />
+    )
+  }
+
   if (isSyncing || isCandidateLoading) {
     return <LoadingSpinner />
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/board/`[year]/candidates/[login]/claims/create/page.tsx
around lines 38 - 50, The current implementation shows a dismissible error toast
when candidateQueryError exists via handleAppError, but then continues to check
the isCandidate condition which returns the same generic "Access Denied"
message, making it unclear whether the error is due to a network failure or
actual permission denial. After the useEffect that calls handleAppError with
candidateQueryError, add an early return before the isCandidate check to display
a specific error state when candidateQueryError is present, ensuring users see
the appropriate error context rather than the generic access denial message.
♻️ Duplicate comments (2)
frontend/src/app/board/[year]/candidates/[login]/claims/create/page.tsx (1)

34-34: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Skip condition allows query execution before ownership is verified.

The skip logic (session && session?.user?.login !== login) evaluates to false when session is null or undefined, allowing the candidate query to execute before authentication completes. This creates unnecessary backend load and potential information disclosure.

🔒 Recommended fix
+  const isOwner = !!session?.user?.login && session.user.login === login
+
   const {
     data: candidateGraphQLData,
     loading: isCandidateLoading,
     error: candidateQueryError,
   } = useQuery(GetBoardCandidateDocument, {
-    skip: !login || !year || (session && session?.user?.login !== login),
+    skip: !login || !year || !isOwner,
     variables: { login: login, year: Number.parseInt(year) },
   })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/board/`[year]/candidates/[login]/claims/create/page.tsx at
line 34, The skip condition in the candidate query uses incorrect logic that
allows query execution when the session is null or undefined. The condition
(session && session?.user?.login !== login) evaluates to false when session is
falsy, causing skip to become false and allowing the query to run before
authentication is verified. Fix the skip condition to explicitly check that
session exists AND that the user's login matches the requested login parameter.
The skip condition should prevent query execution whenever login, year, or
session are missing, or when the authenticated user's login does not match the
requested login parameter.
frontend/src/app/board/[year]/candidates/[login]/claims/page.tsx (1)

38-38: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Skip condition still allows unauthorized query execution.

The ownership check in the skip condition is incomplete. When session is null or undefined (during isSyncing), the expression (session && session?.user?.login !== login) evaluates to false, allowing the query to execute before authentication is verified.

🔒 Recommended fix
+  const isOwner = !!session?.user?.login && session.user.login === login
   const {
     data: graphQLData,
     loading: isLoading,
     error: graphQLRequestError,
   } = useQuery(GetBoardCandidateAndClaimsDocument, {
-    skip: !login || !year || (session && session?.user?.login !== login),
+    skip: !login || !year || !isOwner,
     variables: { login, year: Number.parseInt(year) },
   })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/board/`[year]/candidates/[login]/claims/page.tsx at line 38,
The skip condition for the query does not properly handle the case when session
is null or undefined. The expression (session && session?.user?.login !== login)
evaluates to false when session is missing, which allows the query to execute
without proper authentication verification. Update the skip condition to
explicitly check that session exists before attempting to access
session.user.login, ensuring the query is skipped whenever session is
null/undefined OR when the authenticated user's login does not match the
requested login parameter.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/settings/graphql.py`:
- Around line 53-56: In the __init__ method, the kwargs.pop("execution_context")
call on line 55 lacks a default argument and will raise a KeyError if
"execution_context" is not present in the incoming kwargs. Modify the pop call
to include a default value as the second argument, such as None, to gracefully
handle cases where "execution_context" is not provided in kwargs.

---

Outside diff comments:
In `@frontend/src/app/board/`[year]/candidates/[login]/claims/create/page.tsx:
- Around line 38-50: The current implementation shows a dismissible error toast
when candidateQueryError exists via handleAppError, but then continues to check
the isCandidate condition which returns the same generic "Access Denied"
message, making it unclear whether the error is due to a network failure or
actual permission denial. After the useEffect that calls handleAppError with
candidateQueryError, add an early return before the isCandidate check to display
a specific error state when candidateQueryError is present, ensuring users see
the appropriate error context rather than the generic access denial message.

In `@frontend/src/components/forms/shared/formValidationUtils.ts`:
- Around line 35-44: The validateFileExtension function extracts the file
extension without a leading dot (ext variable from split and pop operations),
but compares it against allowedExtensions items which may include leading dots
(e.g., ['.pdf', '.png']). Normalize both sides of the comparison by stripping
any leading dot from each allowedExtensions item before comparing it with ext,
or add a dot prefix to ext before comparison. This ensures the logic correctly
validates files regardless of whether allowedExtensions uses dot-prefixed or
non-prefixed conventions.

---

Duplicate comments:
In `@frontend/src/app/board/`[year]/candidates/[login]/claims/create/page.tsx:
- Line 34: The skip condition in the candidate query uses incorrect logic that
allows query execution when the session is null or undefined. The condition
(session && session?.user?.login !== login) evaluates to false when session is
falsy, causing skip to become false and allowing the query to run before
authentication is verified. Fix the skip condition to explicitly check that
session exists AND that the user's login matches the requested login parameter.
The skip condition should prevent query execution whenever login, year, or
session are missing, or when the authenticated user's login does not match the
requested login parameter.

In `@frontend/src/app/board/`[year]/candidates/[login]/claims/page.tsx:
- Line 38: The skip condition for the query does not properly handle the case
when session is null or undefined. The expression (session &&
session?.user?.login !== login) evaluates to false when session is missing,
which allows the query to execute without proper authentication verification.
Update the skip condition to explicitly check that session exists before
attempting to access session.user.login, ensuring the query is skipped whenever
session is null/undefined OR when the authenticated user's login does not match
the requested login parameter.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a810207d-2072-4c56-b2d5-abbc215a184b

📥 Commits

Reviewing files that changed from the base of the PR and between 00ae269 and 1df3aa6.

📒 Files selected for processing (15)
  • backend/settings/graphql.py
  • e2e/helpers/mockClaimAuth.ts
  • e2e/pages/BoardCandidateClaimDetails.spec.ts
  • e2e/pages/BoardCandidateClaims.spec.ts
  • frontend/__tests__/mockData/mockClaimData.ts
  • frontend/__tests__/unit/pages/CandidateClaimsPage.test.tsx
  • frontend/__tests__/unit/pages/ClaimDetailsPage.test.tsx
  • frontend/__tests__/unit/pages/EditClaimPage.test.tsx
  • frontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/edit/page.tsx
  • frontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/page.tsx
  • frontend/src/app/board/[year]/candidates/[login]/claims/create/page.tsx
  • frontend/src/app/board/[year]/candidates/[login]/claims/page.tsx
  • frontend/src/components/ClaimActions.tsx
  • frontend/src/components/ClaimForm.tsx
  • frontend/src/components/forms/shared/formValidationUtils.ts

Comment thread backend/settings/graphql.py Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 15 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="frontend/src/components/DropdownActions.tsx">

<violation number="1" location="frontend/src/components/DropdownActions.tsx:29">
P2: Focus is lost after selecting a menu item — the trigger button should be refocused. Add `triggerButtonRef.current?.focus()` after closing the dropdown.</violation>
</file>

<file name="frontend/src/app/board/[year]/candidates/page.tsx">

<violation number="1" location="frontend/src/app/board/[year]/candidates/page.tsx:95">
P2: Calling `useDjangoSession()` inside `CandidateCard` creates N hook instances when rendering many candidates. This hook contains session sync logic with side effects (mutations, toasts, sign-out calls) that should only execute once. Consider lifting it to the parent page and passing `isOwnProfile` as a prop.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread frontend/src/app/board/[year]/candidates/[login]/claims/create/page.tsx Outdated
Comment thread backend/settings/graphql.py Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 23, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="frontend/src/components/DropdownActions.tsx">

<violation number="1" location="frontend/src/components/DropdownActions.tsx:29">
P2: Focus is lost after selecting a menu item — the trigger button should be refocused. Add `triggerButtonRef.current?.focus()` after closing the dropdown.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread frontend/src/app/board/[year]/candidates/[login]/claims/page.tsx Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 23, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Requires human review: Auto-approval blocked by 2 unresolved issues from previous reviews.

Re-trigger cubic

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 24, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 3 files (changes from recent commits).

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

Re-trigger cubic

@rudransh-shrivastava rudransh-shrivastava left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments:

  • Sonar Qube issues are created by GraphQL generated types.
  • All redirects to evidence pages are currently 404 as it will be added in next PR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component is similar to EntityActions but this one is generic. I will update EntityActions to use this component and rename it to something more specific to mentorship portal.

I can't reuse EntityActions as it's highly specific to mentorship portal.

@rudransh-shrivastava rudransh-shrivastava marked this pull request as ready for review June 24, 2026 13:18
@cubic-dev-ai

cubic-dev-ai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment @cubic-dev-ai review.

@arkid15r arkid15r left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kasya could you help w/ the frontend part?
Thank you!

@strawberry_django.field
def has_evidence(self, root: BoardCandidateClaim) -> bool:
"""Resolve whether the claim has any evidence."""
if hasattr(root, "evidence_exists"):

@arkid15r arkid15r Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a case for evidence_exists to be absent?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I only set it in board_candidate_claims query, single node board_candidate_claim does not set it.
I will add it to single node query as well, but also keep the hasattr check just in case.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated both queries to annotate.

@rudransh-shrivastava rudransh-shrivastava marked this pull request as draft June 27, 2026 07:42

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 12 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="frontend/src/components/DropdownActions.tsx">

<violation number="1" location="frontend/src/components/DropdownActions.tsx:29">
P2: Focus is lost after selecting a menu item — the trigger button should be refocused. Add `triggerButtonRef.current?.focus()` after closing the dropdown.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread e2e/pages/BoardCandidateClaimEdit.spec.ts
Comment thread backend/apps/owasp/api/internal/queries/board_candidate_claim.py
@sonarqubecloud

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 2 files (changes from recent commits).

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

Re-trigger cubic

@rudransh-shrivastava rudransh-shrivastava marked this pull request as ready for review June 27, 2026 08:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants