Skip to content

Commit 15392bf

Browse files
committed
Sync OWASP Awards data and integrate with user profiles
1 parent 4479525 commit 15392bf

File tree

15 files changed

+1009
-48
lines changed

15 files changed

+1009
-48
lines changed

backend/Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
include backend/apps/ai/Makefile
22
include backend/apps/github/Makefile
3+
include backend/apps/nest/Makefile
34
include backend/apps/owasp/Makefile
45
include backend/apps/slack/Makefile
56

@@ -109,6 +110,7 @@ shell-db:
109110
sync-data: \
110111
update-data \
111112
enrich-data \
113+
nest-update-user-badges \
112114
index-data
113115

114116
test-backend:
@@ -134,6 +136,7 @@ update-data: \
134136
github-update-users \
135137
owasp-aggregate-projects \
136138
owasp-update-events \
139+
owasp-sync-awards \
137140
owasp-sync-posts \
138141
owasp-update-sponsors \
139142
slack-sync-data

backend/apps/nest/Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
nest-update-user-badges:
2+
@echo "Updating user badges"
3+
@CMD="python manage.py update_user_badges" $(MAKE) exec-backend-command

backend/apps/nest/admin/badge.py

Lines changed: 5 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,15 @@
1-
"""Admin configuration for the Badge model in the OWASP app."""
1+
"""Badge admin configuration."""
22

33
from django.contrib import admin
44

55
from apps.nest.models.badge import Badge
66

77

8+
@admin.register(Badge)
89
class BadgeAdmin(admin.ModelAdmin):
910
"""Admin for Badge model."""
1011

