Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
04bfb79
do not scale/zoom card on hover
rudransh-shrivastava Aug 8, 2026
c378e38
introduce fat methods approving claims
rudransh-shrivastava Aug 8, 2026
82e6975
simplify code by making BoardcandidateClaim.board a mandatory field
rudransh-shrivastava Aug 8, 2026
05463a5
rename BoardOfDirectors.reviewers to claim_reviewers
rudransh-shrivastava Aug 8, 2026
dc12dde
update bod.dump
rudransh-shrivastava Aug 8, 2026
8ff5fc8
address exif metadata stripping related review comments
rudransh-shrivastava Aug 8, 2026
be4f8c6
do not shink candidate card on click
rudransh-shrivastava Aug 8, 2026
f7c7208
add pydantic input validation for graphql mutations
rudransh-shrivastava Aug 9, 2026
65f409a
Merge branch 'feature/bod-candidate-transparency' into feature/bod-ca…
rudransh-shrivastava Aug 9, 2026
01b8d21
generate graphql types post merge
rudransh-shrivastava Aug 9, 2026
cefd1e6
fix source_url bug
rudransh-shrivastava Aug 9, 2026
c658f48
address bot comments
rudransh-shrivastava Aug 9, 2026
099fae5
run prettier
rudransh-shrivastava Aug 9, 2026
5bfb5be
fix frontend tests
rudransh-shrivastava Aug 9, 2026
189c0c1
add s3 bucket for django media storage for production and staging
rudransh-shrivastava Aug 9, 2026
9f54b73
set custom_domain in storage options
rudransh-shrivastava Aug 9, 2026
d3c1385
apply bot comments
rudransh-shrivastava Aug 15, 2026
e99b6bf
update code
rudransh-shrivastava Aug 15, 2026
fcc41cd
publicly show Submitted claims
rudransh-shrivastava Aug 16, 2026
c36300c
update code
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
2 changes: 1 addition & 1 deletion backend/src/apps/owasp/admin/board_of_directors.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
class BoardOfDirectorsAdmin(admin.ModelAdmin):
"""Admin for Snapshot model."""

filter_horizontal = ("reviewers",)
filter_horizontal = ("claim_reviewers",)
list_filter = ("year",)
ordering = ("-year",)
search_fields = ("year",)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,7 @@ def _validate_review_eligibility(
message=INVALID_STATUS_MSG,
)

