Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2a6afd6
update nest badges code
mrkeshav-05 Dec 24, 2025
4115edc
Merge branch 'main' into refactor/nest-badges
mrkeshav-05 Dec 25, 2025
a1a0d96
update tests in nest badge
mrkeshav-05 Dec 25, 2025
1e54fb8
Merge branch 'main' into refactor/nest-badges
mrkeshav-05 Dec 25, 2025
e7132f6
apply coderabbit suggestions
mrkeshav-05 Dec 25, 2025
bcaaad1
Merge branch 'main' into refactor/nest-badges
mrkeshav-05 Dec 27, 2025
0f253bc
Merge branch 'main' into refactor/nest-badges
mrkeshav-05 Dec 29, 2025
374a602
Merge branch 'main' into refactor/nest-badges
mrkeshav-05 Dec 29, 2025
5c51218
Merge branch 'main' into refactor/nest-badges
mrkeshav-05 Dec 29, 2025
8e590fe
Merge branch 'main' into refactor/nest-badges
mrkeshav-05 Dec 30, 2025
6cb3957
Merge branch 'main' into refactor/nest-badges
mrkeshav-05 Dec 31, 2025
14a6dcf
Merge branch 'main' into refactor/nest-badges
mrkeshav-05 Jan 1, 2026
822bed7
Merge branch 'main' into refactor/nest-badges
mrkeshav-05 Jan 1, 2026
07759b0
Merge branch 'main' into refactor/nest-badges
mrkeshav-05 Jan 1, 2026
7cd1fdc
Refactor badges with base command
mrkeshav-05 Jan 1, 2026
21e2374
resolve sonarcloud issues
mrkeshav-05 Jan 1, 2026
53d4443
Update code
arkid15r Jan 2, 2026
dffe527
Merge branch 'main' into refactor/nest-badges
mrkeshav-05 Jan 2, 2026
26d10e9
apply bulk save and pluralize log messages
mrkeshav-05 Jan 2, 2026
a5dffd7
update code
mrkeshav-05 Jan 2, 2026
6b1ed18
Merge branch 'main' into refactor/nest-badges
mrkeshav-05 Jan 2, 2026
1104ce2
Merge branch 'main' into refactor/nest-badges
mrkeshav-05 Jan 4, 2026
79bc870
Merge branch 'main' into refactor/nest-badges
mrkeshav-05 Jan 4, 2026
461f495
Merge branch 'main' into refactor/nest-badges
mrkeshav-05 Jan 6, 2026
c3ab19c
Merge branch 'main' into refactor/nest-badges
mrkeshav-05 Jan 8, 2026
0db9318
Merge branch 'main' into refactor/nest-badges
mrkeshav-05 Jan 9, 2026
cf3a1f7
Update code
arkid15r Jan 10, 2026
baf8898
Merge branch 'main' into refactor/nest-badges
arkid15r Jan 10, 2026
67c1f71
Update code
arkid15r Jan 10, 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
4 changes: 2 additions & 2 deletions backend/apps/github/models/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ def get_logins():
return set(Organization.objects.values_list("login", flat=True))

@staticmethod
def bulk_save(organizations) -> None: # type: ignore[override]
def bulk_save(organizations, fields=None) -> None: # type: ignore[override]
"""Bulk save organizations."""
BulkSaveModel.bulk_save(Organization, organizations)
BulkSaveModel.bulk_save(Organization, organizations, fields=fields)

@staticmethod
def update_data(gh_organization, *, save: bool = True) -> Organization:
Expand Down
14 changes: 11 additions & 3 deletions backend/apps/nest/Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
nest-update-badges:
@echo "Updating Nest user badges"
@CMD="python manage.py nest_update_badges" $(MAKE) exec-backend-command
nest-update-project-leader-badges:
@echo "Updating OWASP Project Leader badges"
@CMD="python manage.py nest_update_project_leader_badges" $(MAKE) exec-backend-command

nest-update-staff-badges:
@echo "Updating OWASP Staff badges"
@CMD="python manage.py nest_update_staff_badges" $(MAKE) exec-backend-command