11-
fieldsets = (
12-
(
13-
"Basic Information",
14-
{
15-
"fields": (
16-
"name",
17-
"description",
18-
"weight",
19-
)
20-
},
21-
),
22-
("Display Settings", {"fields": ("css_class",)}),
23-
(
24-
"Timestamps",
25-
{
26-
"fields": (
27-
"nest_created_at",
28-
"nest_updated_at",
29-
)
30-
},
31-
),
32-
)
33-
list_display = (
34-
"name",
35-
"description",
36-
"weight",
37-
"css_class",
38-
"nest_created_at",
39-
"nest_updated_at",
40-
)
12+
list_display = ("name", "description", "weight", "css_class")
4113
list_filter = ("weight",)
42-
ordering = (
43-
"weight",
44-
"name",
45-
)
46-
readonly_fields = (
47-
"nest_created_at",
48-
"nest_updated_at",
49-
)
50-
search_fields = (
51-
"css_class",
52-
"description",
53-
"name",
54-
)
55-
56-
57-
admin.site.register(Badge, BadgeAdmin)
14+
search_fields = ("name", "description")
15+
ordering = ("weight", "name")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Nest app management commands."""
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Nest app management commands."""
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
"""A command to update user badges based on awards and other achievements."""
2+
3+
import logging
4+
5+
from django.core.management.base import BaseCommand
6+
7+
from apps.github.models.user import User
8+
from apps.nest.models.badge import BadgeType, UserBadge
9+
from apps.owasp.models.award import Award
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
class Command(BaseCommand):
15+
"""Update user badges based on awards and achievements."""
16+
17+
help = "Update user badges based on OWASP awards and other achievements"
18+
19+
def __init__(self, *args, **kwargs):
20+
"""Initialize the command with counters for tracking changes."""
21+
super().__init__(*args, **kwargs)
22+
self.badges_created = 0
23+
self.badges_removed = 0
24+
self.users_processed = 0
25+
26+
def add_arguments(self, parser):
27+
"""Add command arguments."""
28+
parser.add_argument(
29+
"--dry-run",
30+
action="store_true",
31+
help="Run without making changes to the database",
32+
)
33+
parser.add_argument(
34+
"--verbose",
35+
action="store_true",
36+
help="Enable verbose logging",
37+
)
38+
parser.add_argument(
39+
"--user-login",
40+
type=str,
41+
help="Process badges for a specific user (by GitHub login)",
42+
)
43+
44+
def handle(self, *args, **options):
45+
"""Handle the command execution."""
46+
if options["verbose"]:
47+
logger.setLevel(logging.DEBUG)
48+
49+
self.stdout.write("Starting user badges update...")
50+
51+
try:
52+
# Ensure badge types exist
53+
self._ensure_badge_types_exist(dry_run=options["dry_run"])
54+
55+
# Process users
56+
if options["user_login"]:
57+
self._process_single_user(options["user_login"], dry_run=options["dry_run"])
58+
else:
59+
self._process_all_users(dry_run=options["dry_run"])
60+
61+
# Print summary
62+
self._print_summary()
63+
64+
except Exception as e:
65+
logger.exception("Error updating user badges")
66+
self.stdout.write(self.style.ERROR(f"Error updating user badges: {e!s}"))
67+
68+
def _ensure_badge_types_exist(self, *, dry_run: bool = False):
69+
"""Ensure required badge types exist in the database."""
70+
badge_types = [
71+
{
72+
"name": "WASPY Award Winner",
73+
"description": "Awarded to users who have received any WASPY award from OWASP",
74+
"icon": "🏆",
75+
"color": "#FFD700",
76+
},
77+
# Add more badge types here as needed
78+
]
79+
80+
for badge_data in badge_types:
81+
if not dry_run:
82+
badge_type, created = BadgeType.objects.get_or_create(
83+
name=badge_data["name"],
84+
defaults={
85+
"description": badge_data["description"],
86+
"icon": badge_data["icon"],
87+
"color": badge_data["color"],
88+
"is_active": True,
89+
},
90+
)
91+
if created:
92+
logger.debug("Created badge type: %s", badge_type.name)
93+
else:
94+
# Update existing badge type
95+
badge_type.description = badge_data["description"]
96+
badge_type.icon = badge_data["icon"]
97+
badge_type.color = badge_data["color"]
98+
badge_type.save(
99+
update_fields=["description", "icon", "color", "nest_updated_at"]
100+
)
101+
logger.debug("Updated badge type: %s", badge_type.name)
102+
else:
103+
logger.debug("[DRY RUN] Would ensure badge type exists: %s", badge_data["name"])
104+
105+
def _process_all_users(self, *, dry_run: bool = False):
106+
"""Process badges for all users."""
107+
# Get all users who have awards or existing badges
108+
users_with_awards = set(
109+
Award.objects.filter(user__isnull=False).values_list("user_id", flat=True).distinct()
110+
)
111+
112+
users_with_badges = set(UserBadge.objects.values_list("user_id", flat=True).distinct())
113+
114+
all_user_ids = users_with_awards | users_with_badges
115+
116+
if not all_user_ids:
117+
self.stdout.write("No users found with awards or badges to process.")
118+
return
119+
120+
users = User.objects.filter(id__in=all_user_ids).prefetch_related("awards", "badges")
121+
122+
for user in users:
123+
self._process_user_badges(user, dry_run=dry_run)
124+
self.users_processed += 1
125+
126+
def _process_single_user(self, user_login: str, *, dry_run: bool = False):
127+
"""Process badges for a single user."""
128+
try:
129+
user = User.objects.prefetch_related("awards", "badges").get(login=user_login)
130+
self._process_user_badges(user, dry_run=dry_run)
131+
self.users_processed += 1
132+
except User.DoesNotExist:
133+
self.stdout.write(self.style.ERROR(f"User with login '{user_login}' not found"))
134+
135+
def _process_user_badges(self, user: User, *, dry_run: bool = False):
136+
"""Process badges for a specific user."""
137+
logger.debug("Processing badges for user: %s", user.login)
138+
139+
# Process WASPY Award Winner badge
140+
self._process_waspy_award_badge(user, dry_run=dry_run)
141+
142+
# Add more badge processing logic here as needed
143+
144+
def _process_waspy_award_badge(self, user: User, *, dry_run: bool = False):
145+
"""Process WASPY Award Winner badge for a user."""
146+
badge_name = "WASPY Award Winner"
147+
148+
# Check if user has any WASPY awards
149+
waspy_awards = user.awards.filter(category="WASPY", award_type="award")
150+
151+
has_waspy_award = waspy_awards.exists()
152+
153+
if not dry_run:
154+
try:
155+
badge_type = BadgeType.objects.get(name=badge_name)
156+
except BadgeType.DoesNotExist:
157+
logger.exception("Badge type '%s' not found", badge_name)
158+
return
159+
160+
# Check if user already has this badge
161+
existing_badge = UserBadge.objects.filter(user=user, badge_type=badge_type).first()
162+
163+
if has_waspy_award and not existing_badge:
164+
# Award the badge
165+
award_details = [
166+
{
167+
"award_name": award.name,
168+
"year": award.year,
169+
"winner_name": award.winner_name,
170+
}
171+
for award in waspy_awards
172+
]
173+
174+
award_names_str = ", ".join(
175+
[f"{a['award_name']} ({a['year']})" for a in award_details]
176+
)
177+
UserBadge.objects.create(
178+
user=user,
179+
badge_type=badge_type,
180+
reason=f"Received WASPY award(s): {award_names_str}",
181+
metadata={
182+
"awards": award_details,
183+
"award_count": len(award_details),
184+
},
185+
)
186+
self.badges_created += 1
187+
logger.debug("Awarded '%s' badge to %s", badge_name, user.login)
188+
189+
elif not has_waspy_award and existing_badge:
190+
# Remove the badge (award no longer associated)
191+
existing_badge.delete()
192+
self.badges_removed += 1
193+
logger.debug("Removed '%s' badge from %s", badge_name, user.login)
194+
195+
elif has_waspy_award and existing_badge:
196+
# Update badge metadata if needed
197+
award_details = []
198+
for award in waspy_awards:
199+
award_details.append(
200+
{
201+
"award_name": award.name,
202+
"year": award.year,
203+
"winner_name": award.winner_name,
204+
}
205+
)
206+
207+
new_metadata = {
208+
"awards": award_details,
209+
"award_count": len(award_details),
210+
}
211+
212+
if existing_badge.metadata != new_metadata:
213+
award_names_str = ", ".join(
214+
[f"{a['award_name']} ({a['year']})" for a in award_details]
215+
)
216+
existing_badge.metadata = new_metadata
217+
existing_badge.reason = f"Received WASPY award(s): {award_names_str}"
218+
existing_badge.save(update_fields=["metadata", "reason", "nest_updated_at"])
219+
logger.debug("Updated '%s' badge metadata for %s", badge_name, user.login)
220+
221+
# Dry run logging
222+
elif has_waspy_award:
223+
award_names = list(waspy_awards.values_list("name", "year"))
224+
logger.debug(
225+
"[DRY RUN] Would award/update '%s' badge to %s for awards: %s",
226+
badge_name,
227+
user.login,
228+
award_names,
229+
)
230+
else:
231+
logger.debug("[DRY RUN] Would check for badge removal for %s", user.login)
232+
233+
def _print_summary(self):
234+
"""Print command execution summary."""
235+
self.stdout.write("\n" + "=" * 50)
236+
self.stdout.write("User Badges Update Summary")
237+
self.stdout.write("=" * 50)
238+
self.stdout.write(f"Users processed: {self.users_processed}")
239+
self.stdout.write(f"Badges created: {self.badges_created}")
240+
self.stdout.write(f"Badges removed: {self.badges_removed}")
241+
self.stdout.write("\nBadge update completed successfully!")

backend/apps/nest/models/badge.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,4 @@ class Meta:
3838

3939
def __str__(self) -> str:
4040
"""Return the badge string representation."""
41-
return self.name
41+
return self.name

backend/apps/owasp/Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ owasp-process-snapshots:
2626
@echo "Processing OWASP snapshots"
2727
@CMD="python manage.py owasp_process_snapshots" $(MAKE) exec-backend-command
2828

29+
owasp-sync-awards:
30+
@echo "Syncing OWASP awards data"
31+
@CMD="python manage.py owasp_sync_awards" $(MAKE) exec-backend-command
32+
2933
owasp-update-project-health-metrics:
3034
@echo "Updating OWASP project health metrics"
3135
@CMD="python manage.py owasp_update_project_health_metrics" $(MAKE) exec-backend-command

backend/apps/owasp/admin/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from apps.owasp.models.project_health_requirements import ProjectHealthRequirements
66

7+
from .award import AwardAdmin
78
from .chapter import ChapterAdmin
89
from .committee import CommitteeAdmin
910
from .event import EventAdmin

0 commit comments

Comments
 (0)