Skip to content
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
821663b
add BoardCandidateProfile model and update code
rudransh-shrivastava Aug 2, 2026
2a7a638
add candidate profile page with annotations
rudransh-shrivastava Aug 5, 2026
c1cf6e8
integrate claim highlights and candidate page with other components
rudransh-shrivastava Aug 5, 2026
4d64d9f
backend: apply bot comments
rudransh-shrivastava Aug 7, 2026
3930de5
apply frontend bot comments
rudransh-shrivastava Aug 7, 2026
126da9a
refactor frontend to use markdown-to-jsx
rudransh-shrivastava Aug 7, 2026
1d0de2d
frontend cleanup
rudransh-shrivastava Aug 8, 2026
9e9c30d
remove AnnotatedProfile
rudransh-shrivastava Aug 8, 2026
5e390c8
rewrite AnnotatedProfile with better quality code
rudransh-shrivastava Aug 8, 2026
b5a000a
generate graphql types
rudransh-shrivastava Aug 8, 2026
59a0bb5
bot comments, hide popup when overlapping existing highlight
rudransh-shrivastava Aug 8, 2026
c4a83e6
don't show create claim button when highlighted text is not in raw ma…
rudransh-shrivastava Aug 8, 2026
5c200da
remove unused isReviewer
rudransh-shrivastava Aug 8, 2026
2d456a9
display all matched claims instead of first
rudransh-shrivastava Aug 8, 2026
0694855
Merge branch 'feature/bod-candidate-transparency' into feature/bod-ca…
rudransh-shrivastava Aug 8, 2026
b3981fb
post merge, fix generate command to include source text
rudransh-shrivastava Aug 8, 2026
c64d77a
validate source text in backend
rudransh-shrivastava Aug 8, 2026
4767af9
cleanup frontend code
rudransh-shrivastava Aug 8, 2026
162a9a1
Merge branch 'feature/bod-candidate-transparency' into feature/bod-ca…
rudransh-shrivastava Aug 10, 2026
d2b7ee6
update claim status RBAC, show submitted claims, change colors
rudransh-shrivastava Aug 16, 2026
b15e9ee
fix breadcrumbs
rudransh-shrivastava Aug 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions backend/make/apps/owasp.mk
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
owasp-aggregate-member-contributions owasp-aggregate-projects owasp-create-project-metadata-file \
owasp-enrich-chapters owasp-enrich-committees owasp-enrich-events owasp-enrich-projects \
owasp-generate-community-snapshot-video owasp-process-snapshots owasp-scrape-chapters \
owasp-scrape-committees owasp-scrape-projects owasp-sync-posts owasp-update-events \
owasp-update-leaders owasp-update-project-health-metrics owasp-update-project-health-requirements \
owasp-update-project-health-scores owasp-update-sponsors
owasp-scrape-committees owasp-scrape-projects owasp-sync-board-candidates owasp-sync-posts \
owasp-update-events owasp-update-leaders owasp-update-project-health-metrics \
owasp-update-project-health-requirements owasp-update-project-health-scores owasp-update-sponsors

owasp-add-project-custom-tags:
@echo "Adding project custom tags from $(FILE)"
Expand Down Expand Up @@ -77,6 +77,10 @@ owasp-scrape-projects:
owasp-sync-posts:
@CMD="python manage.py owasp_sync_posts" $(MAKE) backend-exec-command

owasp-sync-board-candidates:
@echo "Sync OWASP board candidates"
@CMD="python manage.py owasp_sync_board_candidates $(ARGS)" $(MAKE) backend-exec-command

owasp-update-events:
@echo "Getting OWASP events data"
@CMD="python manage.py owasp_update_events" $(MAKE) backend-exec-command
Expand Down
1 change: 1 addition & 0 deletions backend/src/apps/owasp/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .board_candidate_claim import BoardCandidateClaimAdmin
from .board_candidate_claim_evidence import BoardCandidateClaimEvidenceAdmin
from .board_candidate_claim_review import BoardCandidateClaimReviewAdmin
from .board_candidate_profile import BoardCandidateProfileAdmin
from .board_of_directors import BoardOfDirectorsAdmin
from .chapter import ChapterAdmin
from .committee import CommitteeAdmin
Expand Down
41 changes: 41 additions & 0 deletions backend/src/apps/owasp/admin/board_candidate_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Django admin configuration for BoardCandidateProfile model."""

from django.contrib import admin
from django.db import models

from apps.owasp.models.board_candidate_profile import BoardCandidateProfile


class BoardCandidateProfileAdmin(admin.ModelAdmin):
"""Admin for BoardCandidateProfile model."""
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

autocomplete_fields = ("candidate",)
list_display = (
"__str__",
"nest_created_at",
"nest_updated_at",
)
search_fields = (
"candidate__member_name",
"candidate__member__login",
"raw_markdown",
)
readonly_fields = (
"nest_created_at",
"nest_updated_at",
)

def get_queryset(self, request) -> models.QuerySet:
"""Retrieve optimized queryset with related candidate.

