@@ -58,12 +58,20 @@ def handle(self, *args, **options):
5858 self .stdout .write (self .style .ERROR ("Failed to download awards.yml from GitHub" ))
5959 return
6060
61- awards_data = yaml .safe_load (yaml_content )
61+ try :
62+ awards_data = yaml .safe_load (yaml_content )
63+ except yaml .YAMLError as e :
64+ self .stdout .write (self .style .ERROR (f"Failed to parse awards.yml: { e } " ))
65+ return
6266
6367 if not awards_data :
6468 self .stdout .write (self .style .ERROR ("Failed to parse awards.yml content" ))
6569 return
6670
71+ if not isinstance (awards_data , list ):
72+ self .stdout .write (self .style .ERROR ("awards.yml root must be a list of entries" ))
73+ return
74+
6775 # Process awards data
6876 if options ["dry_run" ]:
6977 self .stdout .write (self .style .WARNING ("DRY RUN MODE - No changes will be made" ))
@@ -75,6 +83,10 @@ def handle(self, *args, **options):
7583 # Print summary
7684 self ._print_summary ()
7785
86+ # Update badges after successful sync
87+ if not options ["dry_run" ]:
88+ self ._update_badges ()
89+
7890 except Exception as e :
7991 logger .exception ("Error syncing awards" )
8092 self .stdout .write (self .style .ERROR (f"Error syncing awards: { e !s} " ))
@@ -90,6 +102,7 @@ def _process_award(self, award_data: dict, *, dry_run: bool = False):
90102 award_name = award_data .get ("title" , "" )
91103 category = award_data .get ("category" , "" )
92104 year = award_data .get ("year" )
105+ award_description = award_data .get ("description" , "" ) or ""
93106 winners = award_data .get ("winners" , [])
94107
95108 if not award_name or not category or not year :
@@ -106,6 +119,7 @@ def _process_award(self, award_data: dict, *, dry_run: bool = False):
106119 "name" : winner_data .get ("name" , "" ),
107120 "info" : winner_data .get ("info" , "" ),
108121 "image" : winner_data .get ("image" , "" ),
122+ "description" : award_description ,
109123 }
110124
111125 self ._process_winner (winner_with_context , dry_run = dry_run )
@@ -138,6 +152,8 @@ def _process_winner(self, winner_data: dict, *, dry_run: bool = False):
138152 is_new = False
139153 except Award .DoesNotExist :
140154 is_new = True
155+ except Award .MultipleObjectsReturned :
156+ is_new = False
141157
142158 # Use the model's update_data method
143159 award = Award .update_data (winner_data , save = True )
@@ -239,15 +255,29 @@ def _extract_github_username(self, text: str) -> str | None:
239255 if not text :
240256 return None
241257
242- # Pattern 1: github.com/username
243- github_url_pattern = r"github\.com/([a-zA-Z0-9\-_]+)"
258+ # Pattern 1: github.com/<username> (exclude known non-user segments)
259+ excluded = {
260+ "orgs" ,
261+ "organizations" ,
262+ "topics" ,
263+ "enterprise" ,
264+ "marketplace" ,
265+ "settings" ,
266+ "apps" ,
267+ "features" ,
268+ "pricing" ,
269+ "sponsors" ,
270+ }
271+ github_url_pattern = r"(?:https?://)?(?:www\.)?github\.com/([A-Za-z0-9-]+)(?=[/\s]|$)"
244272 match = re .search (github_url_pattern , text , re .IGNORECASE )
245273 if match :
246- return match .group (1 )
274+ candidate = match .group (1 )
275+ if candidate .lower () not in excluded :
276+ return candidate
247277
248- # Pattern 2: @username mentions
249- mention_pattern = r"@([a-zA-Z0-9\-_ ]+)"
250- match = re .search (mention_pattern , text )
278+ # Pattern 2: @username mentions (avoid emails/local-parts)
279+ mention_pattern = r"(?<![A-Za-z0-9._%+-]) @([A-Za-z0-9- ]+)\b "
280+ match = re .search (mention_pattern , text , re . IGNORECASE )
251281 if match :
252282 return match .group (1 )
253283
@@ -259,31 +289,25 @@ def _generate_potential_logins(self, name: str, min_login_length: int = 2) -> li
259289 return []
260290
261291 potential_logins = []
262- clean_name = re .sub (r"[^a-zA-Z0-9\s\-_ ]" , "" , name ).strip ()
292+ clean_name = re .sub (r"[^a-zA-Z0-9\s\-]" , "" , name ).strip ()
263293
264294 # Convert to lowercase and replace spaces
265295 base_variations = [
266296 clean_name .lower ().replace (" " , "" ),
267297 clean_name .lower ().replace (" " , "-" ),
268- clean_name .lower ().replace (" " , "_" ),
269298 ]
270299
271300 # Add variations with different cases
272301 for variation in base_variations :
273- potential_logins .extend (
274- [
275- variation ,
276- variation .replace ("-" , "" ),
277- variation .replace ("_" , "" ),
278- variation .replace ("-" , "_" ),
279- variation .replace ("_" , "-" ),
280- ]
281- )
302+ potential_logins .extend ([variation , variation .replace ("-" , "" )])
282303
283304 # Remove duplicates while preserving order
284305 seen = set ()
285306 unique_logins = []
286307 for login in potential_logins :
308+ # Skip invalid characters for GitHub logins
309+ if "_" in login :
310+ continue
287311 if login and login not in seen and len (login ) >= min_login_length :
288312 seen .add (login )
289313 unique_logins .append (login )
@@ -306,3 +330,15 @@ def _print_summary(self):
306330 self .stdout .write (f" - { name } " )
307331
308332 self .stdout .write ("\n Sync completed successfully!" )
333+
334+ def _update_badges (self ):
335+ """Update user badges based on synced awards."""
336+ from django .core .management import call_command
337+
338+ self .stdout .write ("Updating user badges..." )
339+ try :
340+ call_command ("owasp_update_badges" )
341+ self .stdout .write ("Badge update completed successfully!" )
342+ except Exception as e :
343+ logger .exception ("Error updating badges" )
344+ self .stdout .write (self .style .ERROR (f"Error updating badges: { e !s} " ))
0 commit comments