nest-update-badges: \
nest-update-project-leader-badges \
nest-update-staff-badges
90 changes: 90 additions & 0 deletions backend/apps/nest/management/commands/base_badge_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Base command for badge management."""

import logging
from abc import ABC, abstractmethod

from django.core.management.base import BaseCommand
from django.db.models import QuerySet
from django.template.defaultfilters import pluralize

from apps.github.models.user import User
from apps.nest.models.badge import Badge
from apps.nest.models.user_badge import UserBadge

logger = logging.getLogger(__name__)


class BaseBadgeCommand(BaseCommand, ABC):
"""Base class for badge sync commands."""

badge_css_class: str | None = None
badge_description: str | None = None
badge_name: str | None = None
badge_weight: int | None = None

@abstractmethod
def get_eligible_users(self) -> QuerySet[User]:
"""Return users who should have this badge."""

def _log(self, message):
logger.info(message)
self.stdout.write(message)

def handle(self, *args, **options):
if not self.badge_name:
msg = "Badge name must be set"
raise ValueError(msg)

self.stdout.write(f"Syncing {self.badge_name}...")

try:
badge, created = Badge.objects.get_or_create(
name=self.badge_name,
defaults={
"css_class": self.badge_css_class,
"description": self.badge_description,
"weight": self.badge_weight,
},
)

if created:
self._log(f"Created badge: '{badge.name}'")
else:
badge.css_class = self.badge_css_class
badge.description = self.badge_description
badge.weight = self.badge_weight
badge.save(update_fields=["css_class", "description", "weight"])

eligible_users = self.get_eligible_users()
users_to_add_ids = eligible_users.exclude(
user_badges__badge=badge,
user_badges__is_active=True,
)

new_badges = [
UserBadge(user=user, badge=badge, is_active=True) for user in users_to_add_ids
]
UserBadge.bulk_save(new_badges, fields=["is_active"])
added_count = len(new_badges)
self._log(
f"Added '{self.badge_name}' badge to {added_count} user{pluralize(added_count)}"
)

users_to_remove = UserBadge.objects.filter(
badge=badge,
is_active=True,
).exclude(user__in=eligible_users)

removed_count = users_to_remove.count()
if removed_count:
users_to_remove.update(is_active=False)
self._log(
f"Removed '{self.badge_name}' badge from {removed_count} "
f"user{pluralize(removed_count)}"
)

self.stdout.write(self.style.SUCCESS(f"{self.badge_name} synced successfully"))
except Exception:
logger.exception("Failed to sync %s", self.badge_name)
self.stdout.write(self.style.ERROR(f"{self.badge_name} sync failed"))
raise
153 changes: 0 additions & 153 deletions backend/apps/nest/management/commands/nest_update_badges.py

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Sync OWASP Project Leader badges."""

from django.contrib.contenttypes.models import ContentType
from django.db.models import QuerySet

from apps.github.models.user import User
from apps.nest.management.commands.base_badge_command import BaseBadgeCommand
from apps.owasp.models.entity_member import EntityMember
from apps.owasp.models.project import Project


class Command(BaseBadgeCommand):
help = "Sync OWASP Project Leader badges"

badge_css_class = "fa-user-shield"
badge_description = "Official OWASP Project Leader"
badge_name = "OWASP Project Leader"
badge_weight = 90

def get_eligible_users(self) -> QuerySet[User]:
return User.objects.filter(
id__in=EntityMember.objects.filter(
entity_type=ContentType.objects.get_for_model(Project),
is_active=True,
is_reviewed=True,
member__isnull=False,
role=EntityMember.Role.LEADER,
).values_list("member_id", flat=True)
).distinct()
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Sync OWASP Staff badges."""

from django.db.models import QuerySet

from apps.github.models.user import User
from apps.nest.management.commands.base_badge_command import BaseBadgeCommand


class Command(BaseBadgeCommand):
help = "Sync OWASP Staff badges"

badge_css_class = "fa-user-shield"
badge_description = "Official OWASP Staff"
badge_name = "OWASP Staff"
badge_weight = 100

def get_eligible_users(self) -> QuerySet[User]:
return User.objects.filter(is_owasp_staff=True)
5 changes: 5 additions & 0 deletions backend/apps/nest/models/user_badge.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,8 @@ class Meta:
def __str__(self) -> str:
"""Return a human-readable representation of the user badge."""
return f"{self.user.login} - {self.badge.name}"

@staticmethod
def bulk_save(user_badges, fields=None) -> None: # type: ignore[override]
"""Bulk save user badges."""
BulkSaveModel.bulk_save(UserBadge, user_badges, fields=fields)
Binary file not shown.
2 changes: 1 addition & 1 deletion backend/tests/apps/github/models/organization_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def test_bulk_save(self):
mock_org = [Mock(id=None), Mock(id=1)]
with patch("apps.common.models.BulkSaveModel.bulk_save") as mock_bulk_save:
Organization.bulk_save(mock_org)
mock_bulk_save.assert_called_once_with(Organization, mock_org)
mock_bulk_save.assert_called_once_with(Organization, mock_org, fields=None)

@patch("apps.github.models.organization.Organization.objects.get")
def test_update_data(self, mock_get):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Tests for base badge command."""

from io import StringIO
from unittest.mock import MagicMock, patch

import pytest
from django.test import SimpleTestCase

from apps.nest.management.commands.base_badge_command import BaseBadgeCommand


class MockCommand(BaseBadgeCommand):
badge_css_class = "fa-test"
badge_description = "Test"
badge_name = "Test Badge"
badge_weight = 50

def get_eligible_users(self):
return MagicMock()


class TestBaseBadgeCommand(SimpleTestCase):
def test_requires_badge_name(self):
class NoName(BaseBadgeCommand):
def get_eligible_users(self):
return MagicMock()

with pytest.raises(ValueError, match="Badge name"):
NoName().handle()

@patch("apps.nest.management.commands.base_badge_command.UserBadge")
@patch("apps.nest.management.commands.base_badge_command.Badge")
def test_syncs_badge(self, mock_badge, mock_user_badge):
badge = MagicMock()
badge.name = "Test Badge"
mock_badge.objects.get_or_create.return_value = (badge, False)

qs = MagicMock()
qs.exclude.return_value = []
MockCommand.get_eligible_users = MagicMock(return_value=qs)

mock_user_badge.objects.filter.return_value.exclude.return_value.count.return_value = 0

out = StringIO()
cmd = MockCommand()
cmd.stdout = out
cmd.handle()

output = out.getvalue()
assert "Test Badge" in output
assert "synced successfully" in output
Loading