Skip to content

Commit 1a25b2d

Browse files
committed
Implement Award model with bulk operations
1 parent d9665e0 commit 1a25b2d

File tree

2 files changed

+92
-136
lines changed

2 files changed

+92
-136
lines changed

backend/apps/owasp/management/commands/owasp_sync_awards.py

Lines changed: 41 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -82,43 +82,9 @@ def handle(self, *args, **options):
8282
def _process_awards_data(self, awards_data: list[dict], *, dry_run: bool = False):
8383
"""Process the awards data from YAML."""
8484
for item in awards_data:
85-
if item.get("type") == "category":
86-
self._process_category(item, dry_run=dry_run)
87-
elif item.get("type") == "award":
85+
if item.get("type") == "award":
8886
self._process_award(item, dry_run=dry_run)
8987

90-
def _process_category(self, category_data: dict, *, dry_run: bool = False):
91-
"""Process a category definition."""
92-
category_name = category_data.get("title", "")
93-
description = category_data.get("description", "")
94-
95-
if not category_name:
96-
logger.warning("Skipping category with no title")
97-
return
98-
99-
# Create or update category record
100-
if not dry_run:
101-
award, created = Award.objects.get_or_create(
102-
name=category_name,
103-
award_type="category",
104-
defaults={
105-
"category": category_name,
106-
"description": description,
107-
},
108-
)
109-
110-
if created:
111-
self.awards_created += 1
112-
logger.debug("Created category: %s", category_name)
113-
else:
114-
# Update existing category
115-
award.description = description
116-
award.save(update_fields=["description", "nest_updated_at"])
117-
self.awards_updated += 1
118-
logger.debug("Updated category: %s", category_name)
119-
else:
120-
logger.debug("[DRY RUN] Would process category: %s", category_name)
121-
12288
def _process_award(self, award_data: dict, *, dry_run: bool = False):
12389
"""Process an individual award."""
12490
award_name = award_data.get("title", "")
@@ -130,30 +96,31 @@ def _process_award(self, award_data: dict, *, dry_run: bool = False):
13096
logger.warning("Skipping incomplete award: %s", award_data)
13197
return
13298

133-
# Process each winner
99+
# Process each winner using the model's update_data method
134100
for winner_data in winners:
135-
self._process_winner(award_name, category, year, winner_data, dry_run=dry_run)
136-
137-
def _process_winner(
138-
self,
139-
award_name: str,
140-
category: str,
141-
year: int,
142-
winner_data: dict,
143-
*,
144-
dry_run: bool = False,
145-
):
101+
# Prepare winner data with award context
102+
winner_with_context = {
103+
"title": award_name,
104+
"category": category,
105+
"year": year,
106+
"name": winner_data.get("name", ""),
107+
"info": winner_data.get("info", ""),
108+
"image": winner_data.get("image", ""),
109+
}
110+
111+
self._process_winner(winner_with_context, dry_run=dry_run)
112+
113+
def _process_winner(self, winner_data: dict, *, dry_run: bool = False):
146114
"""Process an individual award winner."""
147115
winner_name = winner_data.get("name", "").strip()
148-
winner_info = winner_data.get("info", "")
149-
winner_image = winner_data.get("image", "")
116+
award_name = winner_data.get("title", "")
150117

151118
if not winner_name:
152119
logger.warning("Skipping winner with no name for award: %s", award_name)
153120
return
154121

155122
# Try to match winner with existing user
156-
matched_user = self._match_user(winner_name, winner_info)
123+
matched_user = self._match_user(winner_name, winner_data.get("info", ""))
157124