Args:
request: The HTTP request object.

Returns:
QuerySet: BoardCandidateProfile queryset with prefetched candidate.

"""
return super().get_queryset(request).select_related("candidate__member")


admin.site.register(BoardCandidateProfile, BoardCandidateProfileAdmin)
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class CreateClaimInput:

description: str
name: str
source_text: str = ""
year: int


Expand All @@ -37,6 +38,7 @@ class UpdateClaimInput:
description: str | None = None
key: str
name: str | None = None
source_text: str | None = None
year: int


Expand Down Expand Up @@ -173,6 +175,7 @@ def create_board_candidate_claim(
candidate=candidate,
description=input_data.description,
name=input_data.name,
source_text=input_data.source_text,
)
except IntegrityError:
logger.warning(
Expand Down Expand Up @@ -232,6 +235,9 @@ def update_board_candidate_claim(
if input_data.description:
claim.description = input_data.description
update_fields.append("description")
if input_data.source_text is not None:
claim.source_text = input_data.source_text
update_fields.append("source_text")

try:
claim.save(update_fields=update_fields)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"key",
"name",
"order",
"source_text",
"withdrawn_at",
"withdrawn_reason",
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""OWASP Board Candidate Profile GraphQL node."""

from datetime import datetime

import strawberry
import strawberry_django

from apps.owasp.api.internal.nodes.entity_member import EntityMemberNode
from apps.owasp.models.board_candidate_profile import BoardCandidateProfile


@strawberry_django.type(
BoardCandidateProfile,
fields=[
"raw_markdown",
],
)
class BoardCandidateProfileNode(strawberry.relay.Node):
"""Board Candidate Profile node."""

@strawberry_django.field
def candidate(self, root: BoardCandidateProfile) -> EntityMemberNode:
"""Resolve candidate."""
return root.candidate

@strawberry_django.field
def created_at(self, root: BoardCandidateProfile) -> datetime:
"""Resolve profile creation date."""
return root.nest_created_at

@strawberry_django.field
def updated_at(self, root: BoardCandidateProfile) -> datetime:
"""Resolve profile last update date."""
return root.nest_updated_at
2 changes: 2 additions & 0 deletions backend/src/apps/owasp/api/internal/queries/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from apps.owasp.api.internal.queries.board_candidate_claim_evidence import (
BoardCandidateClaimEvidenceQuery,
)
from apps.owasp.api.internal.queries.board_candidate_profile import BoardCandidateProfileQuery

