Skip to content

Commit 4f93b7c

Browse files
committed
Sync OWASP Awards data and integrate with user profiles
1 parent 7008341 commit 4f93b7c

File tree

17 files changed

+1208
-0
lines changed

17 files changed

+1208
-0
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/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Nest app admin."""
22

33
from .api_key import ApiKeyAdmin
4+
from .badge import BadgeTypeAdmin, UserBadgeAdmin
45
from .user import UserAdmin

backend/apps/nest/admin/badge.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""Badge admin configuration."""
2+
3+
from django.contrib import admin
4+
5+
from apps.nest.models.badge import BadgeType, UserBadge
6+
7+
8+
@admin.register(BadgeType)
9+
class BadgeTypeAdmin(admin.ModelAdmin):
10+
"""Admin for BadgeType model."""
11+
12+
list_display = (
13+
"name",
14+
"description",
15+
"icon",
16+
"color",
17+
"is_active",
18+
"nest_created_at",
19+
"nest_updated_at",
20+
)
21+
list_filter = ("is_active",)
22+
search_fields = ("name", "description")
23+
ordering = ("name",)
24+
25+
fieldsets = (
26+
("Basic Information", {"fields": ("name", "description", "is_active")}),
27+
(
28+
"Appearance",
29+
{
30+
"fields": ("icon", "color"),
31+
"classes": ("collapse",),
32+
},
33+
),
34+
(
35+
"Timestamps",
36+
{
37+
"fields": ("nest_created_at", "nest_updated_at"),
38+
"classes": ("collapse",),
39+
},
40+
),
41+
)
42+
43+
readonly_fields = ("nest_created_at", "nest_updated_at")
44+
45+
46+
@admin.register(UserBadge)
47+
class UserBadgeAdmin(admin.ModelAdmin):
48+
"""Admin for UserBadge model."""
49+
50+
list_display = (
51+
"user",
52+
"badge_type",
53+
"earned_at",
54+
"reason",
55+
"nest_created_at",
56+
)
57+
list_filter = (
58+
"badge_type",
59+
"earned_at",
60+
)
61+
search_fields = (
62+
"user__login",
63+
"user__name",
64+
"badge_type__name",
65+
"reason",
66+
)
67+
ordering = ("-earned_at",)
68+
69+
autocomplete_fields = ("user", "badge_type")
70+
71+
fieldsets = (
72+
("Badge Information", {"fields": ("user", "badge_type", "earned_at", "reason")}),
73+
(
74+
"Metadata",
75+
{
76+
"fields": ("metadata",),
77+
"classes": ("collapse",),
78+
},
79+
),
80+
(
81+
"Timestamps",
82+
{
83+
"fields": ("nest_created_at", "nest_updated_at"),
84+
"classes": ("collapse",),
85+
},
86+
),
87+
)
88+
89+
readonly_fields = ("earned_at", "nest_created_at", "nest_updated_at")
90+
91+
def get_queryset(self, request):
92+
"""Optimize queryset with select_related."""
93+
return super().get_queryset(request).select_related("user", "badge_type")
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/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from .api_key import ApiKey
2+
from .badge import BadgeType, UserBadge
23
from .user import User

0 commit comments

Comments
 (0)