158125
if matched_user:
159126
self.users_matched += 1
@@ -164,42 +131,33 @@ def _process_winner(
164131
logger.warning("Could not match user: %s", winner_name)
165132

166133
if not dry_run:
167-
# Create or update award record
168-
award, created = Award.objects.get_or_create(
169-
name=award_name,
170-
category=category,
171-
year=year,
172-
winner_name=winner_name,
173-
defaults={
174-
"award_type": "award",
175-
"description": "",
176-
"winner_info": winner_info,
177-
"winner_image": winner_image,
178-
"user": matched_user,
179-
},
180-
)
181-
182-
if created:
134+
# Check if award exists before update
135+
try:
136+
Award.objects.get(
137+
name=award_name,
138+
category=winner_data.get("category", ""),
139+
year=winner_data.get("year"),
140+
winner_name=winner_name,
141+
)
142+
is_new = False
143+
except Award.DoesNotExist:
144+
is_new = True
145+
146+
# Use the model's update_data method
147+
award = Award.update_data(winner_data, save=True)
148+
149+
# Update user association if matched
150+
if matched_user and award.user != matched_user:
151+
award.user = matched_user
152+
award.save(update_fields=["user", "nest_updated_at"])
153+
154+
# Track creation/update stats
155+
if is_new:
183156
self.awards_created += 1
184157
logger.debug("Created award: %s for %s", award_name, winner_name)
185158
else:
186-
# Update existing award
187-
updated_fields = []
188-
if award.winner_info != winner_info:
189-
award.winner_info = winner_info
190-
updated_fields.append("winner_info")
191-
if award.winner_image != winner_image:
192-
award.winner_image = winner_image
193-
updated_fields.append("winner_image")
194-
if award.user != matched_user:
195-
award.user = matched_user
196-
updated_fields.append("user")
197-
198-
if updated_fields:
199-
updated_fields.append("nest_updated_at")
200-
award.save(update_fields=updated_fields)
201-
self.awards_updated += 1
202-
logger.debug("Updated award: %s for %s", award_name, winner_name)
159+
self.awards_updated += 1
160+
logger.debug("Updated award: %s for %s", award_name, winner_name)
203161
else:
204162
logger.debug("[DRY RUN] Would process winner: %s for %s", winner_name, award_name)
205163

backend/apps/owasp/models/award.py

Lines changed: 51 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
from django.db import models
66

7-
from apps.common.models import TimestampedModel
7+
from apps.common.models import BulkSaveModel, TimestampedModel
88

99

10-
class Award(TimestampedModel):
10+
class Award(BulkSaveModel, TimestampedModel):
1111
"""OWASP Award model.
1212
1313
Represents OWASP awards based on the canonical source at:
@@ -120,63 +120,61 @@ def get_user_waspy_awards(cls, user):
120120
return cls.objects.filter(user=user, category=cls.Category.WASPY)
121121

122122
@staticmethod
123-
def update_data(award_data: dict, *, save: bool = True):
124-
"""Update award data from YAML structure.
123+
def bulk_save(awards, fields=None) -> None: # type: ignore[override]
124+
"""Bulk save awards."""
125+
BulkSaveModel.bulk_save(Award, awards, fields=fields)
126+
127+
@staticmethod
128+
def update_data(award_data: dict, *, save: bool = True) -> Award:
129+
"""Update award data.
125130
126131
Args:
127-
award_data: Dictionary containing award data from YAML
132+
award_data: Dictionary containing single award winner data
128133
save: Whether to save the award instance
129134
130135
Returns:
131-
Award instance or list of Award instances
136+
Award instance
132137
133138
"""
134-
if award_data.get("type") == "award":
135-
return Award._create_awards_from_winners(award_data, save=save)
136-
return None
137-
138-
@staticmethod
139-
def _create_awards_from_winners(award_data: dict, *, save: bool = True):
140-
"""Create award instances for each winner."""
141-
awards = []
142-
award_name = award_data.get("title", "")
139+
# Create unique identifier for get_or_create
140+
name = award_data.get("title", "")
143141
category = award_data.get("category", "")
144142
year = award_data.get("year")
145-
winners = award_data.get("winners", [])
146-
147-
for winner_data in winners:
148-
winner_name = winner_data.get("name", "").strip()
149-
if not winner_name:
150-
continue
151-
152-
award_defaults = {
153-
"description": "",
154-
"winner_info": winner_data.get("info", ""),
155-
"winner_image_url": winner_data.get("image", ""),
156-
}
157-
158-
if save:
159-
award, created = Award.objects.get_or_create(
160-
name=award_name,
161-
category=category,
162-
year=year,
163-
winner_name=winner_name,
164-
defaults=award_defaults,
165-
)
166-
if not created:
167-
# Update existing award
168-
for field, value in award_defaults.items():
169-
setattr(award, field, value)
170-
award.save()
171-
else:
172-
award = Award(
173-
name=award_name,
174-
category=category,
175-
year=year,
176-
winner_name=winner_name,
177-
**award_defaults,
178-
)
179-
180-
awards.append(award)
181-
182-
return awards
143+
winner_name = award_data.get("name", "").strip()
144+
145+
try:
146+
award = Award.objects.get(
147+
name=name,
148+
category=category,
149+
year=year,
150+
winner_name=winner_name,
151+
)
152+
except Award.DoesNotExist:
153+
award = Award(
154+
name=name,
155+
category=category,
156+
year=year,
157+
winner_name=winner_name,
158+
)
159+
160+
award.from_dict(award_data)
161+
if save:
162+
award.save()
163+
164+
return award
165+
166+
def from_dict(self, data: dict) -> None:
167+
"""Update instance based on dict data.
168+
169+
Args:
170+
data: Dictionary containing award data
171+
172+
"""
173+
fields = {
174+
"description": data.get("description", ""),
175+
"winner_info": data.get("info", ""),
176+
"winner_image_url": data.get("image", ""),
177+
}
178+
179+
for key, value in fields.items():
180+
setattr(self, key, value)

0 commit comments

Comments
 (0)