if (
claim.board
and reviewer.github_user
and claim.board.get_candidate(login=reviewer.github_user.login)
):
if reviewer.github_user and claim.board.get_candidate(login=reviewer.github_user.login):
return ReviewResult(
ok=False,
code="FORBIDDEN",
Expand Down Expand Up @@ -91,7 +87,7 @@ def create_board_candidate_claim_review(
user = info.context.request.user

is_reviewer = BoardOfDirectors.objects.filter(
year=input_data.year, reviewers=user
year=input_data.year, claim_reviewers=user
).exists()
if not user.github_user or not is_reviewer:
return ReviewResult(ok=False, code="FORBIDDEN", message=ACCESS_DENIED_MSG)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,7 @@ def reviews(
return root.reviews.all()

is_reviewer = (
user.is_authenticated
and root.board is not None
and root.board.reviewers.filter(id=user.id).exists()
user.is_authenticated and root.board.claim_reviewers.filter(id=user.id).exists()
Comment thread
rudransh-shrivastava marked this conversation as resolved.
Outdated
)
if is_reviewer:
return root.reviews.filter(reviewer=user)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ def owasp_url(self, root: BoardOfDirectors) -> str:
def reviewer(self, root: BoardOfDirectors, login: str) -> UserNode | None:
"""Resolve board election reviewer."""
user = (
root.reviewers.select_related("github_user").filter(github_user__login=login).first()
root.claim_reviewers.select_related("github_user")
.filter(github_user__login=login)
.first()
)
return user.github_user if user else None
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def board_candidate_claims(
user = info.context.request.user
is_reviewer = (
user.is_authenticated
and BoardOfDirectors.objects.filter(year=year, reviewers=user).exists()
and BoardOfDirectors.objects.filter(year=year, claim_reviewers=user).exists()
)
claims = BoardCandidateClaim.objects.filter(
board__year=year,
Expand Down Expand Up @@ -126,7 +126,9 @@ def board_candidate_claim(
and user.github_user is not None
and user.github_user == claim.candidate.member
)
is_reviewer = user.is_authenticated and claim.board.reviewers.filter(id=user.id).exists()
is_reviewer = (
user.is_authenticated and claim.board.claim_reviewers.filter(id=user.id).exists()
)

return (
claim
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def get_claim_evidence(
and user.github_user == evidence.claim.candidate.member
)
is_reviewer = (
user.is_authenticated and evidence.claim.board.reviewers.filter(id=user.id).exists()
user.is_authenticated and evidence.claim.board.claim_reviewers.filter(id=user.id).exists()
)

return (
Expand Down Expand Up @@ -92,7 +92,9 @@ def board_candidate_claim_evidences(
and claim.candidate.member is not None
and user.github_user == claim.candidate.member
)
is_reviewer = user.is_authenticated and claim.board.reviewers.filter(id=user.id).exists()
is_reviewer = (
user.is_authenticated and claim.board.claim_reviewers.filter(id=user.id).exists()
)

return (
claim.evidences.filter(is_removed=False)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Generated by Django 6.0.6 on 2026-08-08 15:32

import django.core.validators
import django.db.models.deletion
from django.db import migrations, models

import apps.owasp.models.board_candidate_claim_evidence
import apps.owasp.validators


class Migration(migrations.Migration):
dependencies = [
("owasp", "0080_boardcandidateclaimreview_boardofdirectors_reviewers_and_more"),
]

operations = [
migrations.AlterField(
model_name="boardcandidateclaim",
Comment thread
rudransh-shrivastava marked this conversation as resolved.
name="board",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="claims",
to="owasp.boardofdirectors",
),
),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
migrations.AlterField(
model_name="boardcandidateclaimevidence",
name="file",
field=models.FileField(
blank=True,
null=True,
upload_to=apps.owasp.models.board_candidate_claim_evidence.uuid_upload_to,
validators=[
django.core.validators.FileExtensionValidator(
allowed_extensions=["jpeg", "jpg", "pdf", "png", "webp"]
),
apps.owasp.validators.validate_evidence_file_size,
],
verbose_name="File",
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 6.0.6 on 2026-08-08 15:51

from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("owasp", "0081_alter_boardcandidateclaim_board_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.RemoveField(
model_name="boardofdirectors",
name="reviewers",
),
migrations.AddField(
model_name="boardofdirectors",
name="claim_reviewers",
field=models.ManyToManyField(
blank=True,
help_text="Reviewers for this year's board election claims.",
related_name="+",
to=settings.AUTH_USER_MODEL,
verbose_name="Claim reviewers",
),
),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
]
24 changes: 20 additions & 4 deletions backend/src/apps/owasp/models/board_candidate_claim.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,7 @@ class Status(models.TextChoices):
}
WITHDRAWAL_ALLOWED_FIELDS = frozenset({"status", "withdrawn_reason", "withdrawn_at"})

board = models.ForeignKey(
BoardOfDirectors, blank=True, null=True, on_delete=models.SET_NULL, related_name="claims"
)
board = models.ForeignKey(BoardOfDirectors, on_delete=models.CASCADE, related_name="claims")
candidate = models.ForeignKey(EntityMember, on_delete=models.CASCADE, related_name="claims")
description = models.TextField(default="", verbose_name="Description")
is_locked = models.BooleanField(
Expand Down Expand Up @@ -139,7 +137,7 @@ def save(self, *args, **kwargs) -> None:

self.full_clean()

if not self.pk and self.candidate_id and self.board_id:
if not self.pk:
max_order = (
BoardCandidateClaim.objects.filter(
candidate_id=self.candidate_id,
Expand All @@ -155,6 +153,11 @@ def save(self, *args, **kwargs) -> None:

super().save(*args, **kwargs)

def set_status_approved(self) -> None:
"""Set claim status to approved."""
self.status = self.Status.APPROVED
self.save()

@staticmethod
def bulk_save(claims: list, fields: list | None = None) -> None: # type: ignore[override]
"""Bulk save claims.
Expand All @@ -165,3 +168,16 @@ def bulk_save(claims: list, fields: list | None = None) -> None: # type: ignore

"""
BulkSaveModel.bulk_save(BoardCandidateClaim, claims, fields=fields)

@classmethod
def bulk_set_status_approved(cls, claims: list[BoardCandidateClaim]) -> None:
"""Bulk-approve and lock claims.

Args:
claims (list[BoardCandidateClaim]): Claims to approve.

"""
for claim in claims:
claim.status = cls.Status.APPROVED
claim.is_locked = True
cls.objects.bulk_update(claims, ["is_locked", "status"])
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
rudransh-shrivastava marked this conversation as resolved.
Comment thread
rudransh-shrivastava marked this conversation as resolved.
Comment thread
rudransh-shrivastava marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,11 @@ def clean(self) -> None:
err = "Review can only be added to submitted claims."
raise ValidationError(err)

if (
not self.claim.board
or not self.claim.board.reviewers.filter(id=self.reviewer.id).exists()
):
if not self.claim.board.claim_reviewers.filter(id=self.reviewer.id).exists():
err = "Only Claim Reviewers can review claims."
raise ValidationError(err)

if self.claim.board and self.claim.board.get_candidate(
login=self.reviewer.github_user.login
):
if self.claim.board.get_candidate(login=self.reviewer.github_user.login):
err = "A candidate cannot review claims in the same election year."
raise ValidationError(err)

Expand Down
4 changes: 2 additions & 2 deletions backend/src/apps/owasp/models/board_of_directors.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ class Meta:

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
reviewers = models.ManyToManyField(
claim_reviewers = models.ManyToManyField(
"nest.User",
verbose_name="Reviewers",
verbose_name="Claim reviewers",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
related_name="+",
blank=True,
help_text="Reviewers for this year's board election claims.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ def review_post_save_finalize_claim_status(sender, instance, **kwargs): # noqa:
).count()

if approved_count >= threshold:
claim.status = BoardCandidateClaim.Status.APPROVED
claim.save()
claim.set_status_approved()
logger.info(
"Claim '%s' auto-approved with %d approvals (threshold: %d).",
claim.key,
Expand Down
4 changes: 1 addition & 3 deletions backend/src/apps/owasp/signals/board_of_directors.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,10 @@ def board_post_save_re_evaluate_claims(sender, instance, **kwargs): # noqa: ARG
).count()

if approved_count >= threshold:
claim.status = BoardCandidateClaim.Status.APPROVED
claim.is_locked = True
claims_to_approve.append(claim)

if claims_to_approve:
BoardCandidateClaim.objects.bulk_update(claims_to_approve, ["is_locked", "status"])
BoardCandidateClaim.bulk_set_status_approved(claims_to_approve)
logger.info(
"Approved %d claims after threshold change on board %d.",
len(claims_to_approve),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ class TestBoardOfDirectorsAdmin:
"""Tests for BoardOfDirectorsAdmin."""

def test_filter_horizontal(self):
"""Test filter_horizontal includes reviewers."""
"""Test filter_horizontal includes claim_reviewers."""
admin_instance = BoardOfDirectorsAdmin(BoardOfDirectors, AdminSite())

assert admin_instance.filter_horizontal == ("reviewers",)
assert admin_instance.filter_horizontal == ("claim_reviewers",)

def test_list_filter(self):
"""Test list_filter includes year."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def test_reviews_reviewer_sees_own(self):
mock_claim = Mock()
mock_claim.candidate.member = Mock()
mock_claim.status = BoardCandidateClaim.Status.SUBMITTED
mock_claim.board.reviewers.filter.return_value.exists.return_value = True
mock_claim.board.claim_reviewers.filter.return_value.exists.return_value = True
mock_queryset = MagicMock()
mock_claim.reviews = MagicMock()
mock_claim.reviews.filter.return_value = mock_queryset
Expand All @@ -146,7 +146,7 @@ def test_reviews_non_reviewer_gets_empty_on_submitted(self):
mock_claim = Mock()
mock_claim.candidate.member = Mock()
mock_claim.status = BoardCandidateClaim.Status.SUBMITTED
mock_claim.board.reviewers.filter.return_value.exists.return_value = False
mock_claim.board.claim_reviewers.filter.return_value.exists.return_value = False

field = self._get_field_by_name("reviews", BoardCandidateClaimNode)
result = field.base_resolver.wrapped_func(None, mock_claim, info)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,31 +95,25 @@ def test_reviewer_resolver_found(self):
mock_nest_user.github_user = mock_github_user

mock_board = Mock()
mock_board.reviewers.select_related.return_value.filter.return_value.first.return_value = (
mock_nest_user
)
mock_filter = mock_board.claim_reviewers.select_related.return_value.filter
mock_filter.return_value.first.return_value = mock_nest_user

field = self._get_field_by_name("reviewer", BoardOfDirectorsNode)
result = field.base_resolver.wrapped_func(None, mock_board, login="alice")

mock_board.reviewers.select_related.assert_called_once_with("github_user")
mock_board.reviewers.select_related.return_value.filter.assert_called_once_with(
github_user__login="alice"
)
mock_board.claim_reviewers.select_related.assert_called_once_with("github_user")
mock_filter.assert_called_once_with(github_user__login="alice")
assert result is mock_github_user

def test_reviewer_resolver_not_found(self):
"""Test reviewer returns None when no reviewer matches the login."""
mock_board = Mock()
mock_board.reviewers.select_related.return_value.filter.return_value.first.return_value = (
None
)
mock_filter = mock_board.claim_reviewers.select_related.return_value.filter
mock_filter.return_value.first.return_value = None

field = self._get_field_by_name("reviewer", BoardOfDirectorsNode)
result = field.base_resolver.wrapped_func(None, mock_board, login="unknown")

mock_board.reviewers.select_related.assert_called_once_with("github_user")
mock_board.reviewers.select_related.return_value.filter.assert_called_once_with(
github_user__login="unknown"
)
mock_board.claim_reviewers.select_related.assert_called_once_with("github_user")
mock_filter.assert_called_once_with(github_user__login="unknown")
assert result is None
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def test_board_candidate_claim_evidences_non_self_non_approved(self, mock_claim_
login = "alice"

claim = MagicMock()
claim.board.reviewers.filter.return_value.exists.return_value = False
claim.board.claim_reviewers.filter.return_value.exists.return_value = False
claim.candidate.member = None
claim.status = BoardCandidateClaim.Status.SUBMITTED
mock_claim_model.objects.filter.return_value.first.return_value = claim
Expand Down Expand Up @@ -134,7 +134,7 @@ def test_board_candidate_claim_evidences_reviewer_sees_submitted(self, mock_clai
login = "alice"

claim = MagicMock()
claim.board.reviewers.filter.return_value.exists.return_value = True
claim.board.claim_reviewers.filter.return_value.exists.return_value = True
claim.candidate.member = MagicMock()
claim.status = BoardCandidateClaim.Status.SUBMITTED
evidences_qs = MagicMock()
Expand Down Expand Up @@ -219,7 +219,7 @@ def test_board_candidate_claim_evidence_non_self_not_approved(self):
info = _make_info(user)

evidence = MagicMock()
evidence.claim.board.reviewers.filter.return_value.exists.return_value = False
evidence.claim.board.claim_reviewers.filter.return_value.exists.return_value = False
evidence.claim.candidate.member = None
evidence.claim.status = BoardCandidateClaim.Status.SUBMITTED

Expand Down Expand Up @@ -264,7 +264,7 @@ def test_board_candidate_claim_evidence_reviewer_sees_submitted(self):
info = _make_info(user)

evidence = MagicMock()
evidence.claim.board.reviewers.filter.return_value.exists.return_value = True
evidence.claim.board.claim_reviewers.filter.return_value.exists.return_value = True
evidence.claim.candidate.member = MagicMock()
evidence.claim.status = BoardCandidateClaim.Status.SUBMITTED

Expand Down Expand Up @@ -349,7 +349,7 @@ def test_file_url_not_accessible(self):
info = _make_info(user)

evidence = MagicMock()
evidence.claim.board.reviewers.filter.return_value.exists.return_value = False
evidence.claim.board.claim_reviewers.filter.return_value.exists.return_value = False
evidence.claim.candidate.member = None
evidence.claim.status = BoardCandidateClaim.Status.SUBMITTED
evidence.file = MagicMock()
Expand Down Expand Up @@ -423,7 +423,7 @@ def test_file_url_reviewer_accessible(self):
info = _make_info(user)

evidence = MagicMock()
evidence.claim.board.reviewers.filter.return_value.exists.return_value = True
evidence.claim.board.claim_reviewers.filter.return_value.exists.return_value = True
evidence.claim.candidate.member = None
evidence.claim.status = BoardCandidateClaim.Status.SUBMITTED
evidence.file = MagicMock()
Expand Down
Loading
Loading