from .board_of_directors import BoardOfDirectorsQuery
from .chapter import ChapterQuery
Expand All @@ -21,6 +22,7 @@
class OwaspQuery(
BoardCandidateClaimEvidenceQuery,
BoardCandidateClaimQuery,
BoardCandidateProfileQuery,
BoardOfDirectorsQuery,
ChapterQuery,
CommitteeQuery,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,18 @@ def board_candidate_claims(
claims = claims.filter(candidate__member__login=login)

if not is_self and not is_reviewer:
claims = claims.filter(status=BoardCandidateClaim.Status.APPROVED)
claims = claims.filter(
status__in=[
BoardCandidateClaim.Status.APPROVED,
BoardCandidateClaim.Status.REJECTED,
]
)
elif is_reviewer and not is_self:
claims = claims.filter(
status__in=[
BoardCandidateClaim.Status.SUBMITTED,
BoardCandidateClaim.Status.APPROVED,
BoardCandidateClaim.Status.REJECTED,
]
)
elif is_reviewer:
Expand All @@ -62,16 +68,27 @@ def board_candidate_claims(
status__in=[
BoardCandidateClaim.Status.SUBMITTED,
BoardCandidateClaim.Status.APPROVED,
BoardCandidateClaim.Status.REJECTED,
]
)
)
elif user.is_authenticated and user.github_user:
claims = claims.filter(
Q(candidate__member=user.github_user)
| Q(status=BoardCandidateClaim.Status.APPROVED)
| Q(
status__in=[
BoardCandidateClaim.Status.APPROVED,
BoardCandidateClaim.Status.REJECTED,
]
)
)
else:
claims = claims.filter(status=BoardCandidateClaim.Status.APPROVED)
claims = claims.filter(
status__in=[
BoardCandidateClaim.Status.APPROVED,
BoardCandidateClaim.Status.REJECTED,
]
)

return (
claims.annotate(
Expand Down Expand Up @@ -133,7 +150,11 @@ def board_candidate_claim(
if (
is_self
or (is_reviewer and claim.status == BoardCandidateClaim.Status.SUBMITTED)
or claim.status == BoardCandidateClaim.Status.APPROVED
or claim.status
in {
BoardCandidateClaim.Status.APPROVED,
BoardCandidateClaim.Status.REJECTED,
}
)
else None
)
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ def get_claim_evidence(
if (
is_self
or (is_reviewer and evidence.claim.status == BoardCandidateClaim.Status.SUBMITTED)
or evidence.claim.status == BoardCandidateClaim.Status.APPROVED
or evidence.claim.status
Comment thread
rudransh-shrivastava marked this conversation as resolved.
in {
BoardCandidateClaim.Status.APPROVED,
BoardCandidateClaim.Status.REJECTED,
}
)
else None
)
Expand Down Expand Up @@ -99,7 +103,11 @@ def board_candidate_claim_evidences(
if (
is_self
or (is_reviewer and claim.status == BoardCandidateClaim.Status.SUBMITTED)
or claim.status == BoardCandidateClaim.Status.APPROVED
or claim.status
in {
BoardCandidateClaim.Status.APPROVED,
BoardCandidateClaim.Status.REJECTED,
}
)
else []
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""OWASP Board Candidate Profile GraphQL queries."""

import strawberry
import strawberry_django
from django.contrib.contenttypes.models import ContentType

from apps.owasp.api.internal.nodes.board_candidate_profile import BoardCandidateProfileNode
from apps.owasp.models.board_candidate_profile import BoardCandidateProfile
from apps.owasp.models.board_of_directors import BoardOfDirectors
from apps.owasp.models.entity_member import EntityMember


@strawberry.type
class BoardCandidateProfileQuery:
"""GraphQL queries for Board Candidate Profile model."""

@strawberry_django.field
def board_candidate_profile(
self, info: strawberry.Info, login: str, year: int
) -> BoardCandidateProfileNode | None:
"""Resolve Board Candidate Profile.

Args:
info (Info): Strawberry Info.
login (str): The login of the candidate.
year (int): The year of the election.

Returns:
BoardCandidateProfileNode object if found, None otherwise.

"""
try:
board = BoardOfDirectors.objects.get(year=year)
content_type = ContentType.objects.get_for_model(BoardOfDirectors)
return BoardCandidateProfile.objects.select_related("candidate__member").get(
candidate__member__login=login,
candidate__entity_type=content_type,
candidate__entity_id=board.id,
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
candidate__role=EntityMember.Role.CANDIDATE,
candidate__is_active=True,
candidate__is_reviewed=True,
)
except (BoardOfDirectors.DoesNotExist, BoardCandidateProfile.DoesNotExist):
return None
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
from django.core.management.base import BaseCommand

from apps.github.utils import get_repository_file_content
from apps.owasp.models.board_candidate_profile import BoardCandidateProfile
from apps.owasp.models.board_of_directors import BoardOfDirectors
from apps.owasp.models.entity_member import EntityMember

YAML_FRONTMATTER_PATTERN = r"^---\s*\n((?:(?!^---\s*$).*\n)+)^---\s*$"


class Command(BaseCommand):
help = "Sync board election candidates from www-board-candidates repository"
Expand Down Expand Up @@ -52,7 +55,7 @@ def parse_candidate_metadata(self, content: str) -> dict:
dict: Parsed metadata dictionary.

"""
yaml_pattern = re.compile(r"^---\s*\n((?:(?!^---\s*$).*\n)+)^---\s*$", re.MULTILINE)
yaml_pattern = re.compile(YAML_FRONTMATTER_PATTERN, re.MULTILINE)

if not content.startswith("---"):
return {}
Expand All @@ -66,6 +69,23 @@ def parse_candidate_metadata(self, content: str) -> dict:

return {}

def parse_candidate_profile(self, content: str) -> str:
"""Parse profile raw text content without YAML frontmatter from candidate markdown file.

Args:
content (str): The markdown file content.

Returns:
str: Parsed profile raw text.

"""
yaml_pattern = re.compile(YAML_FRONTMATTER_PATTERN, re.MULTILINE)

if not content.startswith("---"):
return content.strip()

return yaml_pattern.sub("", content, count=1).strip()
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def sync_year_candidates(self, year: int) -> int:
"""Sync candidates for a specific year.

Expand Down Expand Up @@ -128,7 +148,13 @@ def sync_year_candidates(self, year: int) -> int:
"order": 0,
}

EntityMember.update_data(data, save=True)
member = EntityMember.update_data(data, save=True)
raw_markdown = self.parse_candidate_profile(file_content)
BoardCandidateProfile.objects.update_or_create(
candidate=member,
Comment thread
rudransh-shrivastava marked this conversation as resolved.
defaults={"raw_markdown": raw_markdown},
)

synced_count += 1

return synced_count
Expand Down
Loading
Loading