Skip to content

Conversation

trucodd
Copy link
Contributor

@trucodd trucodd commented Aug 10, 2025

Proposed change

Resolves #1766

Awards are synced from canonical source
Users are matched to awards automatically
Data is stored and accessible via Award model
Integrated into daily sync pipeline

@trucodd trucodd requested a review from arkid15r as a code owner August 10, 2025 09:36
Copy link
Contributor

coderabbitai bot commented Aug 10, 2025

Summary by CodeRabbit

  • New Features

    • Introduced OWASP Awards support and synchronization.
    • Automatically assigns and updates the “WASPY Award Winner” badge to verified award recipients.
    • Ensures badges are removed when users no longer qualify.
  • Chores

    • Updated backend tasks to include award syncing and badge updates in data update flows.
  • Admin

    • Added Awards management in the admin with search, filters, bulk review actions, and improved display.
    • Simplified Badge admin with cleaner list view and registration.

Walkthrough

Adds an Award model with migrations, admin, tests, and two management commands (awards sync and badge updater); integrates new commands into Makefiles; removes a prior badge migration and updates a merge migration; simplifies the Badge admin UI.

Changes

Cohort / File(s) Summary
Makefile integration
backend/Makefile, backend/apps/owasp/Makefile
Added owasp-update-badges and owasp-sync-awards targets and wired them into sync-data / update-data target chains.
Award model & exports
backend/apps/owasp/models/award.py, backend/apps/owasp/models/__init__.py
New Award model (category, name unique, description, year, winner_*, winner_image_url, user FK, is_reviewed, timestamps) with helpers (from_dict, update_data, bulk_save) and exported from models package.
Migrations: add award / adjust merges
backend/apps/owasp/migrations/0045_award.py, backend/apps/owasp/migrations/0046_merge_0045_badge_0045_project_audience.py, backend/apps/owasp/migrations/0049_award_unique_constraint.py, backend/apps/owasp/migrations/0045_badge.py (deleted)
Added migration creating owasp_awards; removed prior 0045_badge migration; updated merge migration dependency to 0045_award; added a no-op 0049 noting uniqueness.
Award admin
backend/apps/owasp/admin/award.py, backend/apps/owasp/admin/__init__.py
New AwardAdmin registered via decorator with list_display, filters, search, ordering, autocomplete, actions (mark_reviewed/mark_not_reviewed), fieldsets, readonly timestamps; exported from admin package.
Management commands
backend/apps/owasp/management/commands/owasp_sync_awards.py, backend/apps/owasp/management/commands/owasp_update_badges.py
owasp_sync_awards: fetches/parses awards.yml, creates/updates Award records, multi-strategy user matching, dry-run/verbose, transactional writes, prints summary and triggers badge update. owasp_update_badges: ensures WASPY badge exists and assigns/removes badge based on reviewed WASPY awards.
Badge admin cleanup
backend/apps/nest/admin/badge.py
Converted to decorator registration, removed fieldsets/readonly_fields, simplified list_display, search_fields, and ordering.
Tests & test packages
backend/tests/apps/owasp/models/award_test.py, backend/tests/apps/owasp/models/award_duplicate_handling_test.py, backend/tests/apps/owasp/management/commands/owasp_update_badges_test.py, backend/tests/.../__init__.py (several)
Added unit tests for Award model (import, choices, meta), duplicate-handling, and owasp_update_badges command; added docstring-only __init__ files for test packages.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
Create Award model aligned with awards.yml with fields: category, name (unique), description, year, user, timestamps (#1766)
Implement awards sync command: download/parse YAML, create/update records, match users, log unmatched, idempotent (#1766)
Update badge job to assign/remove “WASPY Award Winner” badge based on Award model (#1766)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Simplified Badge admin registration and UI (backend/apps/nest/admin/badge.py) Admin UI refactor is not required by #1766; unrelated to award-sync functional objectives.
Deletion of badge migration (backend/apps/owasp/migrations/0045_badge.py) Removing a Badge migration alters migration history for badges; this change is orthogonal to the award-sync objectives and may affect migration ordering/compatibility.

Possibly related PRs

Suggested reviewers

  • arkid15r
  • kasya
  • aramattamara

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (23)
backend/apps/owasp/models/__init__.py (1)

1-1: Consider exposing the public API explicitly with all

Importing Award here is fine. To make the intended export explicit, consider adding __all__.

 from .award import Award
+
+__all__ = ["Award"]
backend/apps/nest/Makefile (1)

1-3: Add .PHONY and optional no-op targets to satisfy checkmake.

The target is fine. To address checkmake’s minphony warnings and improve Makefile robustness:

  • Mark the target phony.
  • Optionally add no-op all, clean, test to satisfy the linter without altering behavior.

Apply:

+.PHONY: nest-update-user-badges all clean test
+
+all:
+	@:
+
+clean:
+	@:
+
+test:
+	@:
+
 nest-update-user-badges:
 	@echo "Updating user badges"
 	@CMD="python manage.py update_user_badges" $(MAKE) exec-backend-command

If you prefer to keep this file minimal, confirm whether checkmake is enforced on per-app Makefiles in CI. If it is, the above will resolve the warnings.

backend/apps/nest/models/badge.py (3)

108-110: Prefer award year from metadata over earned_at.year in display_name

earned_at is when the badge record was created, not necessarily the award year. If metadata contains the award year, display that and fall back to earned_at.

Apply this small change:

-        return f"{self.badge_type.name} ({self.earned_at.year})"
+        year = None
+        try:
+            year = self.metadata.get("year")
+        except Exception:
+            # metadata may not be a dict; be defensive
+            year = None
+        return f"{self.badge_type.name} ({year or self.earned_at.year})"

87-89: Consider recording awarded_at in addition to earned_at

If you plan to surface when the award was actually received (vs when the badge row was created), adding an optional awarded_at DateField/DateTimeField improves fidelity and sorting by award history.

I can add an awarded_at field and wire the management command to populate it from Award.year (e.g., Jan 1 of the year) while keeping existing earned_at semantics. Want a PR snippet?


38-44: Optional: validate color format

If you expect hex colors, add a RegexValidator to catch invalid inputs at save time. Otherwise, keep as-is for CSS names too.

Additional code outside this range for context:

from django.core.validators import RegexValidator

-    color = models.CharField(
+    color = models.CharField(
         verbose_name="Color",
         max_length=20,
         blank=True,
         default="#FFD700",
-        help_text="Badge color (hex code or CSS color name)",
+        help_text="Badge color (hex code or CSS color name)",
+        validators=[RegexValidator(regex=r'^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$|^[a-zA-Z]+$')],
     )
backend/tests/apps/nest/management/commands/update_user_badges_test.py (4)

42-48: Use TestCase assertions for clearer failures

Switch bare asserts to unittest/Django assertions for better messages and consistency.

-        assert badge_type.name == "WASPY Award Winner"
+        self.assertEqual(badge_type.name, "WASPY Award Winner")
...
-        assert user_badge.user == self.user
-        assert "Test Person of the Year" in user_badge.reason
+        self.assertEqual(user_badge.user, self.user)
+        self.assertIn("Test Person of the Year", user_badge.reason)

55-56: Prefer assertTrue/assertFalse/assertEqual in remaining tests

Minor consistency/readability improvement across tests.

-        assert UserBadge.objects.filter(user=self.user, badge_type=badge_type).exists()
+        self.assertTrue(UserBadge.objects.filter(user=self.user, badge_type=badge_type).exists())
...
-        assert not UserBadge.objects.filter(user=self.user, badge_type=badge_type).exists()
+        self.assertFalse(UserBadge.objects.filter(user=self.user, badge_type=badge_type).exists())
...
-        assert not BadgeType.objects.filter(name="WASPY Award Winner").exists()
-        assert not UserBadge.objects.filter(user=self.user).exists()
+        self.assertFalse(BadgeType.objects.filter(name="WASPY Award Winner").exists())
+        self.assertFalse(UserBadge.objects.filter(user=self.user).exists())
...
-        assert user_badge.user == self.user
+        self.assertEqual(user_badge.user, self.user)

Also applies to: 65-66, 73-74, 83-84


79-80: Strengthen single-user scope test

Consider adding a second user with an award and assert that user_login limits processing to the target user only.

I can extend this test with a second user+award and assertions if you want a patch.


35-39: Add idempotency test (optional)

Running the command twice should not create duplicate UserBadge rows. Add an assertion on count after two runs.

call_command("update_user_badges", verbosity=0)
call_command("update_user_badges", verbosity=0)
badge_type = BadgeType.objects.get(name="WASPY Award Winner")
self.assertEqual(
    UserBadge.objects.filter(user=self.user, badge_type=badge_type).count(), 1
)
backend/apps/nest/admin/badge.py (1)

46-56: Admin config looks solid; consider small optimizations

Good use of autocomplete and select_related. Two optional tweaks:

  • Use list_select_related = ("user", "badge_type") instead of overriding get_queryset for brevity.
  • Add date_hierarchy = "earned_at" to enable date drill-down.

Additional code outside the selected range for context:

class UserBadgeAdmin(admin.ModelAdmin):
    ...
    list_select_related = ("user", "badge_type")
    date_hierarchy = "earned_at"
    # You can then drop get_queryset if desired.

Also applies to: 57-61, 69-70, 91-93

backend/apps/owasp/admin/award.py (2)

27-33: Add user fields to search for better admin UX

Include user login/name in search_fields to quickly find awards by recipient.

     search_fields = (
         "name",
         "category",
         "winner_name",
         "description",
         "winner_info",
+        "user__login",
+        "user__name",
     )

61-63: Prefer list_select_related over overriding get_queryset

Using list_select_related = ("user",) gives the list view optimization without affecting other admin queries. Optional but cleaner.

-    def get_queryset(self, request):
-        """Optimize queryset with select_related."""
-        return super().get_queryset(request).select_related("user")
+    # Optimize list view queries without overriding get_queryset
+    list_select_related = ("user",)
backend/apps/owasp/models/award.py (3)

46-52: Add basic year validators

Constrain year to a sane range to avoid bad data.

 from django.db import models
+from django.core.validators import MinValueValidator, MaxValueValidator

 ...
     year = models.IntegerField(
         verbose_name="Year",
         null=True,
         blank=True,
         help_text="Year the award was given (null for category definitions)",
+        validators=[MinValueValidator(1990), MaxValueValidator(2100)],
     )

98-106: Guard against None in str for safety

Avoid rendering "(None)" if data inconsistent.

     def __str__(self) -> str:
         """Return string representation of the award."""
         if self.award_type == "category":
             return f"{self.name} (Category)"

         if self.winner_name:
-            return f"{self.name} - {self.winner_name} ({self.year})"
+            return (
+                f"{self.name} - {self.winner_name} ({self.year})"
+                if self.year is not None
+                else f"{self.name} - {self.winner_name}"
+            )

-        return f"{self.name} ({self.year})"
+        return f"{self.name} ({self.year})" if self.year is not None else self.name

75-85: Consider TextChoices for award_type

Using TextChoices improves type-safety and avoids magic strings across the codebase.

backend/apps/nest/management/commands/update_user_badges.py (6)

11-11: Hoist badge name to a constant

Avoid magic strings and typos by centralizing the badge name.

 logger = logging.getLogger(__name__)
 
+BADGE_WASPY_AWARD_WINNER = "WASPY Award Winner"

120-121: Prefetch badge_type to avoid per-user queries

Use badges__badge_type to prevent N+1 when resolving existing badges.

-        users = User.objects.filter(id__in=all_user_ids).prefetch_related("awards", "badges")
+        users = (
+            User.objects.filter(id__in=all_user_ids)
+            .prefetch_related("awards", "badges__badge_type")
+        )

129-130: Apply the same prefetch optimization in single-user path

-            user = User.objects.prefetch_related("awards", "badges").get(login=user_login)
+            user = (
+                User.objects.prefetch_related("awards", "badges__badge_type")
+                .get(login=user_login)
+            )

155-159: Don’t log a stack trace for expected missing badge types

Use warning instead of exception to avoid noisy logs.

-            except BadgeType.DoesNotExist:
-                logger.exception("Badge type '%s' not found", badge_name)
+            except BadgeType.DoesNotExist:
+                logger.warning("Badge type '%s' not found", badge_name)
                 return

161-163: Use prefetched badges to avoid extra query per user

Leverage user.badges.all() to find the existing badge in-memory.

-            existing_badge = UserBadge.objects.filter(user=user, badge_type=badge_type).first()
+            existing_badge = next(
+                (b for b in user.badges.all() if b.badge_type_id == badge_type.id),
+                None,
+            )

221-231: Sort dry-run output for readability and parity

-        elif has_waspy_award:
-            award_names = list(waspy_awards.values_list("name", "year"))
+        elif has_waspy_award:
+            award_names = [(a.name, a.year) for a in awards_list]
backend/apps/owasp/management/commands/owasp_sync_awards.py (2)

53-55: Use a constant for the source URL

Centralize the YAML URL for easier maintenance and testing.

+SOURCE_URL = "https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_data/awards.yml"
 ...
-            yaml_content = get_repository_file_content(
-                "https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_data/awards.yml"
-            )
+            yaml_content = get_repository_file_content(SOURCE_URL)

288-300: Harden GitHub username extraction to avoid org/team URLs and false positives

The current regex may extract “OWASP” from github.com/OWASP/teams/.... Tighten the pattern and ensure word boundaries.

-        github_url_pattern = r"github\.com/([a-zA-Z0-9\-_]+)"
-        match = re.search(github_url_pattern, text, re.IGNORECASE)
+        github_url_pattern = (
+            r"(?:https?://)?(?:www\.)?github\.com/"
+            r"(?!orgs/|topics/|enterprise/|features/|apps/)"
+            r"([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}[A-Za-z0-9])?)\b"
+        )
+        match = re.search(github_url_pattern, text, re.IGNORECASE)
         if match:
             return match.group(1)
 
         # Pattern 2: @username mentions
-        mention_pattern = r"@([a-zA-Z0-9\-_]+)"
+        mention_pattern = r"@([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}[A-Za-z0-9])?)\b"
         match = re.search(mention_pattern, text)
         if match:
             return match.group(1)
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7008341 and 04edc7d.

📒 Files selected for processing (20)
  • backend/Makefile (3 hunks)
  • backend/apps/nest/Makefile (1 hunks)
  • backend/apps/nest/admin/__init__.py (1 hunks)
  • backend/apps/nest/admin/badge.py (1 hunks)
  • backend/apps/nest/management/__init__.py (1 hunks)
  • backend/apps/nest/management/commands/__init__.py (1 hunks)
  • backend/apps/nest/management/commands/update_user_badges.py (1 hunks)
  • backend/apps/nest/migrations/0003_badgetype_userbadge.py (1 hunks)
  • backend/apps/nest/models/__init__.py (1 hunks)
  • backend/apps/nest/models/badge.py (1 hunks)
  • backend/apps/owasp/Makefile (1 hunks)
  • backend/apps/owasp/admin/__init__.py (1 hunks)
  • backend/apps/owasp/admin/award.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_sync_awards.py (1 hunks)
  • backend/apps/owasp/migrations/0045_award.py (1 hunks)
  • backend/apps/owasp/models/__init__.py (1 hunks)
  • backend/apps/owasp/models/award.py (1 hunks)
  • backend/tests/apps/nest/management/__init__.py (1 hunks)
  • backend/tests/apps/nest/management/commands/__init__.py (1 hunks)
  • backend/tests/apps/nest/management/commands/update_user_badges_test.py (1 hunks)
🧰 Additional context used
🪛 checkmake (0.2.2)
backend/apps/nest/Makefile

[warning] 1-1: Missing required phony target "all"

(minphony)


[warning] 1-1: Missing required phony target "clean"

(minphony)


[warning] 1-1: Missing required phony target "test"

(minphony)

🔇 Additional comments (14)
backend/apps/nest/management/__init__.py (1)

1-1: Good addition for Django command discovery

Adding an __init__.py here ensures the management package is recognized. The docstring is fine.

backend/tests/apps/nest/management/__init__.py (1)

1-1: Test package initialization looks good

Having this __init__.py helps keep tests organized as a package. No issues.

backend/apps/nest/management/commands/__init__.py (1)

1-1: Required initializer for commands package

This enables Django to discover commands under management/commands. Looks good.

backend/tests/apps/nest/management/commands/__init__.py (1)

1-1: OK to include for test commands structure

Docstring-only initializer is fine and consistent with the rest of the test modules.

backend/apps/owasp/admin/__init__.py (1)

7-7: LGTM. Ensure registration is decorator-based.

Importing AwardAdmin here is consistent with the existing pattern. Just confirm that backend/apps/owasp/admin/award.py uses @admin.register(Award) (or explicitly calls admin.site.register) so the import triggers registration.

backend/apps/nest/admin/__init__.py (1)

4-4: Badge admin imports look good.

Consistent with the module import pattern used elsewhere to register admin classes.

backend/apps/nest/models/__init__.py (1)

2-2: Model exports added correctly.

Re-exporting BadgeType and UserBadge here is consistent and helpful for imports elsewhere.

backend/apps/owasp/Makefile (1)

25-27: owasp-sync-awards target integration verified

The owasp-sync-awards target is included in the main backend workflow (backend/Makefile:139), so it’s wired up correctly. LGTM.

backend/Makefile (3)

3-3: Good integration of Nest Makefile include

Including backend/apps/nest/Makefile cleanly exposes the new badge target to the root Makefile.


139-139: Awards sync wired into update-data at the right position

Placing owasp-sync-awards within update-data (prior to sync-data’s nest-update-user-badges) ensures award data is fresh before badges are computed.


113-113: Badge update correctly sequenced after data update

Verified that the nest-update-user-badges target is defined in backend/apps/nest/Makefile and invokes:

python manage.py update_user_badges

This ensures it runs after update-data and enrich-data in the sync-data recipe, preserving the intended ordering with the awards sync. ✅

backend/apps/nest/models/badge.py (1)

63-66: Confirm uniqueness semantics for multiple awards per user

The uniqueness constraint user + badge_type enforces a single badge per type. This fits “assign a WASPY Award Winner badge to users who have received WASPY awards.” If we ever need one badge per year (multiple WASPY badges), this constraint will block it.

Would you like a single “winner” badge per user (aggregating multiple years in metadata), or distinct badges per year? If the latter, we should either add year to the uniqueness or introduce an awarded_year field. I can provide a migration if needed.

backend/tests/apps/nest/management/commands/update_user_badges_test.py (1)

21-23: Verify field types for created_at/updated_at

If apps.github.models.user.User uses DateTimeField for created_at/updated_at, pass timezone-aware datetimes instead of strings to avoid implicit casting issues.

from django.utils import timezone

self.user = User.objects.create(
    login="testuser",
    name="Test User",
    email="test@example.com",
    created_at=timezone.now(),
    updated_at=timezone.now(),
)
backend/apps/nest/migrations/0003_badgetype_userbadge.py (1)

14-78: Migration aligns with models and requirements

Tables, constraints (unique user+badge_type), and indexes mirror the model definitions. Dependencies are correct. No issues found.

Also applies to: 79-154

Comment on lines 146 to 152
badge_name = "WASPY Award Winner"

# Check if user has any WASPY awards
waspy_awards = user.awards.filter(category="WASPY", award_type="award")

has_waspy_award = waspy_awards.exists()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Stabilize award ordering to keep badge metadata deterministic

Sorting awards avoids unnecessary badge updates caused by non-deterministic queryset ordering.

-        badge_name = "WASPY Award Winner"
+        badge_name = BADGE_WASPY_AWARD_WINNER
 ...
-        waspy_awards = user.awards.filter(category="WASPY", award_type="award")
-
-        has_waspy_award = waspy_awards.exists()
+        waspy_awards = (
+            user.awards.filter(category="WASPY", award_type="award")
+            .order_by("year", "name", "winner_name")
+        )
+        awards_list = list(waspy_awards)
+        has_waspy_award = bool(awards_list)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
badge_name = "WASPY Award Winner"
# Check if user has any WASPY awards
waspy_awards = user.awards.filter(category="WASPY", award_type="award")
has_waspy_award = waspy_awards.exists()
badge_name = BADGE_WASPY_AWARD_WINNER
# Check if user has any WASPY awards
waspy_awards = (
user.awards.filter(category="WASPY", award_type="award")
.order_by("year", "name", "winner_name")
)
awards_list = list(waspy_awards)
has_waspy_award = bool(awards_list)
🤖 Prompt for AI Agents
In backend/apps/nest/management/commands/update_user_badges.py around lines 146
to 152, the queryset for fetching WASPY awards is not ordered, which can cause
non-deterministic ordering and unnecessary badge updates. Fix this by adding an
explicit order_by clause to the waspy_awards queryset to ensure stable and
deterministic ordering of awards.

Comment on lines 165 to 176
award_details = [
{
"award_name": award.name,
"year": award.year,
"winner_name": award.winner_name,
}
for award in waspy_awards
]

award_names_str = ", ".join(
[f"{a['award_name']} ({a['year']})" for a in award_details]
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Build metadata from the sorted list to keep it stable

-                award_details = [
-                    {
-                        "award_name": award.name,
-                        "year": award.year,
-                        "winner_name": award.winner_name,
-                    }
-                    for award in waspy_awards
-                ]
+                award_details = [
+                    {"award_name": a.name, "year": a.year, "winner_name": a.winner_name}
+                    for a in awards_list
+                ]
 
                 award_names_str = ", ".join(
                     [f"{a['award_name']} ({a['year']})" for a in award_details]
                 )

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In backend/apps/nest/management/commands/update_user_badges.py around lines 165
to 176, the award_details list is built directly from waspy_awards without
sorting, which can cause instability in the output order. To fix this, sort
waspy_awards by a consistent key such as award name or year before constructing
award_details to ensure stable and predictable metadata ordering.

Comment on lines 197 to 206
award_details = []
for award in waspy_awards:
award_details.append(
{
"award_name": award.name,
"year": award.year,
"winner_name": award.winner_name,
}
)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Keep update path consistent with create path and ordering

-                award_details = []
-                for award in waspy_awards:
-                    award_details.append(
-                        {
-                            "award_name": award.name,
-                            "year": award.year,
-                            "winner_name": award.winner_name,
-                        }
-                    )
+                award_details = [
+                    {"award_name": a.name, "year": a.year, "winner_name": a.winner_name}
+                    for a in awards_list
+                ]

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In backend/apps/nest/management/commands/update_user_badges.py between lines 197
and 206, the update logic for award_details should follow the same path and
ordering as the create logic to maintain consistency. Review the create path for
award_details and adjust the update loop to match the same structure and order
of fields, ensuring the data is processed and appended in the same sequence and
format.

Comment on lines 38 to 67
fieldsets = (
(
"Basic Information",
{"fields": ("name", "category", "award_type", "year", "description")},
),
(
"Winner Information",
{
"fields": ("winner_name", "winner_info", "winner_image", "user"),
"classes": ("collapse",),
},
),
(
"Timestamps",
{
"fields": ("nest_created_at", "nest_updated_at"),
"classes": ("collapse",),
},
),
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enforce data integrity between award_type and fields

Admin currently allows inconsistent edits (e.g., award_type='category' with non-null year or winner fields). Add model validation (clean) to enforce:

  • category entries: year is None and winner fields empty
  • award entries: year is not None

I can add a clean() method in the model and wire an admin form to surface validation errors in the UI.

🤖 Prompt for AI Agents
In backend/apps/owasp/admin/award.py around lines 38 to 57, the admin form
allows inconsistent data between award_type and related fields. To fix this, add
a clean() method in the Award model that validates: if award_type is 'category',
then year must be None and winner fields must be empty; if award_type is
'award', then year must not be None. Then, create a custom ModelForm for the
admin that calls this clean() method and surfaces validation errors in the UI by
wiring this form to the AwardAdmin class.

Comment on lines 168 to 180
award, created = Award.objects.get_or_create(
name=award_name,
category=category,
year=year,
winner_name=winner_name,
defaults={
"award_type": "award",
"description": "",
"winner_info": winner_info,
"winner_image": winner_image,
"user": matched_user,
},
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential IntegrityError due to unique constraint not including winner_name

get_or_create uses (name, category, year, winner_name). If the DB has a unique constraint only on (name, year, category), inserting a second winner for the same award/year will fail.

Action:

  • Update model and migration to include winner_name in the unique constraint (see model/migration comments). With that in place, this block is correct and idempotent.
🤖 Prompt for AI Agents
In backend/apps/owasp/management/commands/owasp_sync_awards.py around lines 168
to 180, the get_or_create call uses a unique constraint including winner_name,
but the database unique constraint only covers name, year, and category. To fix
this, update the Award model and its migration files to include winner_name in
the unique_together or UniqueConstraint definition so that the database enforces
uniqueness on (name, category, year, winner_name). After applying this
migration, the current get_or_create usage will be correct and idempotent.

Comment on lines 121 to 118
models.UniqueConstraint(
fields=("name", "year", "category"), name="unique_award_name_year_category"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Migration unique constraint must include winner_name

Align the DB constraint with the model to allow multiple winners per award/year. Without this, inserts will fail when there are multiple winners.

-                    models.UniqueConstraint(
-                        fields=("name", "year", "category"), name="unique_award_name_year_category"
-                    )
+                    models.UniqueConstraint(
+                        fields=("name", "year", "category", "winner_name"),
+                        name="unique_award_name_year_category_winner",
+                    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
models.UniqueConstraint(
fields=("name", "year", "category"), name="unique_award_name_year_category"
)
models.UniqueConstraint(
fields=("name", "year", "category", "winner_name"),
name="unique_award_name_year_category_winner",
)
🤖 Prompt for AI Agents
In backend/apps/owasp/migrations/0045_award.py around lines 121 to 123, the
UniqueConstraint on the award model currently includes only "name", "year", and
"category" fields, which prevents multiple winners for the same
award/year/category. To fix this, add "winner_name" to the fields tuple in the
UniqueConstraint so that the database allows multiple winners per
award/year/category without insert failures.

Comment on lines 26 to 37
constraints = [
models.UniqueConstraint(
fields=["name", "year", "category"], name="unique_award_name_year_category"
)
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Uniqueness is too coarse; blocks multiple winners for the same award/year

Current unique constraint on (name, year, category) prevents multiple winners for a single award/year. The YAML contains multiple winners per award in some cases. This will raise IntegrityError when inserting the second winner.

Update the constraint to include winner_name:

-        constraints = [
-            models.UniqueConstraint(
-                fields=["name", "year", "category"], name="unique_award_name_year_category"
-            )
-        ]
+        constraints = [
+            models.UniqueConstraint(
+                fields=["name", "year", "category", "winner_name"],
+                name="unique_award_name_year_category_winner",
+            )
+        ]

Also update the migration accordingly (see migration comment).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constraints = [
models.UniqueConstraint(
fields=["name", "year", "category"], name="unique_award_name_year_category"
)
]
constraints = [
models.UniqueConstraint(
fields=["name", "year", "category", "winner_name"],
name="unique_award_name_year_category_winner",
)
]
🤖 Prompt for AI Agents
In backend/apps/owasp/models/award.py around lines 26 to 30, the unique
constraint on (name, year, category) is too broad and prevents multiple winners
for the same award and year. Modify the UniqueConstraint to include the field
winner_name so that uniqueness is enforced per winner as well. After updating
the model, generate and update the corresponding migration file to reflect this
change in the database schema.

Copy link
Collaborator

@arkid15r arkid15r left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a decent code 👍
Moreover, I like some of your ideas around badges implementation.

Let's do the following: remove badge related code from the PR while focusing on syncing the awards and matching them to users.

We use badges separately just for displaying some user attributes to visitors

from apps.common.models import TimestampedModel


class BadgeType(TimestampedModel):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Badge model already exists at owasp/models/badge.py

Comment on lines 76 to 69
award_type = models.CharField(
verbose_name="Award Type",
max_length=20,
choices=[
("category", "Category"),
("award", "Award"),
],
default="award",
help_text="Type of entry: category definition or individual award",
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look right, it seems we need Distinguished Lifetime Memberships and WASPY -- please double check.

verbose_name="Award Type",
max_length=20,
choices=[
("category", "Category"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also make the categories a separate subclass (see an example)

verbose_name="Year",
null=True,
blank=True,
help_text="Year the award was given (null for category definitions)",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The category part needs to be hardcoded. I don't think we should create a separate table for them as they rarely change.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After removing that you can focus completely on award entities.

"""
from apps.github.models.user import User

return User.objects.filter(awards__category="WASPY", awards__award_type="award").distinct()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reuse WASPY value after defining a separate choice field for categories.

Comment on lines 33 to 35
category = models.CharField(
verbose_name="Category",
max_length=100,
help_text="Award category (e.g., 'WASPY', 'Lifetime Achievement')",
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
category = models.CharField(
verbose_name="Category",
max_length=100,
help_text="Award category (e.g., 'WASPY', 'Lifetime Achievement')",
)

@trucodd trucodd marked this pull request as draft August 12, 2025 06:15
@trucodd
Copy link
Contributor Author

trucodd commented Aug 12, 2025

The badge model was not there when I started so I created I will refactor to use the existing model and make the other requested changes.

@trucodd trucodd force-pushed the feature/owasp-awards-profilesync branch from 04edc7d to 082c701 Compare August 13, 2025 02:40
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (2)
backend/apps/owasp/models/award.py (1)

33-37: Allow multiple winners per award/year by including winner_name in the unique constraint

As-is, inserting a second winner for the same award/year/category will raise IntegrityError. Include winner_name in the uniqueness constraint.

-        constraints = [
-            models.UniqueConstraint(
-                fields=["name", "year", "category"], name="unique_award_name_year_category"
-            )
-        ]
+        constraints = [
+            models.UniqueConstraint(
+                fields=["name", "year", "category", "winner_name"],
+                name="unique_award_name_year_category_winner",
+            )
+        ]
backend/apps/owasp/migrations/0045_award.py (1)

121-123: Migration unique constraint must include winner_name to allow multiple winners

Keep the DB schema aligned with the model and the YAML reality (multiple winners per award/year).

-                    models.UniqueConstraint(
-                        fields=("name", "year", "category"), name="unique_award_name_year_category"
-                    )
+                    models.UniqueConstraint(
+                        fields=("name", "year", "category", "winner_name"),
+                        name="unique_award_name_year_category_winner",
+                    )
🧹 Nitpick comments (8)
backend/tests/apps/owasp/models/award_test.py (5)

26-26: Use enum for category instead of magic strings

Prefer Award.Category.WASPY for consistency and refactor safety.

-            category="WASPY",
+            category=Award.Category.WASPY,

Also applies to: 43-43, 57-57


33-37: Prefer unittest assertions for clearer failures

Use TestCase assertions to improve failure messages and consistency.

-        assert award.name == "Test Award"
-        assert award.category == "WASPY"
-        assert award.year == 2024
-        assert award.winner_name == "Test User"
-        assert award.user == self.user
+        self.assertEqual(award.name, "Test Award")
+        self.assertEqual(award.category, Award.Category.WASPY)
+        self.assertEqual(award.year, 2024)
+        self.assertEqual(award.winner_name, "Test User")
+        self.assertEqual(award.user, self.user)

50-52: Prefer unittest assertions for membership checks

Use assertIn for clarity.

-        assert self.user in winners
+        self.assertIn(self.user, winners)

64-65: Prefer unittest assertions for membership checks

Use assertIn for clarity.

-        assert award in user_awards
+        self.assertIn(award, user_awards)

14-20: Ensure field types match the User model

created_at/updated_at literals are ISO strings; if the User model uses DateTimeField, parsing may vary. Prefer timezone-aware datetimes to avoid surprises.

Proposed change (outside current range): add an import and use timezone.now():

from django.utils import timezone
...
self.user = User.objects.create(
    login="testuser",
    name="Test User",
    email="test@example.com",
    created_at=timezone.now(),
    updated_at=timezone.now(),
)

Additionally, consider adding a test that asserts multiple winners for the same award/year are allowed (to guard the DB uniqueness fix).

backend/apps/owasp/models/award.py (3)

148-151: Normalize parsed strings from YAML (strip) to avoid accidental duplicates

Trim title and category to prevent whitespace differences from creating distinct rows.

-        award_name = award_data.get("title", "")
-        category = award_data.get("category", "")
+        award_name = (award_data.get("title") or "").strip()
+        category = (award_data.get("category") or "").strip()

136-142: Correct the return type in the docstring

update_data returns a list of Award instances (or None), not a single instance.

-        Returns:
-            Award instance or list of Award instances
+        Returns:
+            list[Award]: when award_data['type'] == 'award'
+            None: otherwise

115-126: Optional: provide deterministic ordering for per-user listings

Adding a default ordering improves UX and test determinism.

-        return cls.objects.filter(user=user, category=cls.Category.WASPY)
+        return cls.objects.filter(user=user, category=cls.Category.WASPY).order_by("-year", "name")
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04edc7d and 082c701.

📒 Files selected for processing (12)
  • backend/Makefile (1 hunks)
  • backend/apps/owasp/Makefile (1 hunks)
  • backend/apps/owasp/admin/__init__.py (1 hunks)
  • backend/apps/owasp/admin/award.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_sync_awards.py (1 hunks)
  • backend/apps/owasp/migrations/0045_award.py (1 hunks)
  • backend/apps/owasp/migrations/0045_badge.py (0 hunks)
  • backend/apps/owasp/models/__init__.py (1 hunks)
  • backend/apps/owasp/models/award.py (1 hunks)
  • backend/tests/apps/nest/management/__init__.py (1 hunks)
  • backend/tests/apps/nest/management/commands/__init__.py (1 hunks)
  • backend/tests/apps/owasp/models/award_test.py (1 hunks)
💤 Files with no reviewable changes (1)
  • backend/apps/owasp/migrations/0045_badge.py
✅ Files skipped from review due to trivial changes (1)
  • backend/tests/apps/nest/management/init.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • backend/apps/owasp/admin/award.py
  • backend/apps/owasp/admin/init.py
  • backend/apps/owasp/models/init.py
  • backend/Makefile
  • backend/tests/apps/nest/management/commands/init.py
  • backend/apps/owasp/Makefile
  • backend/apps/owasp/management/commands/owasp_sync_awards.py
🧰 Additional context used
🧬 Code Graph Analysis (2)
backend/tests/apps/owasp/models/award_test.py (1)
backend/apps/owasp/models/award.py (3)
  • Award (10-188)
  • get_waspy_award_winners (104-113)
  • get_user_waspy_awards (116-126)
backend/apps/owasp/models/award.py (2)
backend/apps/common/models.py (1)
  • TimestampedModel (37-46)
backend/apps/github/api/internal/queries/user.py (1)
  • user (40-56)
🔇 Additional comments (1)
backend/apps/owasp/migrations/0045_award.py (1)

51-58: Model/migration mismatch: year is nullable here but not in the model

Ensure the model matches this (blank=True, null=True) or adjust the migration to null=False to avoid drift. The model change is recommended given category-type entries.

Comment on lines 88 to 92
(
"award_type",
models.CharField(
choices=[("category", "Category"), ("award", "Award")],
default="award",
help_text="Type of entry: category definition or individual award",
max_length=20,
verbose_name="Award Type",
),
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Model/migration mismatch: award_type exists in migration but not in the model

Add award_type to the model (with choices and default) as suggested in the model review to avoid runtime errors when passing award_type in code/tests.

🤖 Prompt for AI Agents
In backend/apps/owasp/migrations/0045_award.py around lines 88 to 97, the
migration adds an "award_type" CharField with choices
[("category","Category"),("award","Award")] and default "award", but the model
class lacks this field; add an award_type = models.CharField(...) to the Award
model in the app's models.py using the same max_length, choices, default,
help_text and verbose_name as the migration so the model and migration match
(then run tests and, if necessary, makemigrations to ensure no further
mismatches).

Comment on lines +17 to +23
class Category(models.TextChoices):
WASPY = "WASPY", "WASPY"
DISTINGUISHED_LIFETIME = (
"Distinguished Lifetime Memberships",
"Distinguished Lifetime Memberships",
)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Add EntryType choices and the award_type field to match migration and tests

The migration and tests expect award_type with choices ("category", "award"). Define choices and the field.

Add the EntryType choices class near Category:

 class Award(TimestampedModel):
@@
     class Category(models.TextChoices):
         WASPY = "WASPY", "WASPY"
         DISTINGUISHED_LIFETIME = (
             "Distinguished Lifetime Memberships",
             "Distinguished Lifetime Memberships",
         )
+
+    class EntryType(models.TextChoices):
+        CATEGORY = "category", "Category"
+        AWARD = "award", "Award"

Add the award_type field after winner_image:

     winner_image = models.CharField(
         verbose_name="Winner Image",
         max_length=500,
         blank=True,
         default="",
         help_text="Path to winner's image",
     )
 
+    award_type = models.CharField(
+        verbose_name="Award Type",
+        max_length=20,
+        choices=EntryType.choices,
+        default=EntryType.AWARD,
+        help_text="Type of entry: category definition or individual award",
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class Category(models.TextChoices):
WASPY = "WASPY", "WASPY"
DISTINGUISHED_LIFETIME = (
"Distinguished Lifetime Memberships",
"Distinguished Lifetime Memberships",
)
class Award(TimestampedModel):
class Category(models.TextChoices):
WASPY = "WASPY", "WASPY"
DISTINGUISHED_LIFETIME = (
"Distinguished Lifetime Memberships",
"Distinguished Lifetime Memberships",
)
class EntryType(models.TextChoices):
CATEGORY = "category", "Category"
AWARD = "award", "Award"
winner_image = models.CharField(
verbose_name="Winner Image",
max_length=500,
blank=True,
default="",
help_text="Path to winner's image",
)
award_type = models.CharField(
verbose_name="Award Type",
max_length=20,
choices=EntryType.choices,
default=EntryType.AWARD,
help_text="Type of entry: category definition or individual award",
)
🤖 Prompt for AI Agents
In backend/apps/owasp/models/award.py around lines 17-23, add an EntryType
TextChoices class defining the two choices ("category", "award") and then add an
award_type CharField immediately after the winner_image field; implement
EntryType with human-readable labels (e.g., CATEGORY="category","Category" and
AWARD="award","Award") and add award_type = models.CharField(max_length=20,
choices=EntryType.choices, default=EntryType.AWARD) so the model matches the
migration and tests.

Comment on lines +54 to +48
year = models.IntegerField(
verbose_name="Year",
help_text="Year the award was given",
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Align model field nullability with migration and data shape

The migration makes year nullable (blank=True, null=True). The model currently does not. Align these to avoid a follow-up migration and to support category-type rows if ever stored.

-    year = models.IntegerField(
-        verbose_name="Year",
-        help_text="Year the award was given",
-    )
+    year = models.IntegerField(
+        verbose_name="Year",
+        help_text="Year the award was given (null for category definitions)",
+        blank=True,
+        null=True,
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
year = models.IntegerField(
verbose_name="Year",
help_text="Year the award was given",
)
year = models.IntegerField(
verbose_name="Year",
help_text="Year the award was given (null for category definitions)",
blank=True,
null=True,
)
🤖 Prompt for AI Agents
In backend/apps/owasp/models/award.py around lines 54 to 57, the model's year
field is currently defined as a non-nullable IntegerField but the migration made
it nullable; update the field to include null=True and blank=True so the model
definition matches the migration and supports nullable year values (e.g.,
category-type rows).

Comment on lines 24 to 31
award = Award.objects.create(
name="Test Award",
category="WASPY",
year=2024,
award_type="award",
winner_name="Test User",
user=self.user,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix model/test mismatch: award_type is referenced but not defined on the model

Award.objects.create(..., award_type="award") will raise TypeError because Award currently lacks an award_type field. The migration includes this field, so the model must define it to stay in sync.

Apply the model-side fixes suggested in backend/apps/owasp/models/award.py (add award_type field and its choices) rather than removing it from the tests.

🤖 Prompt for AI Agents
In backend/tests/apps/owasp/models/award_test.py around lines 24 to 31, the test
sets award_type="award" but the Award model lacks an award_type field; update
the model (backend/apps/owasp/models/award.py) to add an award_type CharField
with the same choices used in migrations (include the choice tuples and a
reasonable default or allow null if migration expects it), import/define the
choices constant, and run makemigrations/migrate if needed so the model and
tests are in sync.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
backend/apps/owasp/migrations/0045_award.py (1)

116-118: UniqueConstraint should include winner_name to allow multiple winners per award/year/category

As-is, multiple recipients for the same award in the same year will violate the uniqueness constraint. Include winner_name in the constraint to support multiple winners.

Apply this diff:

-                    models.UniqueConstraint(
-                        fields=("name", "year", "category"), name="unique_award_name_year_category"
-                    )
+                    models.UniqueConstraint(
+                        fields=("name", "year", "category", "winner_name"),
+                        name="unique_award_name_year_category_winner",
+                    )
🧹 Nitpick comments (3)
backend/apps/owasp/management/commands/owasp_update_badges.py (1)

31-36: Optional micro-optimization: avoid re-evaluating QuerySet for count

If keeping the current approach, calling waspy_winners.count() after iterating re-queries the DB. Cache IDs or compute len() on a list once.

backend/apps/owasp/migrations/0045_award.py (2)

84-91: Consider URLField for winner_image

If winner_image stores URLs from awards.yml, URLField with validation would be more appropriate than CharField.

Proposed field change (requires data migration if existing rows):

  • Replace CharField(max_length=500) with models.URLField(max_length=500, blank=True, default="", ...)

1-11: Minor: Migration headers show mixed Django versions

This migration says “Generated by Django 5.2.5” while 0046 says 5.2.4. Not harmful, but standardizing generation version helps avoid unnecessary diffs across environments.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 082c701 and b0a3e87.

📒 Files selected for processing (6)
  • backend/Makefile (2 hunks)
  • backend/apps/owasp/Makefile (2 hunks)
  • backend/apps/owasp/admin/award.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_update_badges.py (1 hunks)
  • backend/apps/owasp/migrations/0045_award.py (1 hunks)
  • backend/apps/owasp/migrations/0046_merge_0045_badge_0045_project_audience.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/apps/owasp/Makefile
  • backend/apps/owasp/admin/award.py
🧰 Additional context used
🧬 Code Graph Analysis (2)
backend/apps/owasp/management/commands/owasp_update_badges.py (2)
backend/apps/owasp/models/award.py (2)
  • get_waspy_award_winners (104-113)
  • get_user_waspy_awards (116-126)
backend/apps/owasp/models/badge.py (1)
  • Badge (10-41)
backend/apps/owasp/migrations/0045_award.py (1)
backend/apps/owasp/migrations/0046_merge_0045_badge_0045_project_audience.py (1)
  • Migration (6-12)
🔇 Additional comments (5)
backend/Makefile (2)

109-114: Good wiring: update badges after enrich and before indexing

Placing owasp-update-badges after enrich-data and before index-data ensures badges reflect freshly synced data before search indexing. This ordering looks correct.


128-142: Include awards sync in update-data pipeline

Adding owasp-sync-awards into update-data is appropriate and sequenced after github-update-users, which should improve recipient matching. Nice.

backend/apps/owasp/migrations/0046_merge_0045_badge_0045_project_audience.py (1)

7-10: 0045_badge migration not found—safe to remove dependency
A scan of backend/apps/owasp/migrations shows no file or references for 0045_badge.py. The only existing migrations are 0045_award.py and 0045_project_audience.py, which match the updated dependencies in the merge file. Removing the nonexistent 0045_badge dependency is therefore safe.

backend/apps/owasp/management/commands/owasp_update_badges.py (1)

18-25: Idempotent badge creation is correct

Using get_or_create with sensible defaults for the WASPY badge is good and keeps the command idempotent.

backend/apps/owasp/migrations/0045_award.py (1)

27-38: Confirm category choices cover all values present in awards.yml

Hard-coding only two choices risks rejecting future categories from the canonical source. If awards.yml contains other categories, migrations or parsing will fail. Consider a looser validation model-level (Enum that you extend) or map unknown categories to a generic bucket.

Would you confirm the set of categories present in awards.yml and whether we expect more over time? If dynamic, we should adjust the choices or parsing strategy to avoid breaking on new categories.

@trucodd trucodd force-pushed the feature/owasp-awards-profilesync branch 2 times, most recently from 309b84a to 867a047 Compare August 13, 2025 05:34
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
backend/apps/nest/admin/badge.py (1)

8-8: Confirm single source of truth for Badge model

A previous review noted another Badge model at owasp/models/badge.py. Ensure we’re using a single, canonical Badge model across apps to avoid divergence.

Run this script to locate any duplicate Badge model definitions and imports:

#!/bin/bash
set -euo pipefail

echo "Listing potential badge model files:"
fd -H -i -t f 'badge.py' | sort

echo
echo "Searching for class Badge definitions:"
ast-grep --pattern $'class Badge($_):\n  $$$' || true

echo
echo "Searching for Badge imports:"
rg -n -t py $'from\\s+apps\\.(owasp|nest)\\.models(\\.badge)?\\s+import\\s+Badge' -A 1 || true
🧹 Nitpick comments (5)
backend/apps/nest/models/badge.py (1)

41-41: Add trailing newline to satisfy Ruff W292

Line 41 lacks a trailing newline. Many tools expect POSIX-compliant files to end with a newline; Ruff flags this as W292.

Apply this diff:

-        return self.name
+        return self.name
+
backend/apps/owasp/models/__init__.py (1)

1-3: Define all to make the public API explicit

Explicitly declare the public surface to avoid accidental re-exports and stabilize external imports.

Apply this diff:

 from .award import Award
 from .project import Project
+
+__all__ = ["Project", "Award"]
backend/apps/owasp/admin/award.py (2)

5-5: Prefer importing from the models package for consistency

Since Award is re-exported in apps.owasp.models, import from the package rather than the module path.

Apply this diff:

-from apps.owasp.models.award import Award
+from apps.owasp.models import Award

59-61: Use list_select_related instead of overriding get_queryset

Django’s list_select_related property provides the same optimization with less code and better readability.

Apply this diff to remove the method:

-    def get_queryset(self, request):
-        """Optimize queryset with select_related."""
-        return super().get_queryset(request).select_related("user")

Then add this attribute near the other admin attributes:

# Add within AwardAdmin
list_select_related = ("user",)
backend/apps/nest/admin/badge.py (1)

15-15: Add trailing newline to satisfy Ruff W292

File is missing a trailing newline on Line 15.

Apply this diff:

-    ordering = ("weight", "name")
+    ordering = ("weight", "name")
+
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b0a3e87 and 309b84a.

📒 Files selected for processing (15)
  • backend/Makefile (2 hunks)
  • backend/apps/nest/admin/badge.py (1 hunks)
  • backend/apps/nest/models/badge.py (1 hunks)
  • backend/apps/owasp/Makefile (2 hunks)
  • backend/apps/owasp/admin/__init__.py (1 hunks)
  • backend/apps/owasp/admin/award.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_sync_awards.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_update_badges.py (1 hunks)
  • backend/apps/owasp/migrations/0045_award.py (1 hunks)
  • backend/apps/owasp/migrations/0046_merge_0045_badge_0045_project_audience.py (1 hunks)
  • backend/apps/owasp/models/__init__.py (1 hunks)
  • backend/apps/owasp/models/award.py (1 hunks)
  • backend/tests/apps/nest/management/__init__.py (1 hunks)
  • backend/tests/apps/nest/management/commands/__init__.py (1 hunks)
  • backend/tests/apps/owasp/models/award_test.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (11)
  • backend/tests/apps/nest/management/commands/init.py
  • backend/tests/apps/nest/management/init.py
  • backend/Makefile
  • backend/apps/owasp/management/commands/owasp_sync_awards.py
  • backend/tests/apps/owasp/models/award_test.py
  • backend/apps/owasp/migrations/0046_merge_0045_badge_0045_project_audience.py
  • backend/apps/owasp/Makefile
  • backend/apps/owasp/models/award.py
  • backend/apps/owasp/migrations/0045_award.py
  • backend/apps/owasp/admin/init.py
  • backend/apps/owasp/management/commands/owasp_update_badges.py
🧰 Additional context used
🧬 Code Graph Analysis (3)
backend/apps/owasp/admin/award.py (1)
backend/apps/owasp/models/award.py (1)
  • Award (10-188)
backend/apps/owasp/models/__init__.py (1)
backend/apps/owasp/models/award.py (1)
  • Award (10-188)
backend/apps/nest/admin/badge.py (1)
backend/apps/nest/models/badge.py (1)
  • Badge (10-41)
🪛 Ruff (0.12.2)
backend/apps/nest/admin/badge.py

15-15: No newline at end of file

Add trailing newline

(W292)

backend/apps/nest/models/badge.py

41-41: No newline at end of file

Add trailing newline

(W292)

🔇 Additional comments (3)
backend/apps/owasp/models/__init__.py (1)

1-1: Good addition: re-exporting Award at the package level

Making Award importable via apps.owasp.models aligns with how Project is exposed and simplifies imports elsewhere.

backend/apps/owasp/admin/award.py (1)

8-57: Admin configuration is solid and comprehensive

The list_display/filter/search organization looks good, and autocomplete_fields for user is appropriate. Fieldsets/read-only timestamp fields are clear.

backend/apps/nest/admin/badge.py (1)

8-8: Decorator-based registration looks good

Switching to @admin.register(Badge) is consistent with modern Django admin patterns and other admins in this PR.

@trucodd trucodd force-pushed the feature/owasp-awards-profilesync branch from 867a047 to bc6244d Compare August 13, 2025 08:38
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (3)
backend/apps/owasp/models/award.py (2)

24-33: Add a composite UniqueConstraint to allow multiple years and winners while preventing duplicates

Enforce uniqueness at the tuple level: (name, year, category, winner_name). This allows the same award each year and multiple winners per year without collisions.

Apply:

 class Meta:
     db_table = "owasp_awards"
     indexes = [
         models.Index(fields=["category", "year"], name="owasp_award_category_year"),
         models.Index(fields=["-year"], name="owasp_award_year_desc"),
         models.Index(fields=["name"], name="owasp_award_name"),
     ]
     verbose_name = "Award"
     verbose_name_plural = "Awards"
+    constraints = [
+        models.UniqueConstraint(
+            fields=["name", "year", "category", "winner_name"],
+            name="unique_award_name_year_category_winner",
+        )
+    ]

154-158: Persist description and sanitize winner fields; avoid None writes

  • The YAML description is dropped; store it.
  • Coalesce winner fields to empty strings and strip them to avoid writing None into non-nullable fields.

Apply:

-            award_defaults = {
-                "description": "",
-                "winner_info": winner_data.get("info", ""),
-                "winner_image": winner_data.get("image", ""),
-            }
+            award_defaults = {
+                "description": (award_data.get("description") or "").strip(),
+                "winner_info": (winner_data.get("info") or "").strip(),
+                "winner_image": (winner_data.get("image") or "").strip(),
+            }
backend/apps/owasp/migrations/0045_award.py (1)

107-116: Add a composite UniqueConstraint on (name, year, category, winner_name)

Align DB constraints with intended semantics and the model.

Apply:

             options={
                 "verbose_name": "Award",
                 "verbose_name_plural": "Awards",
                 "db_table": "owasp_awards",
-                "indexes": [
+                "indexes": [
                     models.Index(fields=["category", "year"], name="owasp_award_category_year"),
                     models.Index(fields=["-year"], name="owasp_award_year_desc"),
                     models.Index(fields=["name"], name="owasp_award_name"),
                 ],
+                "constraints": [
+                    models.UniqueConstraint(
+                        fields=("name", "year", "category", "winner_name"),
+                        name="unique_award_name_year_category_winner",
+                    )
+                ],
             },
🧹 Nitpick comments (2)
backend/apps/owasp/models/award.py (2)

141-148: Normalize parsed strings to reduce duplicate rows caused by whitespace/case

Trim award title and category and validate category against choices to prevent accidental duplicates due to spacing.

Apply:

-        award_name = award_data.get("title", "")
-        category = award_data.get("category", "")
+        award_name = (award_data.get("title") or "").strip()
+        category = (award_data.get("category") or "").strip()
+        # Optional: validate category
+        if category not in dict(Award.Category.choices):
+            # If YAML differs only in case/spacing, try a tolerant match
+            normalized = {k.lower(): k for k, _ in Award.Category.choices}
+            key = category.lower()
+            category = normalized.get(key, category)

50-53: Optional: Make year nullable if you plan to store category definition entries

If you ever persist “category” type rows from YAML (no year), year should be nullable; if not, keep as-is.

-    year = models.IntegerField(
+    year = models.IntegerField(
         verbose_name="Year",
-        help_text="Year the award was given",
+        help_text="Year the award was given",
+        null=True,
+        blank=True,
     )
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 867a047 and bc6244d.

📒 Files selected for processing (8)
  • backend/Makefile (2 hunks)
  • backend/apps/nest/admin/badge.py (1 hunks)
  • backend/apps/owasp/Makefile (2 hunks)
  • backend/apps/owasp/admin/award.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_update_badges.py (1 hunks)
  • backend/apps/owasp/migrations/0045_award.py (1 hunks)
  • backend/apps/owasp/migrations/0046_merge_0045_badge_0045_project_audience.py (1 hunks)
  • backend/apps/owasp/models/award.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • backend/apps/owasp/admin/award.py
  • backend/apps/owasp/migrations/0046_merge_0045_badge_0045_project_audience.py
  • backend/Makefile
  • backend/apps/owasp/management/commands/owasp_update_badges.py
  • backend/apps/nest/admin/badge.py
🧰 Additional context used
🧬 Code Graph Analysis (2)
backend/apps/owasp/models/award.py (1)
backend/apps/common/models.py (1)
  • TimestampedModel (37-46)
backend/apps/owasp/migrations/0045_award.py (1)
backend/apps/owasp/migrations/0046_merge_0045_badge_0045_project_audience.py (1)
  • Migration (6-12)
🔇 Additional comments (3)
backend/apps/owasp/models/award.py (2)

17-23: Categories defined via TextChoices look correct

The two categories requested (“WASPY”, “Distinguished Lifetime Memberships”) are present and typed as choices.


99-123: Query helpers are concise and appropriate

  • get_waspy_award_winners: distinct user set via reverse relation is efficient and readable.
  • get_user_waspy_awards: straightforward filter; consider adding .only(...) in call sites if needed.
backend/apps/owasp/migrations/0045_award.py (1)

111-114: No issues with descending index configuration

The project’s default database engine is PostgreSQL (django.db.backends.postgresql), which fully supports descending indexes. The models.Index(fields=["-year"], …) declaration will be applied correctly—no changes to the migration are needed.

@trucodd trucodd force-pushed the feature/owasp-awards-profilesync branch 2 times, most recently from 25e47e3 to 02f6581 Compare August 14, 2025 09:09
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
backend/tests/apps/owasp/models/award_test.py (1)

1-1: Re-check model/test sync for award_type and add a targeted test if it exists.

Prior review flagged award_type present in migration but missing on the model. If award_type remains, add a simple test to assert the field exists and choices align with migration; else, remove it from migrations to keep parity.

Run this script to verify the current state and locate any award_type discrepancies:

#!/bin/bash
set -euo pipefail

echo "Searching for 'award_type' across the repo..."
rg -n --hidden 'award_type' || true

echo
echo "Inspect Award model(s):"
fd -a 'award.py' backend | xargs -I{} bash -lc 'echo "==> {}"; rg -n "class Award|award_type|TextChoices|Meta|db_table" "{}" || true'

echo
echo "Inspect OWASP migrations referencing Award:"
fd -a 'migrations' backend/apps/owasp | xargs -I{} rg -n --heading 'award_type|class Award|migrations\.CreateModel|migrations\.AddField' {} || true
🧹 Nitpick comments (5)
backend/tests/apps/owasp/models/award_test.py (5)

6-8: DRY imports with setUpTestData to reduce repetition.

Centralize the Award import once for reuse across tests.

Apply this diff:

 class AwardModelTest(TestCase):
     """Test cases for Award model."""
-
+    @classmethod
+    def setUpTestData(cls):
+        from apps.owasp.models.award import Award
+        cls.Award = Award

9-14: Prefer unittest assertions over bare asserts.

Bare asserts can be optimized away and provide poorer failure messages.

Apply this diff:

     def test_award_import(self):
         """Test that Award model can be imported."""
         from apps.owasp.models.award import Award
 
-        assert Award is not None
+        self.assertIsNotNone(Award)

15-25: Use class-cached model and unittest assertions.

Removes redundant import and uses assertIn for clearer failures.

Apply this diff:

     def test_award_category_choices(self):
         """Test Award category choices."""
-        from apps.owasp.models.award import Award
-
-        choices = Award.Category.choices
-        assert ("WASPY", "WASPY") in choices
-        assert (
-            "Distinguished Lifetime Memberships",
-            "Distinguished Lifetime Memberships",
-        ) in choices
+        choices = self.Award.Category.choices
+        self.assertIn(("WASPY", "WASPY"), choices)
+        self.assertIn(("Distinguished Lifetime Memberships", "Distinguished Lifetime Memberships"), choices)

26-31: Use unittest assertions and verify verbose_name_plural as well.

This adds a useful extra assertion and improves assertion clarity.

Apply this diff:

     def test_award_meta(self):
         """Test Award model meta."""
-        from apps.owasp.models.award import Award
-
-        assert Award._meta.db_table == "owasp_awards"
-        assert Award._meta.verbose_name == "Award"
+        self.assertEqual(self.Award._meta.db_table, "owasp_awards")
+        self.assertEqual(self.Award._meta.verbose_name, "Award")
+        self.assertEqual(self.Award._meta.verbose_name_plural, "Awards")

6-7: Consider adding tests for unique constraints and model dunder methods.

  • Validate that name is unique (creating a second Award with the same name should raise IntegrityError).
  • Add a str test (commonly returns name) if implemented.
    These will exercise real persistence concerns beyond metadata.

I can draft these tests once you confirm required fields on Award (beyond category, name, description, year).

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bc6244d and 25e47e3.

📒 Files selected for processing (9)
  • backend/Makefile (2 hunks)
  • backend/apps/nest/admin/badge.py (1 hunks)
  • backend/apps/owasp/Makefile (2 hunks)
  • backend/apps/owasp/admin/award.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_update_badges.py (1 hunks)
  • backend/apps/owasp/migrations/0045_award.py (1 hunks)
  • backend/apps/owasp/migrations/0046_merge_0045_badge_0045_project_audience.py (1 hunks)
  • backend/apps/owasp/models/award.py (1 hunks)
  • backend/tests/apps/owasp/models/award_test.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • backend/apps/owasp/migrations/0046_merge_0045_badge_0045_project_audience.py
  • backend/Makefile
  • backend/apps/owasp/management/commands/owasp_update_badges.py
  • backend/apps/nest/admin/badge.py
  • backend/apps/owasp/migrations/0045_award.py
  • backend/apps/owasp/models/award.py
  • backend/apps/owasp/Makefile
  • backend/apps/owasp/admin/award.py
🧰 Additional context used
🧬 Code Graph Analysis (1)
backend/tests/apps/owasp/models/award_test.py (1)
backend/apps/owasp/models/award.py (2)
  • Award (10-183)
  • Category (17-22)
🔇 Additional comments (1)
backend/tests/apps/owasp/models/award_test.py (1)

1-5: Solid baseline smoke tests for Award model metadata and choices.

This covers importability, Category.choices, and essential Meta attributes. Good starting coverage.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (6)
backend/apps/owasp/admin/award.py (1)

8-10: Enforce data integrity between award_type/year/winner fields.

Add model-level validation (clean) to ensure category entries have year=None and no winner fields; award entries must have non-null year. Surface these errors in admin forms.

backend/apps/owasp/models/award.py (4)

49-52: Make year nullable to support category rows.

Category entries don't have a year; current non-null constraint will fail saves.

-    year = models.IntegerField(
-        verbose_name="Year",
-        help_text="Year the award was given",
-    )
+    year = models.IntegerField(
+        verbose_name="Year",
+        help_text="Year the award was given (null for category definitions)",
+        blank=True,
+        null=True,
+    )

24-33: Add unique constraint to ensure idempotent inserts per winner.

Prevents duplicates and aligns with get_or_create(name, category, year, winner_name).

     class Meta:
         db_table = "owasp_awards"
         indexes = [
             models.Index(fields=["category", "year"], name="owasp_award_category_year"),
             models.Index(fields=["-year"], name="owasp_award_year_desc"),
             models.Index(fields=["name"], name="owasp_award_name"),
         ]
         verbose_name = "Award"
         verbose_name_plural = "Awards"
+        constraints = [
+            models.UniqueConstraint(
+                fields=["name", "category", "year", "winner_name"],
+                name="unique_award_name_category_year_winner",
+            )
+        ]

153-157: Persist YAML description and award_type; normalize strings.

Currently description is dropped and award_type is not set on created/updated rows.

-            award_defaults = {
-                "description": "",
-                "winner_info": winner_data.get("info", ""),
-                "winner_image": winner_data.get("image", ""),
-            }
+            award_defaults = {
+                "description": (award_data.get("description") or "").strip(),
+                "winner_info": (winner_data.get("info") or "").strip(),
+                "winner_image": (winner_data.get("image") or "").strip(),
+                "award_type": Award.EntryType.AWARD,
+            }

17-23: Critical: add missing award_type field to Award model — management command depends on it

The management command backend/apps/owasp/management/commands/owasp_sync_awards.py calls get_or_create with award_type (seen at lines with award_type="category" and "award"), but backend/apps/owasp/models/award.py currently has no award_type field — this will raise FieldError/TypeError at runtime. Add the EntryType choices and the award_type CharField, and create/apply a migration.

Files to update:

  • backend/apps/owasp/models/award.py — add EntryType TextChoices and award_type CharField near the Category/TextChoices and model fields.
  • backend/apps/owasp/management/commands/owasp_sync_awards.py — references found at:
    • backend/apps/owasp/management/commands/owasp_sync_awards.py:103 (award_type="category")
    • backend/apps/owasp/management/commands/owasp_sync_awards.py:174 ( "award_type": "award")

Suggested diff to apply in backend/apps/owasp/models/award.py:

@@
     class Category(models.TextChoices):
         WASPY = "WASPY", "WASPY"
         DISTINGUISHED_LIFETIME = (
             "Distinguished Lifetime Memberships",
             "Distinguished Lifetime Memberships",
         )
+
+    class EntryType(models.TextChoices):
+        CATEGORY = "category", "Category"
+        AWARD = "award", "Award"
@@
     winner_image = models.CharField(
         verbose_name="Winner Image",
         max_length=500,
         blank=True,
         default="",
         help_text="Path to winner's image",
     )
+
+    award_type = models.CharField(
+        verbose_name="Entry Type",
+        max_length=20,
+        choices=EntryType.choices,
+        default=EntryType.AWARD,
+        help_text="Type of entry: 'category' rows define a category; 'award' rows represent a winner",
+    )
backend/apps/owasp/management/commands/owasp_sync_awards.py (1)

259-271: Fuzzy matching is too permissive; risks mis-assigning users.

Require at least two tokens and both to match; avoid single-token partial hits.

-    def _try_fuzzy_name_match(self, clean_name: str, min_name_parts: int) -> User | None:
-        """Try fuzzy name matching with partial matches."""
-        name_parts = clean_name.split()
-        if len(name_parts) >= min_name_parts:
-            # Try "FirstName LastName" variations
-            for i in range(len(name_parts)):
-                for j in range(i + 1, len(name_parts) + 1):
-                    partial_name = " ".join(name_parts[i:j])
-                    try:
-                        return User.objects.get(name__icontains=partial_name)
-                    except (User.DoesNotExist, User.MultipleObjectsReturned):
-                        continue
-        return None
+    def _try_fuzzy_name_match(self, clean_name: str, min_name_parts: int) -> User | None:
+        """Fuzzy match by requiring at least two tokens present in name."""
+        parts = [p for p in clean_name.split() if p]
+        if len(parts) < max(min_name_parts, 2):
+            return None
+        first, last = parts[0], parts[-1]
+        qs = User.objects.filter(name__icontains=first).filter(name__icontains=last)
+        count = qs.count()
+        if count == 1:
+            return qs.first()
+        return None
🧹 Nitpick comments (2)
backend/apps/owasp/admin/award.py (1)

34-35: Prefer list_select_related over overriding get_queryset.

Cleaner and declarative; avoids method override.

@@
-    autocomplete_fields = ("user",)
+    autocomplete_fields = ("user",)
+    list_select_related = ("user",)
@@
-    def get_queryset(self, request):
-        """Optimize queryset with select_related."""
-        return super().get_queryset(request).select_related("user")
+    # get_queryset override not needed with list_select_related

Also applies to: 59-61

backend/apps/owasp/management/commands/owasp_sync_awards.py (1)

51-66: Handle YAML parse errors explicitly (optional).

Catching yaml.YAMLError gives clearer diagnostics than a broad Exception.

-            awards_data = yaml.safe_load(yaml_content)
+            try:
+                awards_data = yaml.safe_load(yaml_content)
+            except yaml.YAMLError as ye:
+                logger.exception("YAML parsing error for awards.yml")
+                self.stdout.write(self.style.ERROR(f"Failed to parse awards.yml: {ye!s}"))
+                return
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 25e47e3 and 02f6581.

📒 Files selected for processing (15)
  • backend/Makefile (2 hunks)
  • backend/apps/nest/admin/badge.py (1 hunks)
  • backend/apps/owasp/Makefile (2 hunks)
  • backend/apps/owasp/admin/__init__.py (1 hunks)
  • backend/apps/owasp/admin/award.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_sync_awards.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_update_badges.py (1 hunks)
  • backend/apps/owasp/migrations/0045_award.py (1 hunks)
  • backend/apps/owasp/migrations/0045_badge.py (0 hunks)
  • backend/apps/owasp/migrations/0046_merge_0045_badge_0045_project_audience.py (1 hunks)
  • backend/apps/owasp/models/__init__.py (1 hunks)
  • backend/apps/owasp/models/award.py (1 hunks)
  • backend/tests/apps/nest/management/__init__.py (1 hunks)
  • backend/tests/apps/nest/management/commands/__init__.py (1 hunks)
  • backend/tests/apps/owasp/models/award_test.py (1 hunks)
💤 Files with no reviewable changes (1)
  • backend/apps/owasp/migrations/0045_badge.py
✅ Files skipped from review due to trivial changes (1)
  • backend/Makefile
🚧 Files skipped from review as they are similar to previous changes (10)
  • backend/tests/apps/nest/management/init.py
  • backend/apps/owasp/migrations/0046_merge_0045_badge_0045_project_audience.py
  • backend/tests/apps/nest/management/commands/init.py
  • backend/tests/apps/owasp/models/award_test.py
  • backend/apps/owasp/admin/init.py
  • backend/apps/owasp/management/commands/owasp_update_badges.py
  • backend/apps/owasp/models/init.py
  • backend/apps/nest/admin/badge.py
  • backend/apps/owasp/Makefile
  • backend/apps/owasp/migrations/0045_award.py
🧰 Additional context used
🧬 Code Graph Analysis (3)
backend/apps/owasp/admin/award.py (1)
backend/apps/owasp/models/award.py (1)
  • Award (10-183)
backend/apps/owasp/models/award.py (1)
backend/apps/common/models.py (1)
  • TimestampedModel (37-46)
backend/apps/owasp/management/commands/owasp_sync_awards.py (2)
backend/apps/github/utils.py (1)
  • get_repository_file_content (60-79)
backend/apps/owasp/models/award.py (1)
  • Award (10-183)

Comment on lines 12 to 21
list_display = (
"name",
"category",
"year",
"winner_name",
"user",
"nest_created_at",
"nest_updated_at",
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Expose and filter by award_type in admin (once model field is added).

Admin needs visibility into entry type to distinguish category rows vs actual awards.

Apply these diffs:

@@
     list_display = (
         "name",
         "category",
+        "award_type",
         "year",
         "winner_name",
         "user",
         "nest_created_at",
         "nest_updated_at",
     )
@@
     list_filter = (
         "category",
+        "award_type",
         "year",
     )
@@
         (
             "Basic Information",
-            {"fields": ("name", "category", "year", "description")},
+            {"fields": ("name", "category", "award_type", "year", "description")},
         ),

Also applies to: 21-24, 38-40

🤖 Prompt for AI Agents
In backend/apps/owasp/admin/award.py around lines 12-20, 21-24, and 38-40, add
the new model field "award_type" to the admin configuration so the admin can see
and filter by entry type: include "award_type" in the list_display tuple at
lines 12-20 and add it to the appropriate list_filter (and any other admin
tuples for visible columns or default ordering shown at lines 21-24 and 38-40)
so that category rows and actual awards are distinguishable once the model field
exists.

Comment on lines 99 to 108
# Create or update category record
if not dry_run:
award, created = Award.objects.get_or_create(
name=category_name,
award_type="category",
defaults={
"category": category_name,
"description": description,
},
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use model enum instead of string literals for award_type.

Prevents typos and centralizes truth in the model.

-            award, created = Award.objects.get_or_create(
-                name=category_name,
-                award_type="category",
+            award, created = Award.objects.get_or_create(
+                name=category_name,
+                award_type=Award.EntryType.CATEGORY,
                 defaults={
                     "category": category_name,
                     "description": description,
                 },
             )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Create or update category record
if not dry_run:
award, created = Award.objects.get_or_create(
name=category_name,
award_type="category",
defaults={
"category": category_name,
"description": description,
},
)
# Create or update category record
if not dry_run:
award, created = Award.objects.get_or_create(
name=category_name,
award_type=Award.EntryType.CATEGORY,
defaults={
"category": category_name,
"description": description,
},
)
🤖 Prompt for AI Agents
In backend/apps/owasp/management/commands/owasp_sync_awards.py around lines 99
to 108, the code passes the literal "category" into award_type; replace that
string with the model's enum/constant (for example Award.AwardType.CATEGORY or
the specific Award class constant your model defines) by importing/referencing
the enum from the Award model and using it in the get_or_create call so
award_type uses the canonical enum value instead of a raw string.

Comment on lines 166 to 180
if not dry_run:
# Create or update award record
award, created = Award.objects.get_or_create(
name=award_name,
category=category,
year=year,
winner_name=winner_name,
defaults={
"award_type": "award",
"description": "",
"winner_info": winner_info,
"winner_image": winner_image,
"user": matched_user,
},
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use model enum and carry description through to award rows.

Propagate award description to each winner row; avoid hardcoded strings.

Consider passing description down from _process_award (see next comment).

             award, created = Award.objects.get_or_create(
                 name=award_name,
                 category=category,
                 year=year,
                 winner_name=winner_name,
                 defaults={
-                    "award_type": "award",
-                    "description": "",
+                    "award_type": Award.EntryType.AWARD,
+                    "description": award_description,
                     "winner_info": winner_info,
                     "winner_image": winner_image,
                     "user": matched_user,
                 },
             )

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In backend/apps/owasp/management/commands/owasp_sync_awards.py around lines 166
to 180, replace the hardcoded award_type string and empty description with the
model enum and the actual description value passed down from _process_award: use
the Award model's enum constant for the award_type (e.g. Award.AwardType.AWARD
or the project's actual enum attribute) instead of "award", pass the real
description variable into defaults for get_or_create, and ensure that when an
existing award is found you also update its description (and award_type if
needed) so the winner rows carry the propagated description; adjust
_process_award to accept/forward the description if it doesn’t already.

@trucodd trucodd marked this pull request as ready for review August 14, 2025 10:25
@trucodd trucodd marked this pull request as draft August 14, 2025 11:03
@trucodd trucodd marked this pull request as ready for review August 14, 2025 11:28
Copy link
Collaborator

@arkid15r arkid15r left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code probably works and does the job. However, I'd like to suggest structural improvements and some performance related changes:

Comment on lines 26 to 29
indexes = [
models.Index(fields=["category", "year"], name="owasp_award_category_year"),
models.Index(fields=["-year"], name="owasp_award_year_desc"),
models.Index(fields=["name"], name="owasp_award_name"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to introduce indexes later.

default="",
help_text="Detailed information about the winner",
)
winner_image = models.CharField(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It probably could be winner_image_url with proper URL type?

verbose_name="User",
help_text="Associated GitHub user (if matched)",
)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the matching process needs to be verified by a human. Could you introduce is_reviewed flag for this model?

return None

@staticmethod
def _create_awards_from_winners(award_data: dict, *, save: bool = True):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code doesn't look consistent with the rest of models where update_data is responsible for a single instance creation/update logic.

return cls.objects.filter(user=user, category=cls.Category.WASPY)

@staticmethod
def update_data(award_data: dict, *, save: bool = True):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see event or post handling as an example (note the bulk saving approach).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one should wait until the initial badge processing job is implemented in another PR. Then we'll just update it with additional logic.

@trucodd trucodd marked this pull request as draft August 16, 2025 08:51
@trucodd trucodd force-pushed the feature/owasp-awards-profilesync branch from 02f6581 to 1a25b2d Compare August 17, 2025 02:49
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (6)
backend/apps/owasp/management/commands/owasp_update_badges.py (2)

3-7: Add transaction import for atomic badge updates

We’ll wrap handle() in a transaction to make add/remove operations consistent.

 from django.core.management.base import BaseCommand
+from django.db import transaction
 
 from apps.github.models.user import User
 from apps.nest.models.badge import Badge
 from apps.owasp.models.award import Award

15-47: Avoid N+1 queries; diff sets and report actual adds/removes

Compute set differences and only touch users whose badge membership actually changes. This also produces accurate logs.

     def handle(self, *args, **options):
         """Handle the command execution."""
-        # Get or create WASPY badge
-        waspy_badge, created = Badge.objects.get_or_create(
-            name="WASPY Award Winner",
-            defaults={
-                "description": "Recipient of WASPY award from OWASP",
-                "css_class": "badge-waspy",
-                "weight": 10,
-            },
-        )
-
-        if created:
-            self.stdout.write(f"Created badge: {waspy_badge.name}")
-
-        # Get users with reviewed WASPY awards only
-        waspy_winners = User.objects.filter(
-            awards__category=Award.Category.WASPY, awards__is_reviewed=True
-        ).distinct()
-
-        # Add badge to WASPY winners
-        for user in waspy_winners:
-            user.badges.add(waspy_badge)
-
-        # Remove badge from users without reviewed WASPY awards
-        users_with_badge = User.objects.filter(badges=waspy_badge)
-        for user in users_with_badge:
-            if not Award.objects.filter(
-                user=user, category=Award.Category.WASPY, is_reviewed=True
-            ).exists():
-                user.badges.remove(waspy_badge)
-
-        self.stdout.write(f"Updated badges for {waspy_winners.count()} WASPY winners")
+        with transaction.atomic():
+            # Get or create WASPY badge
+            waspy_badge, created = Badge.objects.get_or_create(
+                name="WASPY Award Winner",
+                defaults={
+                    "description": "Recipient of WASPY award from OWASP",
+                    "css_class": "badge-waspy",
+                    "weight": 10,
+                },
+            )
+            if created:
+                self.stdout.write(f"Created badge: {waspy_badge.name}")
+
+            # Compute desired vs current badge holders
+            winner_ids = set(
+                User.objects.filter(
+                    awards__category=Award.Category.WASPY, awards__is_reviewed=True
+                )
+                .distinct()
+                .values_list("id", flat=True)
+            )
+            current_ids = set(
+                User.objects.filter(badges=waspy_badge).values_list("id", flat=True)
+            )
+
+            to_add_ids = winner_ids - current_ids
+            to_remove_ids = current_ids - winner_ids
+
+            if to_add_ids:
+                for user in User.objects.filter(id__in=to_add_ids):
+                    user.badges.add(waspy_badge)
+            if to_remove_ids:
+                for user in User.objects.filter(id__in=to_remove_ids):
+                    user.badges.remove(waspy_badge)
+
+            self.stdout.write(
+                f"WASPY badge updated: +{len(to_add_ids)} added, -{len(to_remove_ids)} removed, total_winners={len(winner_ids)}"
+            )
backend/apps/owasp/models/award.py (1)

24-28: Enforce idempotency with a DB-level unique constraint

Without a uniqueness constraint, duplicates can slip in (or a manual duplicate can break update_data via MultipleObjectsReturned). Constrain by (name, category, year, winner_name).

     class Meta:
         db_table = "owasp_awards"
         verbose_name = "Award"
         verbose_name_plural = "Awards"
+        constraints = [
+            models.UniqueConstraint(
+                fields=["name", "category", "year", "winner_name"],
+                name="unique_award_name_year_category_winner",
+            )
+        ]
backend/apps/owasp/migrations/0045_award.py (1)

113-119: Add a unique constraint to back the model’s idempotency contract

Match the model change and ensure DB-level protection against duplicates of the same award/winner/year/category.

             options={
                 "verbose_name": "Award",
                 "verbose_name_plural": "Awards",
                 "db_table": "owasp_awards",
+                "constraints": [
+                    models.UniqueConstraint(
+                        fields=("name", "year", "category", "winner_name"),
+                        name="unique_award_name_year_category_winner",
+                    )
+                ],
             },
backend/apps/owasp/management/commands/owasp_sync_awards.py (2)

133-141: Verify DB uniqueness includes winner_name to prevent IntegrityError

Both the pre-check and update_data identify rows by (name, category, year, winner_name). Ensure the model/migration UniqueConstraint uses these four fields so get-or-create behavior is truly idempotent.

Run to verify constraints reference winner_name:

#!/usr/bin/env bash
set -euo pipefail

echo "Model constraints referencing winner_name:"
rg -nP -C2 'UniqueConstraint|unique_together|constraints' backend/apps/owasp/models/award.py | rg -nP 'winner_name|UniqueConstraint|unique_together' -n -C2 || true

echo
echo "Migrations that define unique constraints for Award:"
fd -t f award.py backend/apps/owasp/migrations | while read -r f; do
  echo "---- $f"
  rg -nP -C2 'UniqueConstraint|unique_together|constraints|AlterUniqueTogether' "$f" | rg -nP 'winner_name|UniqueConstraint|unique_together|AlterUniqueTogether' -n -C2 || true
done

Expected: a UniqueConstraint on (name, category, year, winner_name).


217-229: Fuzzy matching is too permissive; restrict to two-token match (first+last) to avoid mis-assignments

Single-token partial matches can link awards to the wrong user (e.g., “John”). Restrict matching to require at least two distinct tokens and both must be present.

-    def _try_fuzzy_name_match(self, clean_name: str, min_name_parts: int) -> User | None:
-        """Try fuzzy name matching with partial matches."""
-        name_parts = clean_name.split()
-        if len(name_parts) >= min_name_parts:
-            # Try "FirstName LastName" variations
-            for i in range(len(name_parts)):
-                for j in range(i + 1, len(name_parts) + 1):
-                    partial_name = " ".join(name_parts[i:j])
-                    try:
-                        return User.objects.get(name__icontains=partial_name)
-                    except (User.DoesNotExist, User.MultipleObjectsReturned):
-                        continue
-        return None
+    def _try_fuzzy_name_match(self, clean_name: str, min_name_parts: int) -> User | None:
+        """Require at least two tokens and match on first AND last tokens."""
+        parts = [p for p in re.split(r"\s+", clean_name) if p]
+        if len(parts) < max(2, min_name_parts):
+            return None
+        first, last = parts[0], parts[-1]
+        qs = User.objects.filter(name__icontains=first).filter(name__icontains=last)
+        if qs.count() == 1:
+            return qs.first()
+        return None
🧹 Nitpick comments (8)
backend/tests/apps/owasp/models/award_test.py (2)

11-11: Import via the public models package to validate API surface

Importing from apps.owasp.models ensures the public export is wired (via init.py) and avoids coupling tests to module layout.

-        from apps.owasp.models.award import Award
+        from apps.owasp.models import Award
@@
-        from apps.owasp.models.award import Award
+        from apps.owasp.models import Award
@@
-        from apps.owasp.models.award import Award
+        from apps.owasp.models import Award

Also applies to: 17-17, 28-28


6-32: Broaden coverage: add idempotency and parsing tests for update_data()

Current tests validate choices and meta only. Given the sync relies on Award.update_data(), add:

  • A test proving update_data is idempotent (no duplicate rows on repeated calls).
  • A test verifying mapping of YAML keys (title/category/year/name/description/info/image) to model fields.

I can draft these tests using factories or minimal dicts; say the word and I’ll open a follow-up PR with them.

backend/apps/owasp/admin/award.py (1)

36-69: Add admin actions to mark awards reviewed/unreviewed in bulk

Useful for triaging matched users quickly.

     autocomplete_fields = ("user",)
+    actions = ("mark_reviewed", "mark_unreviewed")
@@
     readonly_fields = ("nest_created_at", "nest_updated_at")
 
+    def mark_reviewed(self, request, queryset):
+        updated = queryset.update(is_reviewed=True)
+        self.message_user(request, f"Marked {updated} award(s) as reviewed.")
+
+    mark_reviewed.short_description = "Mark selected awards as reviewed"
+
+    def mark_unreviewed(self, request, queryset):
+        updated = queryset.update(is_reviewed=False)
+        self.message_user(request, f"Marked {updated} award(s) as unreviewed.")
+
+    mark_unreviewed.short_description = "Mark selected awards as unreviewed"
+
     def get_queryset(self, request):
         """Optimize queryset with select_related."""
         return super().get_queryset(request).select_related("user")
backend/apps/owasp/models/award.py (1)

145-158: Use get_or_create to simplify and avoid racey get/except flows

This tightens the idempotent path and reduces boilerplate. Still relies on the unique constraint above to prevent duplicates.

-        try:
-            award = Award.objects.get(
-                name=name,
-                category=category,
-                year=year,
-                winner_name=winner_name,
-            )
-        except Award.DoesNotExist:
-            award = Award(
-                name=name,
-                category=category,
-                year=year,
-                winner_name=winner_name,
-            )
+        award, _ = Award.objects.get_or_create(
+            name=name,
+            category=category,
+            year=year,
+            winner_name=winner_name,
+        )
backend/apps/owasp/management/commands/owasp_sync_awards.py (4)

61-66: Catch yaml.YAMLError to surface parse failures precisely

safe_load can raise yaml.YAMLError. Catching it yields a clearer failure mode than the generic exception handler.

-            awards_data = yaml.safe_load(yaml_content)
-
-            if not awards_data:
-                self.stdout.write(self.style.ERROR("Failed to parse awards.yml content"))
-                return
+            try:
+                awards_data = yaml.safe_load(yaml_content)
+            except yaml.YAMLError as e:
+                self.stdout.write(self.style.ERROR(f"Failed to parse awards.yml: {e}"))
+                return
+
+            if not awards_data:
+                self.stdout.write(self.style.ERROR("Failed to parse awards.yml content"))
+                return

133-161: Avoid double DB lookups when determining created vs updated

You first query to determine is_new, then call Award.update_data which internally does its own get/create. This doubles DB hits per winner.

Option A (leave as-is for clarity) or Option B (preferred): make update_data return (award, created) and use that here.

Proposed change in this file if update_data returns a tuple:

-            # Check if award exists before update
-            try:
-                Award.objects.get(
-                    name=award_name,
-                    category=winner_data.get("category", ""),
-                    year=winner_data.get("year"),
-                    winner_name=winner_name,
-                )
-                is_new = False
-            except Award.DoesNotExist:
-                is_new = True
-
-            # Use the model's update_data method
-            award = Award.update_data(winner_data, save=True)
+            # Create or update in one call; update_data returns (award, created)
+            award, is_new = Award.update_data(winner_data, save=True)

Outside this file (model change) you can update Award.update_data to:

def update_data(award_data: dict, *, save: bool = True) -> tuple["Award", bool]:
    # return (award, created)

I can draft the model changes if you want to pursue this.


287-295: Prune impossible login variants (underscores aren’t valid GitHub usernames)

This reduces unnecessary queries while keeping your 10-variant cap.

         unique_logins = []
         for login in potential_logins:
-            if login and login not in seen and len(login) >= min_login_length:
+            # Skip invalid characters for GitHub logins
+            if "_" in login:
+                continue
+            if login and login not in seen and len(login) >= min_login_length:
                 seen.add(login)
                 unique_logins.append(login)

44-60: Nit: surface HTTP fetch timeouts and source URL as options

Minor: get_repository_file_content supports a timeout; exposing --timeout and/or --source-url can help with debugging and self-hosted mirrors. Not required for this PR.

Example:

     def add_arguments(self, parser):
         """Add command arguments."""
         parser.add_argument(
             "--dry-run",
             action="store_true",
             help="Run without making changes to the database",
         )
+        parser.add_argument(
+            "--timeout",
+            type=float,
+            default=30,
+            help="HTTP timeout when fetching awards.yml (seconds)",
+        )
@@
-            yaml_content = get_repository_file_content(
-                "https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_data/awards.yml"
-            )
+            source_url = "https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_data/awards.yml"
+            yaml_content = get_repository_file_content(source_url, timeout=options.get("timeout", 30))
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 02f6581 and 1a25b2d.

📒 Files selected for processing (15)
  • backend/Makefile (2 hunks)
  • backend/apps/nest/admin/badge.py (1 hunks)
  • backend/apps/owasp/Makefile (2 hunks)
  • backend/apps/owasp/admin/__init__.py (1 hunks)
  • backend/apps/owasp/admin/award.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_sync_awards.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_update_badges.py (1 hunks)
  • backend/apps/owasp/migrations/0045_award.py (1 hunks)
  • backend/apps/owasp/migrations/0045_badge.py (0 hunks)
  • backend/apps/owasp/migrations/0046_merge_0045_badge_0045_project_audience.py (1 hunks)
  • backend/apps/owasp/models/__init__.py (1 hunks)
  • backend/apps/owasp/models/award.py (1 hunks)
  • backend/tests/apps/nest/management/__init__.py (1 hunks)
  • backend/tests/apps/nest/management/commands/__init__.py (1 hunks)
  • backend/tests/apps/owasp/models/award_test.py (1 hunks)
💤 Files with no reviewable changes (1)
  • backend/apps/owasp/migrations/0045_badge.py
🚧 Files skipped from review as they are similar to previous changes (8)
  • backend/tests/apps/nest/management/init.py
  • backend/tests/apps/nest/management/commands/init.py
  • backend/apps/owasp/models/init.py
  • backend/apps/owasp/admin/init.py
  • backend/apps/owasp/migrations/0046_merge_0045_badge_0045_project_audience.py
  • backend/Makefile
  • backend/apps/owasp/Makefile
  • backend/apps/nest/admin/badge.py
🧰 Additional context used
🧬 Code Graph Analysis (6)
backend/apps/owasp/management/commands/owasp_update_badges.py (2)
backend/apps/nest/models/badge.py (1)
  • Badge (10-41)
backend/apps/owasp/models/award.py (2)
  • Award (10-180)
  • Category (17-22)
backend/tests/apps/owasp/models/award_test.py (1)
backend/apps/owasp/models/award.py (2)
  • Award (10-180)
  • Category (17-22)
backend/apps/owasp/admin/award.py (1)
backend/apps/owasp/models/award.py (1)
  • Award (10-180)
backend/apps/owasp/migrations/0045_award.py (1)
backend/apps/owasp/migrations/0046_merge_0045_badge_0045_project_audience.py (1)
  • Migration (6-12)
backend/apps/owasp/models/award.py (1)
backend/apps/common/models.py (2)
  • BulkSaveModel (10-34)
  • TimestampedModel (37-46)
backend/apps/owasp/management/commands/owasp_sync_awards.py (2)
backend/apps/github/utils.py (1)
  • get_repository_file_content (60-79)
backend/apps/owasp/models/award.py (2)
  • Award (10-180)
  • update_data (128-164)
🔇 Additional comments (3)
backend/apps/owasp/admin/award.py (1)

12-27: Admin config looks solid

Good defaults, useful filters, search, ordering, and select_related("user") optimization.

backend/apps/owasp/management/commands/owasp_sync_awards.py (2)

67-77: Transactional writes + dry-run separation look solid

Good use of transaction.atomic() for real runs and a clear dry-run branch. This guards against partial syncs and makes the command safe to iterate on.


82-87: Filtering by “award” is correct
Fetched the canonical awards.yml shows only two types—52 award and 2 category. The code’s if item.get("type") == "award" correctly processes only award entries; category items are grouping headers and should be skipped. No change required.

@trucodd trucodd marked this pull request as ready for review August 17, 2025 06:35
@trucodd trucodd marked this pull request as draft August 17, 2025 09:18
@trucodd trucodd marked this pull request as draft August 19, 2025 14:58
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
backend/apps/owasp/management/commands/owasp_update_badges.py (1)

30-45: Avoid N+1 operations and report accurate changes; compute add/remove diffs within a transaction

Current flow iterates all winners to add the badge (even if they already have it) and then iterates all users-with-badge to remove if not a winner. This causes avoidable queries/writes and the final message reports total winners, not the delta. Compute set differences and wrap in a single atomic transaction for a consistent snapshot and fewer round-trips.

Apply this diff to streamline updates and improve logging:

-        # Get users with WASPY awards using the model method
-        waspy_winners = Award.get_waspy_award_winners()
-
-        # Add badge to WASPY winners
-        for user in waspy_winners:
-            user.badges.add(waspy_badge)
-
-        # Remove badge from users no longer on the WASPY winners list
-        users_with_badge = User.objects.filter(badges=waspy_badge)
-        waspy_winner_ids = set(waspy_winners.values_list("id", flat=True))
-
-        for user in users_with_badge:
-            if user.id not in waspy_winner_ids:
-                user.badges.remove(waspy_badge)
-
-        self.stdout.write(f"Updated badges for {waspy_winners.count()} WASPY winners")
+        with transaction.atomic():
+            # Compute desired and current holders
+            winner_ids = set(
+                Award.get_waspy_award_winners().values_list("id", flat=True)
+            )
+            current_ids = set(
+                User.objects.filter(badges=waspy_badge).values_list("id", flat=True)
+            )
+
+            to_add_ids = winner_ids - current_ids
+            to_remove_ids = current_ids - winner_ids
+
+            if to_add_ids:
+                for user in User.objects.filter(id__in=to_add_ids):
+                    user.badges.add(waspy_badge)
+            if to_remove_ids:
+                for user in User.objects.filter(id__in=to_remove_ids):
+                    user.badges.remove(waspy_badge)
+
+            self.stdout.write(
+                f"WASPY badge updated: +{len(to_add_ids)} added, -{len(to_remove_ids)} removed, total_winners={len(winner_ids)}"
+            )

Add the missing import at the top of the file:

from django.db import transaction
🧹 Nitpick comments (1)
backend/apps/owasp/management/commands/owasp_update_badges.py (1)

10-16: Add focused tests for badge updates (add/remove) via management command

Recommend a small test ensuring:

  • Users with reviewed WASPY awards gain the badge.
  • Users who no longer qualify lose the badge.
  • Idempotency (running twice doesn’t create duplicates).

I can draft a test like this (adjust paths/model factories to your project setup):

from django.core.management import call_command
from django.test import TestCase
from apps.github.models.user import User
from apps.nest.models.badge import Badge
from apps.owasp.models.award import Award

class TestOwaspUpdateBadges(TestCase):
    def setUp(self):
        self.user1 = User.objects.create(username="winner1")
        self.user2 = User.objects.create(username="not_winner")
        # Reviewed WASPY award -> should receive badge
        Award.objects.create(
            category=Award.Category.WASPY,
            name="Event Person of the Year - Winner One (2024)",
            description="",
            year=2024,
            user=self.user1,
            is_reviewed=True,
        )

    def test_award_badge_add_and_remove(self):
        call_command("owasp_update_badges")

        waspy_badge = Badge.objects.get(name="WASPY Award Winner")
        self.assertTrue(self.user1.badges.filter(pk=waspy_badge.pk).exists())
        self.assertFalse(self.user2.badges.filter(pk=waspy_badge.pk).exists())

        # Make user1 no longer eligible
        Award.objects.filter(user=self.user1).update(is_reviewed=False)
        call_command("owasp_update_badges")

        self.user1.refresh_from_db()
        self.assertFalse(self.user1.badges.filter(pk=waspy_badge.pk).exists())

    def test_idempotency(self):
        call_command("owasp_update_badges")
        call_command("owasp_update_badges")  # run twice without side effects
        waspy_badge = Badge.objects.get(name="WASPY Award Winner")
        self.assertEqual(
            self.user1.badges.filter(pk=waspy_badge.pk).count(), 1
        )
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f831803 and aebe403.

📒 Files selected for processing (6)
  • backend/Makefile (2 hunks)
  • backend/apps/owasp/Makefile (2 hunks)
  • backend/apps/owasp/management/commands/owasp_sync_awards.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_update_badges.py (1 hunks)
  • backend/apps/owasp/migrations/0045_award.py (1 hunks)
  • backend/apps/owasp/models/award.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • backend/apps/owasp/Makefile
  • backend/apps/owasp/models/award.py
  • backend/Makefile
  • backend/apps/owasp/migrations/0045_award.py
  • backend/apps/owasp/management/commands/owasp_sync_awards.py
🧰 Additional context used
🧬 Code Graph Analysis (1)
backend/apps/owasp/management/commands/owasp_update_badges.py (2)
backend/apps/nest/models/badge.py (1)
  • Badge (10-41)
backend/apps/owasp/models/award.py (2)
  • Award (10-181)
  • get_waspy_award_winners (99-110)
🔇 Additional comments (1)
backend/apps/owasp/management/commands/owasp_update_badges.py (1)

1-14: Good structure and imports — clear, idiomatic Django management command setup

Module docstring, imports, and Command skeleton look clean and consistent with the rest of the codebase.

@trucodd trucodd marked this pull request as ready for review August 19, 2025 16:03
Copy link
Collaborator

@arkid15r arkid15r left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@trucodd could you resolve coderabbit's comments first?

@trucodd trucodd marked this pull request as draft August 20, 2025 18:45
@trucodd trucodd force-pushed the feature/owasp-awards-profilesync branch from aebe403 to e224251 Compare August 23, 2025 05:52
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
backend/apps/owasp/Makefile (2)

29-33: PHONY decl + wiring for awards sync looks good.

Target correctly declared as phony and uses the existing exec-backend-command pattern to invoke the Django command.


73-77: Badge updater target is properly declared and consistent with the pipeline.

PHONY added and invocation mirrors other targets. No additional changes needed here.

backend/apps/owasp/management/commands/owasp_sync_awards.py (2)

108-121: Good: description is propagated into winner context

This addresses earlier feedback about carrying the award description to each winner row. Nice.


225-237: Fuzzy match still allows single-token matches; restrict to first+last containment

Current logic tries all contiguous token slices, including single tokens, which can misassign common first names. Require both first and last tokens to be present.

-    def _try_fuzzy_name_match(self, clean_name: str, min_name_parts: int) -> User | None:
-        """Try fuzzy name matching with partial matches."""
-        name_parts = clean_name.split()
-        if len(name_parts) >= min_name_parts:
-            # Try "FirstName LastName" variations
-            for i in range(len(name_parts)):
-                for j in range(i + 1, len(name_parts) + 1):
-                    partial_name = " ".join(name_parts[i:j])
-                    try:
-                        return User.objects.get(name__icontains=partial_name)
-                    except (User.DoesNotExist, User.MultipleObjectsReturned):
-                        continue
-        return None
+    def _try_fuzzy_name_match(self, clean_name: str, min_name_parts: int) -> User | None:
+        """Try fuzzy match requiring at least two tokens (e.g., first and last) to reduce false positives."""
+        parts = [p for p in clean_name.split() if p]
+        if len(parts) < max(min_name_parts, 2):
+            return None
+        first, last = parts[0], parts[-1]
+        qs = User.objects.filter(name__icontains=first).filter(name__icontains=last)
+        count = qs.count()
+        if count == 1:
+            return qs.first()
+        return None
🧹 Nitpick comments (8)
backend/tests/apps/owasp/management/commands/owasp_update_badges_test.py (1)

35-36: Optional: de-duplicate hardcoded badge name.

Using a module-level constant reduces string drift across tests and the command.

@@
-        waspy_badge = Badge.objects.get(name="WASPY Award Winner")
+        WASPY_BADGE_NAME = "WASPY Award Winner"
+        waspy_badge = Badge.objects.get(name=WASPY_BADGE_NAME)
@@
-        waspy_badge = Badge.objects.get(name="WASPY Award Winner")
+        waspy_badge = Badge.objects.get(name=WASPY_BADGE_NAME)
@@
-        Badge.objects.filter(name="WASPY Award Winner").delete()
+        Badge.objects.filter(name=WASPY_BADGE_NAME).delete()
@@
-        assert Badge.objects.filter(name="WASPY Award Winner").exists()
+        assert Badge.objects.filter(name=WASPY_BADGE_NAME).exists()
@@
-        waspy_badge = Badge.objects.get(name="WASPY Award Winner")
+        waspy_badge = Badge.objects.get(name=WASPY_BADGE_NAME)
@@
-        waspy_badge = Badge.objects.get(name="WASPY Award Winner")
+        waspy_badge = Badge.objects.get(name=WASPY_BADGE_NAME)

Also applies to: 52-54, 59-65, 67-68, 85-88

backend/apps/owasp/admin/award.py (2)

27-33: Add user__login to search fields for admin usability.

Makes it easier to find awards by GitHub username.

     search_fields = (
         "name",
         "winner_name",
         "description",
         "winner_info",
+        "user__login",
     )

68-80: Prefer @admin.action decorator over setting short_description attributes.

Modern, less error-prone, and avoids deprecated patterns.

-    def mark_reviewed(self, request, queryset):
+    @admin.action(description="Mark selected awards as reviewed")
+    def mark_reviewed(self, request, queryset):
         """Mark selected awards as reviewed."""
         updated = queryset.update(is_reviewed=True)
         self.message_user(request, f"Marked {updated} award(s) as reviewed.")
-
-    mark_reviewed.short_description = "Mark selected awards as reviewed"
 
-    def mark_unreviewed(self, request, queryset):
+    @admin.action(description="Mark selected awards as unreviewed")
+    def mark_unreviewed(self, request, queryset):
         """Mark selected awards as unreviewed."""
         updated = queryset.update(is_reviewed=False)
         self.message_user(request, f"Marked {updated} award(s) as unreviewed.")
-
-    mark_unreviewed.short_description = "Mark selected awards as unreviewed"
backend/tests/apps/owasp/models/award_test.py (3)

15-24: Prefer unittest assertions over bare asserts

Using unittest assertions gives better failure messages and avoids being skipped under Python optimizations.

Apply this diff:

-        choices = Award.Category.choices
-        assert ("WASPY", "WASPY") in choices
-        assert (
-            "Distinguished Lifetime Memberships",
-            "Distinguished Lifetime Memberships",
-        ) in choices
+        choices = Award.Category.choices
+        self.assertIn(("WASPY", "WASPY"), choices)
+        self.assertIn(
+            ("Distinguished Lifetime Memberships", "Distinguished Lifetime Memberships"),
+            choices,
+        )

26-31: Use unittest assertions for meta checks

Small consistency/readability win.

-        assert Award._meta.db_table == "owasp_awards"
-        assert Award._meta.verbose_name == "Award"
+        self.assertEqual(Award._meta.db_table, "owasp_awards")
+        self.assertEqual(Award._meta.verbose_name, "Award")

6-32: Optional: add behavior tests for str and display_name

Would lock in the formatting contract used by admin/UI and other call sites.

If helpful, I can add a follow-up test verifying:

  • str(Award(name="X - Y (2024)", year=2024, winner_name="Y")) == "X - Y (2024)"
  • award.display_name == "X - Y (2024)"
backend/apps/owasp/management/commands/owasp_sync_awards.py (2)

61-70: Harden YAML type validation to avoid runtime errors

Some YAML parsers return dicts when anchors/aliases are used. Guard for a non-list root to prevent iteration/type errors downstream.

             try:
                 awards_data = yaml.safe_load(yaml_content)
             except yaml.YAMLError as e:
                 self.stdout.write(self.style.ERROR(f"Failed to parse awards.yml: {e}"))
                 return
 
-            if not awards_data:
+            if not awards_data:
                 self.stdout.write(self.style.ERROR("Failed to parse awards.yml content"))
                 return
+
+            if not isinstance(awards_data, list):
+                self.stdout.write(self.style.ERROR("awards.yml root must be a list of entries"))
+                return

282-311: Broaden login variations with common initials patterns

Consider adding jdoe (first initial + last) and johnd (first + last initial). These are frequent GitHub login styles and relatively low-risk.

-        # Add variations with different cases
+        # Add variations with different cases
         for variation in base_variations:
             potential_logins.extend([variation, variation.replace("-", "")])
 
+        # Add initials-based patterns: jdoe, johnD, j-doe
+        parts = [p for p in clean_name.lower().split() if p]
+        if len(parts) >= 2:
+            first, last = parts[0], parts[-1]
+            fi = first[0]
+            li = last[0]
+            initials_variations = [
+                f"{fi}{last}",
+                f"{first}{li}",
+                f"{fi}-{last}",
+            ]
+            potential_logins.extend(initials_variations)
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between aebe403 and e224251.

📒 Files selected for processing (20)
  • backend/Makefile (2 hunks)
  • backend/apps/nest/admin/badge.py (1 hunks)
  • backend/apps/owasp/Makefile (2 hunks)
  • backend/apps/owasp/admin/__init__.py (1 hunks)
  • backend/apps/owasp/admin/award.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_sync_awards.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_update_badges.py (1 hunks)
  • backend/apps/owasp/migrations/0045_award.py (1 hunks)
  • backend/apps/owasp/migrations/0045_badge.py (0 hunks)
  • backend/apps/owasp/migrations/0046_merge_0045_badge_0045_project_audience.py (1 hunks)
  • backend/apps/owasp/migrations/0049_award_unique_constraint.py (1 hunks)
  • backend/apps/owasp/models/__init__.py (1 hunks)
  • backend/apps/owasp/models/award.py (1 hunks)
  • backend/tests/apps/nest/management/__init__.py (1 hunks)
  • backend/tests/apps/nest/management/commands/__init__.py (1 hunks)
  • backend/tests/apps/owasp/management/__init__.py (1 hunks)
  • backend/tests/apps/owasp/management/commands/__init__.py (1 hunks)
  • backend/tests/apps/owasp/management/commands/owasp_update_badges_test.py (1 hunks)
  • backend/tests/apps/owasp/models/award_duplicate_handling_test.py (1 hunks)
  • backend/tests/apps/owasp/models/award_test.py (1 hunks)
💤 Files with no reviewable changes (1)
  • backend/apps/owasp/migrations/0045_badge.py
✅ Files skipped from review due to trivial changes (2)
  • backend/tests/apps/owasp/management/init.py
  • backend/tests/apps/owasp/management/commands/init.py
🚧 Files skipped from review as they are similar to previous changes (10)
  • backend/apps/owasp/models/init.py
  • backend/tests/apps/nest/management/init.py
  • backend/apps/owasp/admin/init.py
  • backend/Makefile
  • backend/apps/owasp/management/commands/owasp_update_badges.py
  • backend/tests/apps/nest/management/commands/init.py
  • backend/apps/owasp/migrations/0046_merge_0045_badge_0045_project_audience.py
  • backend/apps/nest/admin/badge.py
  • backend/apps/owasp/migrations/0045_award.py
  • backend/apps/owasp/models/award.py
🧰 Additional context used
🧬 Code graph analysis (5)
backend/tests/apps/owasp/models/award_duplicate_handling_test.py (1)
backend/apps/owasp/models/award.py (3)
  • Award (10-183)
  • Category (17-22)
  • update_data (131-167)
backend/tests/apps/owasp/management/commands/owasp_update_badges_test.py (2)
backend/apps/nest/models/badge.py (1)
  • Badge (10-41)
backend/apps/owasp/models/award.py (2)
  • Award (10-183)
  • Category (17-22)
backend/apps/owasp/admin/award.py (1)
backend/apps/owasp/models/award.py (1)
  • Award (10-183)
backend/tests/apps/owasp/models/award_test.py (1)
backend/apps/owasp/models/award.py (2)
  • Award (10-183)
  • Category (17-22)
backend/apps/owasp/management/commands/owasp_sync_awards.py (3)
backend/apps/github/utils.py (1)
  • get_repository_file_content (60-79)
backend/apps/owasp/models/award.py (2)
  • Award (10-183)
  • update_data (131-167)
backend/apps/owasp/management/commands/owasp_update_badges.py (2)
  • Command (10-46)
  • handle (15-46)
🪛 checkmake (0.2.2)
backend/apps/owasp/Makefile

[warning] 73-73: Missing required phony target "all"

(minphony)


[warning] 73-73: Missing required phony target "clean"

(minphony)


[warning] 73-73: Missing required phony target "test"

(minphony)

🔇 Additional comments (5)
backend/tests/apps/owasp/management/commands/owasp_update_badges_test.py (1)

30-46: End-to-end coverage for add/remove flow looks solid.

Covers creation of the badge when missing, assignment, removal on unreview, and idempotency. Nice.

backend/tests/apps/owasp/models/award_test.py (1)

9-14: Import test looks good

The import check is clear and scoped. No issues.

backend/apps/owasp/management/commands/owasp_sync_awards.py (3)

249-281: Nice hardening of GitHub username extraction

Excluding reserved path segments and avoiding email local-part matches reduces false positives. Looks solid.


44-89: Overall command flow is solid and transactional

  • Safe YAML loading, dry-run support, atomic writes, and a clear summary are all in good shape.
  • Post-sync badge refresh via call_command is appropriate and decoupled.

143-161: Ignore removal of nest_updated_at—field is defined on Award
The nest_updated_at field is declared in apps/common/models.py and inherited by Award (which subclasses TimestampedModel). Keeping update_fields=["user", "nest_updated_at"] is correct.

• apps/common/models.py:46 defines nest_updated_at = models.DateTimeField(auto_now=True)
• apps/owasp/models/award.py:10 declares class Award(BulkSaveModel, TimestampedModel)

No changes needed here.

Likely an incorrect or invalid review comment.

@trucodd trucodd force-pushed the feature/owasp-awards-profilesync branch from e224251 to 289c8c5 Compare August 23, 2025 06:08
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (7)
backend/apps/owasp/Makefile (2)

29-33: Good addition: PHONY target and wiring look correct

The owasp-sync-awards target mirrors existing patterns and avoids filename collisions via .PHONY. Nice.


73-77: Good addition: PHONY target for badges update

The owasp-update-badges target is consistent with the rest of the Makefile, and the .PHONY guard is in place.

backend/apps/owasp/models/award.py (4)

36-41: Do not bake winner/year into name; remove unique=True and use a composite unique constraint instead

Current strategy builds name like "Title - Winner (Year)" to satisfy unique=True. This breaks semantics, causes duplication in str/display_name, and makes updates fragile. Model the domain explicitly.

Apply:

-    name = models.CharField(
+    name = models.CharField(
         verbose_name="Name",
         max_length=200,
-        unique=True,
         help_text="Award name/title (e.g., 'Event Person of the Year')",
     )

And add a UniqueConstraint (see next comment).


24-28: Enforce proper uniqueness at the DB layer

Allow recurring titles but ensure per-award/year/winner uniqueness.

 class Meta:
     db_table = "owasp_awards"
     verbose_name = "Award"
     verbose_name_plural = "Awards"
+    constraints = [
+        models.UniqueConstraint(
+            fields=["name", "category", "year", "winner_name"],
+            name="unique_award_name_category_year_winner",
+        )
+    ]

87-96: Fix duplicated winner/year in str and display_name

With the current ingestion, str becomes "Title - Winner (Year) - Winner (Year)" and display_name doubles the year. Return clean strings.

-    def __str__(self) -> str:
-        """Return string representation of the award."""
-        if self.winner_name:
-            return f"{self.name} - {self.winner_name} ({self.year})"
-        return f"{self.name} ({self.year})"
+    def __str__(self) -> str:
+        """Return string representation of the award."""
+        parts = [self.name]
+        if self.winner_name:
+            parts.append(f"- {self.winner_name}")
+        if self.year is not None:
+            parts.append(f"({self.year})")
+        return " ".join(parts)
@@
-    def display_name(self) -> str:
-        """Get display name for the award."""
-        return f"{self.name} ({self.year})"
+    def display_name(self) -> str:
+        """Get display name for the award."""
+        return f"{self.name} ({self.year})" if self.year is not None else self.name

141-161: Refactor update_data to use explicit keys instead of a synthesized “unique name”

Querying by synthesized name is brittle. Use the proper fields; this aligns with the composite UniqueConstraint and keeps name clean.

-        # Create unique name for each winner to satisfy unique constraint
-        award_title = (award_data.get("title") or "").strip()
-        category = (award_data.get("category") or "").strip()
-        year = award_data.get("year")
-        winner_name = (award_data.get("name") or "").strip()
-
-        # Create unique name combining award title, winner, and year
-        unique_name = f"{award_title} - {winner_name} ({year})"
-
-        try:
-            award = Award.objects.get(name=unique_name)
-        except Award.DoesNotExist:
-            award = Award(
-                name=unique_name,
-                category=category,
-                year=year,
-                winner_name=winner_name,
-            )
-        except Award.MultipleObjectsReturned:
-            award = Award.objects.filter(name=unique_name).order_by("id").first()
+        # Normalize fields
+        award_title = (award_data.get("title") or "").strip()
+        category = (award_data.get("category") or "").strip()
+        year = award_data.get("year")
+        winner_name = (award_data.get("name") or "").strip()
+
+        # Look up by domain fields (not a synthesized name)
+        try:
+            award = Award.objects.get(
+                name=award_title,
+                category=category,
+                year=year,
+                winner_name=winner_name,
+            )
+        except Award.DoesNotExist:
+            award = Award(
+                name=award_title,
+                category=category,
+                year=year,
+                winner_name=winner_name,
+            )
+        except Award.MultipleObjectsReturned:
+            award = (
+                Award.objects.filter(
+                    name=award_title,
+                    category=category,
+                    year=year,
+                    winner_name=winner_name,
+                )
+                .order_by("id")
+                .first()
+            )
backend/apps/owasp/management/commands/owasp_sync_awards.py (1)

225-237: Fuzzy matching is too permissive; risk of wrong user assignment

Single-token substring matches can collide easily (“John”, “Alex”). Require at least two name tokens and both to be present to consider a unique match.

-    def _try_fuzzy_name_match(self, clean_name: str, min_name_parts: int) -> User | None:
-        """Try fuzzy name matching with partial matches."""
-        name_parts = clean_name.split()
-        if len(name_parts) >= min_name_parts:
-            # Try "FirstName LastName" variations
-            for i in range(len(name_parts)):
-                for j in range(i + 1, len(name_parts) + 1):
-                    partial_name = " ".join(name_parts[i:j])
-                    try:
-                        return User.objects.get(name__icontains=partial_name)
-                    except (User.DoesNotExist, User.MultipleObjectsReturned):
-                        continue
-        return None
+    def _try_fuzzy_name_match(self, clean_name: str, min_name_parts: int) -> User | None:
+        """Try conservative fuzzy match: require first and last token to be present."""
+        parts = [p for p in clean_name.split() if p]
+        if len(parts) < max(2, min_name_parts):
+            return None
+        first, last = parts[0], parts[-1]
+        qs = User.objects.filter(name__icontains=first).filter(name__icontains=last)
+        count = qs.count()
+        return qs.first() if count == 1 else None
🧹 Nitpick comments (5)
backend/apps/owasp/Makefile (1)

73-73: Silence checkmake “minphony” warnings (optional)

Static analysis flags missing phony targets all/clean/test for this Makefile. If checkmake runs against sub-app Makefiles, either add stub targets or configure the linter to skip minphony here.

Example stubs near the top of this file:

+.PHONY: all clean test
+all:
+	@echo "noop (backend/apps/owasp/Makefile)"
+clean:
+	@echo "noop (backend/apps/owasp/Makefile)"
+test:
+	@echo "noop (backend/apps/owasp/Makefile)"
backend/tests/apps/owasp/models/award_test.py (1)

6-32: Increase coverage: idempotency, multi-winner per award/year, and string/display formatting

Current tests don’t cover update_data behavior, handling multiple winners for the same award/year, or str/display_name. Adding these will guard against regressions in ingestion and presentation.

Sample additions:

@@
 class AwardModelTest(TestCase):
@@
+    def test_update_data_idempotent_and_multi_winner(self):
+        from apps.owasp.models import Award
+        payload_common = {"title": "Community Member of the Year", "category": "WASPY", "year": 2024, "description": "Desc"}
+        w1 = {**payload_common, "name": "Alice", "info": "github.com/alice", "image": "/assets/a.png"}
+        w2 = {**payload_common, "name": "Bob", "info": "github.com/bob", "image": "/assets/b.png"}
+        a1 = Award.update_data(w1)
+        a1_again = Award.update_data(w1)
+        a2 = Award.update_data(w2)
+        assert a1.id == a1_again.id, "update_data must be idempotent for same winner"
+        assert {a1.winner_name, a2.winner_name} == {"Alice", "Bob"}, "multiple winners per award/year should be allowed"
+
+    def test_str_and_display_do_not_duplicate_winner_or_year(self):
+        from apps.owasp.models import Award
+        a = Award.update_data({"title": "Event Person of the Year", "category": "WASPY", "year": 2024, "name": "Charlie"})
+        # After saving, __str__ and display_name should not repeat (winner) or (year) info redundantly
+        s = str(a)
+        disp = a.display_name
+        assert "Charlie" in s and s.count("(2024)") == 1
+        assert disp == "Event Person of the Year (2024)"
backend/apps/owasp/admin/award.py (2)

27-33: Improve search by enabling user login search

Search often starts with a GitHub handle. Add user__login to search_fields.

     search_fields = (
         "name",
         "winner_name",
         "description",
         "winner_info",
+        "user__login",
     )

68-80: Use @admin.action decorator and update timestamps on bulk status changes

The decorator is cleaner than setting short_description. Also consider updating nest_updated_at, since bulk update won’t touch auto_now.

+from django.utils import timezone
@@
-    def mark_reviewed(self, request, queryset):
+    @admin.action(description="Mark selected awards as reviewed")
+    def mark_reviewed(self, request, queryset):
         """Mark selected awards as reviewed."""
-        updated = queryset.update(is_reviewed=True)
+        updated = queryset.update(is_reviewed=True, nest_updated_at=timezone.now())
         self.message_user(request, f"Marked {updated} award(s) as reviewed.")
-
-    mark_reviewed.short_description = "Mark selected awards as reviewed"
@@
-    def mark_unreviewed(self, request, queryset):
+    @admin.action(description="Mark selected awards as unreviewed")
+    def mark_unreviewed(self, request, queryset):
         """Mark selected awards as unreviewed."""
-        updated = queryset.update(is_reviewed=False)
+        updated = queryset.update(is_reviewed=False, nest_updated_at=timezone.now())
         self.message_user(request, f"Marked {updated} award(s) as unreviewed.")
-
-    mark_unreviewed.short_description = "Mark selected awards as unreviewed"
backend/apps/owasp/models/award.py (1)

64-69: Relative image paths from YAML may fail URLField validation in admin

awards.yml often uses “/assets/…”. URLField form validation expects absolute URLs. Consider a CharField here or a custom admin form field/validator that accepts absolute or site-relative paths.

If you want to keep URLField in the DB but relax admin validation only:

# admin form (place in admin/award.py)
from django import forms
from .award import Award

class AwardAdminForm(forms.ModelForm):
    class Meta:
        model = Award
        fields = "__all__"

    def clean_winner_image_url(self):
        val = self.cleaned_data.get("winner_image_url", "")
        # Accept site-relative paths like "/assets/.."
        return val

# then wire it:
class AwardAdmin(admin.ModelAdmin):
    form = AwardAdminForm
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e224251 and 289c8c5.

📒 Files selected for processing (11)
  • backend/apps/owasp/Makefile (2 hunks)
  • backend/apps/owasp/admin/award.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_sync_awards.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_update_badges.py (1 hunks)
  • backend/apps/owasp/migrations/0049_award_unique_constraint.py (1 hunks)
  • backend/apps/owasp/models/award.py (1 hunks)
  • backend/tests/apps/owasp/management/__init__.py (1 hunks)
  • backend/tests/apps/owasp/management/commands/__init__.py (1 hunks)
  • backend/tests/apps/owasp/management/commands/owasp_update_badges_test.py (1 hunks)
  • backend/tests/apps/owasp/models/award_duplicate_handling_test.py (1 hunks)
  • backend/tests/apps/owasp/models/award_test.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • backend/tests/apps/owasp/management/init.py
  • backend/apps/owasp/migrations/0049_award_unique_constraint.py
  • backend/tests/apps/owasp/management/commands/owasp_update_badges_test.py
  • backend/tests/apps/owasp/models/award_duplicate_handling_test.py
  • backend/apps/owasp/management/commands/owasp_update_badges.py
  • backend/tests/apps/owasp/management/commands/init.py
🧰 Additional context used
🧬 Code graph analysis (4)
backend/apps/owasp/admin/award.py (1)
backend/apps/owasp/models/award.py (1)
  • Award (10-183)
backend/tests/apps/owasp/models/award_test.py (1)
backend/apps/owasp/models/award.py (2)
  • Award (10-183)
  • Category (17-22)
backend/apps/owasp/models/award.py (1)
backend/apps/common/models.py (2)
  • BulkSaveModel (10-34)
  • TimestampedModel (37-46)
backend/apps/owasp/management/commands/owasp_sync_awards.py (3)
backend/apps/github/utils.py (1)
  • get_repository_file_content (60-79)
backend/apps/owasp/models/award.py (2)
  • Award (10-183)
  • update_data (131-167)
backend/apps/owasp/management/commands/owasp_update_badges.py (2)
  • Command (10-46)
  • handle (15-46)
🪛 checkmake (0.2.2)
backend/apps/owasp/Makefile

[warning] 73-73: Missing required phony target "all"

(minphony)


[warning] 73-73: Missing required phony target "clean"

(minphony)


[warning] 73-73: Missing required phony target "test"

(minphony)

🔇 Additional comments (8)
backend/apps/owasp/Makefile (1)

29-33: Confirmed correct ordering of OWASP targets

The backend/Makefile declares:

  • sync-data: update-data \ enrich-data \ owasp-update-badges \ index-data
  • update-data: … \ owasp-sync-awards \ …

This ensures that owasp-sync-awards (via update-data) runs before owasp-update-badges. No changes are required.

backend/tests/apps/owasp/models/award_test.py (2)

19-24: LGTM: Category choices assertions are aligned with the model

The checks for "WASPY" and "Distinguished Lifetime Memberships" match Award.Category.choices.


26-31: LGTM: Meta checks are precise

db_table and verbose_name assertions are correct and valuable for regression protection.

backend/apps/owasp/admin/award.py (1)

12-21: Solid admin list configuration

The chosen columns surface the most relevant fields (incl. review status and timestamps) and select_related/user autocomplete is a good touch for performance/UX.

backend/apps/owasp/models/award.py (1)

98-111: Confirm intended gating: badges only for reviewed awards

get_waspy_award_winners filters by is_reviewed=True. This means badges won’t be assigned until manual review. If this is intentional, all good; if not, consider making the gating conditional.

Would you like badges to reflect unreviewed matches too (e.g., behind a management command flag), or keep the current conservative behavior?

backend/apps/owasp/management/commands/owasp_sync_awards.py (3)

254-276: Nice hardening for GitHub handle extraction

Excluding reserved paths and avoiding email local-parts reduces false positives. Character class aligns with GitHub username rules.


330-340: Confirm post-sync badge update policy

Badges are updated after every successful sync (non-dry-run). Given get_waspy_award_winners filters reviewed=True, this is safe. If you later choose to include unreviewed matches, consider adding a --include-unreviewed flag to owasp_update_badges and pass it here.

Would you like me to add an optional flag path now, or keep the reviewed-only default?


96-105: Category field mapping verified as correct
The award_data.get("category") call aligns with the OWASP awards.yml schema, where award entries indeed use the category key for grouping (e.g., “WASPY”). No changes required.

@trucodd trucodd force-pushed the feature/owasp-awards-profilesync branch 2 times, most recently from cce7323 to 3decd96 Compare August 23, 2025 06:34
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (6)
backend/tests/apps/owasp/models/award_duplicate_handling_test.py (1)

23-34: Good fix: simulate duplicates via patch instead of violating uniqueness.

Switching to patching Award.objects.get to raise MultipleObjectsReturned is the right way to exercise the branch without breaking the unique constraint. Matches the earlier guidance.

backend/apps/owasp/models/award.py (3)

36-41: Remove unique=True on name; enforce uniqueness on domain fields instead.

name should hold the award title (e.g., “Event Person of the Year”). Making it unique prevents valid inserts across years and winners, and forced the code to embed winner/year into name. Move uniqueness to a composite including name, category, year, and winner_name.

-    name = models.CharField(
+    name = models.CharField(
         verbose_name="Name",
         max_length=200,
-        unique=True,
         help_text="Award name/title (e.g., 'Event Person of the Year')",
     )

24-28: Add a composite unique constraint to allow multiple years and winners.

This aligns persistence with the data model (award title repeats each year and can have multiple winners).

 class Meta:
     db_table = "owasp_awards"
     verbose_name = "Award"
     verbose_name_plural = "Awards"
+    constraints = [
+        models.UniqueConstraint(
+            fields=["name", "category", "year", "winner_name"],
+            name="unique_award_name_year_category_winner",
+        )
+    ]

87-96: Fix double-print and avoid “(None)” in string representations.

Current __str__ duplicates winner/year if name already embeds them; and display_name shows (None) if year is missing in any future cases. Build strings from fields, not preformatted name.

-    def __str__(self) -> str:
-        """Return string representation of the award."""
-        if self.winner_name:
-            return f"{self.name} - {self.winner_name} ({self.year})"
-        return f"{self.name} ({self.year})"
+    def __str__(self) -> str:
+        """Return string representation of the award."""
+        parts = [self.name]
+        if self.winner_name:
+            parts.append(f"- {self.winner_name}")
+        if self.year is not None:
+            parts.append(f"({self.year})")
+        return " ".join(parts)
@@
-    def display_name(self) -> str:
-        """Get display name for the award."""
-        return f"{self.name} ({self.year})"
+    def display_name(self) -> str:
+        """Get display name for the award."""
+        return f"{self.name} ({self.year})" if self.year is not None else self.name
backend/apps/owasp/management/commands/owasp_sync_awards.py (2)

147-159: Detect create/update using domain fields, not synthesized unique_name.

This logic should mirror the model’s uniqueness (name, category, year, winner_name). It also prevents divergence if string formatting changes.

-            # Check if award exists before update using unique name
-            unique_name = f"{award_name} - {winner_name} ({winner_data.get('year')})"
-            try:
-                Award.objects.get(name=unique_name)
-                is_new = False
-            except Award.DoesNotExist:
-                is_new = True
-            except Award.MultipleObjectsReturned:
-                is_new = False
+            # Check existence using domain fields (mirrors model uniqueness)
+            is_new = not Award.objects.filter(
+                name=award_name,
+                category=winner_data.get("category", ""),
+                year=winner_data.get("year"),
+                winner_name=winner_name,
+            ).exists()

229-241: Fuzzy matching is too permissive; tighten to reduce false positives.

Single-token containment can easily misassign users (e.g., “John”). Require at least two tokens and ensure both are present.

-    def _try_fuzzy_name_match(self, clean_name: str, min_name_parts: int) -> User | None:
-        """Try fuzzy name matching with partial matches."""
-        name_parts = clean_name.split()
-        if len(name_parts) >= min_name_parts:
-            # Try "FirstName LastName" variations
-            for i in range(len(name_parts)):
-                for j in range(i + 1, len(name_parts) + 1):
-                    partial_name = " ".join(name_parts[i:j])
-                    try:
-                        return User.objects.get(name__icontains=partial_name)
-                    except (User.DoesNotExist, User.MultipleObjectsReturned):
-                        continue
-        return None
+    def _try_fuzzy_name_match(self, clean_name: str, min_name_parts: int) -> User | None:
+        """Try fuzzy match by requiring at least two tokens present in name."""
+        parts = [p for p in clean_name.split() if p]
+        if len(parts) < max(2, min_name_parts):
+            return None
+        first, last = parts[0], parts[-1]
+        qs = User.objects.filter(name__icontains=first).filter(name__icontains=last)
+        count = qs.count()
+        if count == 1:
+            return qs.first()
+        return None
🧹 Nitpick comments (6)
backend/tests/apps/owasp/models/award_duplicate_handling_test.py (1)

35-36: Prefer unittest assertions over bare assert.

Use self.assertEqual for clearer failure messages and consistency with Django’s TestCase.

-        assert result_award.id == award1.id
+        self.assertEqual(result_award.id, award1.id)
backend/apps/owasp/Makefile (1)

73-73: Optional: Satisfy checkmake minphony (all/clean/test) or suppress locally.

Static analysis warns about missing all, clean, test. If this app-level Makefile is linted independently, consider stubbing these or relaxing the rule for this path.

If you want to appease checkmake without changing the top-level workflow, add no-op stubs:

.PHONY: all clean test
all:
	@echo "Use top-level Makefile targets"

clean:
	@echo "Use top-level Makefile clean"

test:
	@echo "Use top-level Makefile test"
backend/apps/owasp/admin/award.py (2)

12-21: Admin config is solid; consider minor UX tweaks.

  • Keep as-is functionally. Optional improvements:
    • Add list_display_links = ("name", "winner_name") to make rows easier to open.
    • Add list_per_page = 50 to keep pages lighter on large datasets.
 class AwardAdmin(admin.ModelAdmin):
@@
-    list_display = (
+    list_display = (
         "name",
         "category",
         "year",
         "winner_name",
         "user",
         "is_reviewed",
         "nest_created_at",
         "nest_updated_at",
     )
+    list_display_links = ("name", "winner_name")
+    list_per_page = 50

40-57: Heads-up: winner_image_url may be relative; admin edits can fail URL validation.

awards.yml often uses paths like /assets/.... Django admin will validate URLField and may reject these on save. Normalize to an absolute URL in the model setter to prevent admin friction. See model comment for a concrete patch to from_dict.

backend/apps/owasp/models/award.py (2)

176-180: Normalize relative image paths to absolute URLs for admin compatibility.

awards.yml often uses /assets/..., which fails URLField validation in admin. Prefix with the site domain when path-like.

-        fields = {
-            "description": (data.get("description") or "").strip(),
-            "winner_info": (data.get("info") or data.get("winner_info") or "").strip(),
-            "winner_image_url": (data.get("image") or data.get("winner_image_url") or "").strip(),
-        }
+        raw_image = (data.get("image") or data.get("winner_image_url") or "").strip()
+        if raw_image and raw_image.startswith("/"):
+            # Convert to absolute URL for admin/form validation
+            raw_image = f"https://owasp.org{raw_image}"
+        fields = {
+            "description": (data.get("description") or "").strip(),
+            "winner_info": (data.get("info") or data.get("winner_info") or "").strip(),
+            "winner_image_url": raw_image,
+        }

45-48: Confirm whether year needs to be nullable.

If you plan to persist “category” rows (no year) later, align with a nullable year (null=True, blank=True) to avoid future migrations. If awards-only, current non-null is fine.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cce7323 and 3decd96.

📒 Files selected for processing (11)
  • backend/apps/owasp/Makefile (2 hunks)
  • backend/apps/owasp/admin/award.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_sync_awards.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_update_badges.py (1 hunks)
  • backend/apps/owasp/migrations/0049_award_unique_constraint.py (1 hunks)
  • backend/apps/owasp/models/award.py (1 hunks)
  • backend/tests/apps/owasp/management/__init__.py (1 hunks)
  • backend/tests/apps/owasp/management/commands/__init__.py (1 hunks)
  • backend/tests/apps/owasp/management/commands/owasp_update_badges_test.py (1 hunks)
  • backend/tests/apps/owasp/models/award_duplicate_handling_test.py (1 hunks)
  • backend/tests/apps/owasp/models/award_test.py (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • backend/tests/apps/owasp/management/commands/init.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • backend/tests/apps/owasp/management/init.py
  • backend/apps/owasp/management/commands/owasp_update_badges.py
  • backend/tests/apps/owasp/models/award_test.py
  • backend/apps/owasp/migrations/0049_award_unique_constraint.py
  • backend/tests/apps/owasp/management/commands/owasp_update_badges_test.py
🧰 Additional context used
🧬 Code graph analysis (4)
backend/apps/owasp/models/award.py (1)
backend/apps/common/models.py (2)
  • BulkSaveModel (10-34)
  • TimestampedModel (37-46)
backend/tests/apps/owasp/models/award_duplicate_handling_test.py (1)
backend/apps/owasp/models/award.py (3)
  • Award (10-183)
  • Category (17-22)
  • update_data (131-167)
backend/apps/owasp/admin/award.py (1)
backend/apps/owasp/models/award.py (1)
  • Award (10-183)
backend/apps/owasp/management/commands/owasp_sync_awards.py (3)
backend/apps/github/utils.py (1)
  • get_repository_file_content (60-79)
backend/apps/owasp/models/award.py (2)
  • Award (10-183)
  • update_data (131-167)
backend/apps/owasp/management/commands/owasp_update_badges.py (2)
  • Command (10-46)
  • handle (15-46)
🪛 checkmake (0.2.2)
backend/apps/owasp/Makefile

[warning] 73-73: Missing required phony target "all"

(minphony)


[warning] 73-73: Missing required phony target "clean"

(minphony)


[warning] 73-73: Missing required phony target "test"

(minphony)

🔇 Additional comments (5)
backend/apps/owasp/Makefile (2)

29-34: PHONY target and wiring look good.

owasp-sync-awards is correctly marked .PHONY and calls the Django command through the existing exec wrapper.


73-77: PHONY target for badges update looks good.

owasp-update-badges is correctly marked .PHONY and delegates to manage.py owasp_update_badges.

backend/apps/owasp/models/award.py (1)

1-184: AI summary mismatch: it claims unique=True was removed, but code still has it.

The AI summary mentions removing unique=True from name; the code still declares it. Please reconcile to avoid schema drift.

backend/apps/owasp/management/commands/owasp_sync_awards.py (2)

100-111: YAML key mapping LGTM; continue skipping non-award entries.

Reading title/category/year and skipping where incomplete is correct; limiting to type == "award" matches the upstream structure.


334-344: Badge update hook is appropriate and isolated.

Calling owasp_update_badges after a successful sync (outside dry-run) is correct and keeps concerns separated.

@trucodd trucodd force-pushed the feature/owasp-awards-profilesync branch from 3decd96 to c2eb7a1 Compare August 23, 2025 07:10
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (5)
backend/apps/owasp/Makefile (1)

29-34: PHONY + wiring for owasp-sync-awards look good

Target is correctly declared phony and wired through the existing exec-backend-command mechanism. No further action from my side.

backend/apps/owasp/models/award.py (2)

24-41: Fix uniqueness: remove unique=True on name and enforce composite uniqueness

A single award title repeats across years and can have multiple winners. Storing a synthesized “unique name” in name is brittle and breaks admin display; relying on unique=True here either blocks valid data or forces the current workaround. Prefer domain-level uniqueness at the DB: (name, category, year, winner_name).

Apply:

 class Meta:
     db_table = "owasp_awards"
     verbose_name = "Award"
     verbose_name_plural = "Awards"
+    constraints = [
+        models.UniqueConstraint(
+            fields=["name", "category", "year", "winner_name"],
+            name="unique_award_name_year_category_winner",
+        )
+    ]

@@
-    name = models.CharField(
+    name = models.CharField(
         verbose_name="Name",
         max_length=200,
-        unique=True,
         help_text="Award name/title (e.g., 'Event Person of the Year')",
     )

Follow-up: generate a migration for the new constraint and dropping the unique index on name.


130-162: Stop synthesizing name; select/update by domain fields

Building a “unique_name = '{title} - {winner} ({year})'” leaks presentation into storage, breaks admin display, and complicates lookups. Use the real title in name and select/update by (name, category, year, winner_name). This also aligns with the composite DB constraint above.

-        # Create unique name for each winner to satisfy unique constraint
-        award_title = (award_data.get("title") or "").strip()
-        category = (award_data.get("category") or "").strip()
-        year = award_data.get("year")
-        winner_name = (award_data.get("name") or "").strip()
-
-        # Create unique name combining award title, winner, and year
-        unique_name = f"{award_title} - {winner_name} ({year})"
-
-        try:
-            award = Award.objects.get(name=unique_name)
-        except Award.DoesNotExist:
-            award = Award(
-                name=unique_name,
-                category=category,
-                year=year,
-                winner_name=winner_name,
-            )
-        except Award.MultipleObjectsReturned:
-            award = Award.objects.filter(name=unique_name).order_by("id").first()
+        award_title = (award_data.get("title") or "").strip()
+        category = (award_data.get("category") or "").strip()
+        year = award_data.get("year")
+        winner_name = (award_data.get("name") or "").strip()
+
+        # Select by domain fields; tolerate duplicates defensively
+        award = (
+            Award.objects.filter(
+                name=award_title,
+                category=category,
+                year=year,
+                winner_name=winner_name,
+            )
+            .order_by("id")
+            .first()
+        )
+        if not award:
+            award = Award(
+                name=award_title,
+                category=category,
+                year=year,
+                winner_name=winner_name,
+            )

No behavioral change to from_dict/save below; they remain correct.

backend/apps/owasp/management/commands/owasp_sync_awards.py (2)

147-157: Existence check should use domain fields (not synthesized name)

Relying on a formatted string is fragile and diverges from the model’s intended uniqueness. Detect on (name, category, year, winner_name) so stats are accurate and logic remains stable if formatting changes.

-            # Check if award exists before update using unique name
-            unique_name = f"{award_name} - {winner_name} ({winner_data.get('year')})"
-            try:
-                Award.objects.get(name=unique_name)
-                is_new = False
-            except Award.DoesNotExist:
-                is_new = True
-            except Award.MultipleObjectsReturned:
-                is_new = False
+            # Check existence using domain fields
+            exists = Award.objects.filter(
+                name=award_name,
+                category=winner_data.get("category", ""),
+                year=winner_data.get("year"),
+                winner_name=winner_name,
+            ).exists()
+            is_new = not exists

229-241: Constrain fuzzy matching to reduce false-positive user assignments

Single-token contains() matches are risky (e.g., “John” will hit many). Require at least two tokens (first+last) and ensure both are present in the name. This materially reduces mis-assignments.

-    def _try_fuzzy_name_match(self, clean_name: str, min_name_parts: int) -> User | None:
-        """Try fuzzy name matching with partial matches."""
-        name_parts = clean_name.split()
-        if len(name_parts) >= min_name_parts:
-            # Try "FirstName LastName" variations
-            for i in range(len(name_parts)):
-                for j in range(i + 1, len(name_parts) + 1):
-                    partial_name = " ".join(name_parts[i:j])
-                    try:
-                        return User.objects.get(name__icontains=partial_name)
-                    except (User.DoesNotExist, User.MultipleObjectsReturned):
-                        continue
-        return None
+    def _try_fuzzy_name_match(self, clean_name: str, min_name_parts: int) -> User | None:
+        """Try fuzzy match by requiring at least two tokens present in name."""
+        parts = [p for p in clean_name.split() if p]
+        if len(parts) < max(2, min_name_parts):
+            return None
+        first, last = parts[0], parts[-1]
+        qs = User.objects.filter(name__icontains=first).filter(name__icontains=last)
+        if qs.count() == 1:
+            return qs.first()
+        return None
🧹 Nitpick comments (1)
backend/apps/owasp/models/award.py (1)

87-96: Avoid duplicate winner/year in string representation

Because update_data currently writes winner and year into name, str and display_name end up duplicating them. Even after fixing update_data, this is a safer implementation and prevents “(None)” when year is absent in future.

-    def __str__(self) -> str:
-        """Return string representation of the award."""
-        if self.winner_name:
-            return f"{self.name} - {self.winner_name} ({self.year})"
-        return f"{self.name} ({self.year})"
+    def __str__(self) -> str:
+        """Return string representation of the award."""
+        parts = [self.name]
+        if self.winner_name:
+            parts.append(f"- {self.winner_name}")
+        if self.year is not None:
+            parts.append(f"({self.year})")
+        return " ".join(parts)
@@
-    def display_name(self) -> str:
-        """Get display name for the award."""
-        return f"{self.name} ({self.year})"
+    def display_name(self) -> str:
+        """Get display name for the award."""
+        return f"{self.name} ({self.year})" if self.year is not None else self.name
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3decd96 and c2eb7a1.

📒 Files selected for processing (11)
  • backend/apps/owasp/Makefile (2 hunks)
  • backend/apps/owasp/admin/award.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_sync_awards.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_update_badges.py (1 hunks)
  • backend/apps/owasp/migrations/0049_award_unique_constraint.py (1 hunks)
  • backend/apps/owasp/models/award.py (1 hunks)
  • backend/tests/apps/owasp/management/__init__.py (1 hunks)
  • backend/tests/apps/owasp/management/commands/__init__.py (1 hunks)
  • backend/tests/apps/owasp/management/commands/owasp_update_badges_test.py (1 hunks)
  • backend/tests/apps/owasp/models/award_duplicate_handling_test.py (1 hunks)
  • backend/tests/apps/owasp/models/award_test.py (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • backend/tests/apps/owasp/management/commands/init.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • backend/tests/apps/owasp/management/init.py
  • backend/tests/apps/owasp/management/commands/owasp_update_badges_test.py
  • backend/apps/owasp/management/commands/owasp_update_badges.py
  • backend/tests/apps/owasp/models/award_test.py
  • backend/apps/owasp/admin/award.py
  • backend/apps/owasp/migrations/0049_award_unique_constraint.py
  • backend/tests/apps/owasp/models/award_duplicate_handling_test.py
🧰 Additional context used
🧬 Code graph analysis (2)
backend/apps/owasp/management/commands/owasp_sync_awards.py (3)
backend/apps/github/utils.py (1)
  • get_repository_file_content (60-79)
backend/apps/owasp/models/award.py (2)
  • Award (10-187)
  • update_data (131-167)
backend/apps/owasp/management/commands/owasp_update_badges.py (2)
  • Command (10-46)
  • handle (15-46)
backend/apps/owasp/models/award.py (1)
backend/apps/common/models.py (2)
  • BulkSaveModel (10-34)
  • TimestampedModel (37-46)
🪛 checkmake (0.2.2)
backend/apps/owasp/Makefile

[warning] 73-73: Missing required phony target "all"

(minphony)


[warning] 73-73: Missing required phony target "clean"

(minphony)


[warning] 73-73: Missing required phony target "test"

(minphony)

🔇 Additional comments (6)
backend/apps/owasp/Makefile (1)

73-77: Orchestration confirmed: owasp-update-badges runs after awards sync

Verified that in backend/Makefile, the sync-data target depends first on update-data (which invokes owasp-sync-awards), then on enrich-data, and only afterward on owasp-update-badges. The intended ordering is correct—no further changes are needed.

backend/apps/owasp/models/award.py (2)

98-111: WASPY winners query is concise and correct

Using the reverse relation and distinct() is efficient and clear. Good call on gating by is_reviewed to reflect human validation.


45-48: Year nullability confirmed aligned with migrations

The year field in the initial CreateModel migration (0045_award.py) is defined without null=True (i.e. non-nullable), matching the current model declaration.

  • If every Award row will always have a year, no changes are required.
  • If you intend to persist “category” entries without a year, you’ll need to update both the model and create a new migration to add null=True (and blank=True if desired) on year to avoid schema drift.
backend/apps/owasp/management/commands/owasp_sync_awards.py (3)

100-109: YAML field mapping looks correct

award_name from "title", category from "category", and year from "year" match the current awards.yml structure for items where type: award. Verified against the canonical file on August 23, 2025. (raw.githubusercontent.com)


112-126: Good: winner context carries description/info/image

Threading description into each winner ensures Award.description mirrors upstream and avoids empty descriptions on created rows. Nice.


334-345: Post-sync badge update is a nice touch

Updating badges after a successful sync keeps UX coherent. Wrapped in try/except with logging — LGTM.

@trucodd trucodd force-pushed the feature/owasp-awards-profilesync branch from c2eb7a1 to 84f3b1f Compare August 23, 2025 07:33
Copy link

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
backend/apps/owasp/management/commands/owasp_sync_awards.py (2)

229-241: Tighten fuzzy matching to reduce false positives

Require at least two tokens and ensure both appear in the candidate’s name; avoid single-token substring matches that can misassign users.

-    def _try_fuzzy_name_match(self, clean_name: str, min_name_parts: int) -> User | None:
-        """Try fuzzy name matching with partial matches."""
-        name_parts = clean_name.split()
-        if len(name_parts) >= min_name_parts:
-            # Try "FirstName LastName" variations
-            for i in range(len(name_parts)):
-                for j in range(i + 1, len(name_parts) + 1):
-                    partial_name = " ".join(name_parts[i:j])
-                    try:
-                        return User.objects.get(name__icontains=partial_name)
-                    except (User.DoesNotExist, User.MultipleObjectsReturned):
-                        continue
-        return None
+    def _try_fuzzy_name_match(self, clean_name: str, min_name_parts: int) -> User | None:
+        """Try fuzzy match by requiring at least two tokens present in the same name."""
+        parts = [p for p in clean_name.split() if p]
+        if len(parts) < max(2, min_name_parts):
+            return None
+        first, last = parts[0], parts[-1]
+        qs = User.objects.filter(name__icontains=first).filter(name__icontains=last)
+        return qs.first() if qs.count() == 1 else None

147-156: Detect created/updated via composite fields, not synthesized unique_name

Once the model enforces composite uniqueness, use the same fields here to decide is_new. This removes the brittle unique_name coupling.

-        if not dry_run:
-            # Check if award exists before update using unique name
-            unique_name = f"{award_name} - {winner_name} ({winner_data.get('year')})"
-            try:
-                Award.objects.get(name=unique_name)
-                is_new = False
-            except Award.DoesNotExist:
-                is_new = True
-            except Award.MultipleObjectsReturned:
-                is_new = False
+        if not dry_run:
+            # Detect existence using domain fields
+            composite_lookup = {
+                "name": award_name,
+                "category": winner_data.get("category", ""),
+                "year": winner_data.get("year"),
+                "winner_name": winner_name,
+            }
+            is_new = not Award.objects.filter(**composite_lookup).exists()
🧹 Nitpick comments (2)
backend/tests/apps/owasp/models/award_test.py (1)

13-14: Prefer unittest assertions over bare asserts

Use TestCase helpers for clearer failures and consistency with Django’s test runner.

-        assert Award is not None
+        self.assertIsNotNone(Award)

-        choices = Award.Category.choices
-        assert ("WASPY", "WASPY") in choices
-        assert (
-            "Distinguished Lifetime Memberships",
-            "Distinguished Lifetime Memberships",
-        ) in choices
+        choices = Award.Category.choices
+        self.assertIn(("WASPY", "WASPY"), choices)
+        self.assertIn(
+            ("Distinguished Lifetime Memberships", "Distinguished Lifetime Memberships"),
+            choices,
+        )

-        assert Award._meta.db_table == "owasp_awards"
-        assert Award._meta.verbose_name == "Award"
+        self.assertEqual(Award._meta.db_table, "owasp_awards")
+        self.assertEqual(Award._meta.verbose_name, "Award")

Also applies to: 19-24, 30-31

backend/apps/owasp/Makefile (1)

73-73: Optional: add common stub targets if checkmake is gating your CI

Static analysis hints flagged missing “all/clean/test”. If your repo enforces these per-Makefile, add minimal stubs; otherwise ignore.

If needed, add at the end of this Makefile (example):

.PHONY: all clean test
all:
	@echo "backend/apps/owasp: no default target"

clean:
	@echo "backend/apps/owasp: nothing to clean"

test:
	@echo "backend/apps/owasp: no tests bound here"
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c2eb7a1 and 84f3b1f.

📒 Files selected for processing (11)
  • backend/apps/owasp/Makefile (2 hunks)
  • backend/apps/owasp/admin/award.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_sync_awards.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_update_badges.py (1 hunks)
  • backend/apps/owasp/migrations/0049_award_unique_constraint.py (1 hunks)
  • backend/apps/owasp/models/award.py (1 hunks)
  • backend/tests/apps/owasp/management/__init__.py (1 hunks)
  • backend/tests/apps/owasp/management/commands/__init__.py (1 hunks)
  • backend/tests/apps/owasp/management/commands/owasp_update_badges_test.py (1 hunks)
  • backend/tests/apps/owasp/models/award_duplicate_handling_test.py (1 hunks)
  • backend/tests/apps/owasp/models/award_test.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • backend/tests/apps/owasp/management/init.py
  • backend/tests/apps/owasp/management/commands/init.py
  • backend/apps/owasp/migrations/0049_award_unique_constraint.py
  • backend/apps/owasp/management/commands/owasp_update_badges.py
  • backend/tests/apps/owasp/management/commands/owasp_update_badges_test.py
  • backend/tests/apps/owasp/models/award_duplicate_handling_test.py
  • backend/apps/owasp/admin/award.py
🧰 Additional context used
🧬 Code graph analysis (3)
backend/tests/apps/owasp/models/award_test.py (1)
backend/apps/owasp/models/award.py (2)
  • Award (10-190)
  • Category (17-22)
backend/apps/owasp/management/commands/owasp_sync_awards.py (2)
backend/apps/github/utils.py (1)
  • get_repository_file_content (60-79)
backend/apps/owasp/models/award.py (2)
  • Award (10-190)
  • update_data (134-170)
backend/apps/owasp/models/award.py (1)
backend/apps/common/models.py (2)
  • BulkSaveModel (10-34)
  • TimestampedModel (37-46)
🪛 checkmake (0.2.2)
backend/apps/owasp/Makefile

[warning] 73-73: Missing required phony target "all"

(minphony)


[warning] 73-73: Missing required phony target "clean"

(minphony)


[warning] 73-73: Missing required phony target "test"

(minphony)

🔇 Additional comments (3)
backend/apps/owasp/Makefile (1)

29-34: New PHONY targets are wired correctly to management commands

Targets and echo lines look good; discovery will work via the existing exec-backend-command pattern.

Also applies to: 73-77

backend/apps/owasp/models/award.py (1)

45-48: No action needed on year nullability

The 0045_award.py migration defines the year field without null=True, making it non-nullable by default—this aligns with the model’s current IntegerField (which is also null=False by default). There’s no drift between the model and migrations here, so you can leave the field as-is.

backend/apps/owasp/management/commands/owasp_sync_awards.py (1)

258-283: Good hardening of GitHub username extraction

Nice exclusion of reserved paths and removal of underscores; avoids matching emails and org URLs.

Comment on lines +36 to +41
name = models.CharField(
verbose_name="Name",
max_length=200,
unique=True,
help_text="Award name/title (e.g., 'Event Person of the Year')",
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix schema: remove unique=True from name and enforce composite uniqueness; stop synthesizing name

Current design stores “title - winner (year)” in name to satisfy a single-field unique constraint, which fights the domain model and complicates queries. Use the real award title in name and enforce uniqueness across (name, year, category, winner_name). Update update_data accordingly.

 class Meta:
     db_table = "owasp_awards"
     verbose_name = "Award"
     verbose_name_plural = "Awards"
+    constraints = [
+        models.UniqueConstraint(
+            fields=["name", "year", "category", "winner_name"],
+            name="unique_award_name_year_category_winner",
+        )
+    ]

@@
-    name = models.CharField(
-        verbose_name="Name",
-        max_length=200,
-        unique=True,
-        help_text="Award name/title (e.g., 'Event Person of the Year')",
-    )
+    name = models.CharField(
+        verbose_name="Name",
+        max_length=200,
+        help_text="Award name/title (e.g., 'Event Person of the Year')",
+    )

@@
-        # Create unique name for each winner to satisfy unique constraint
-        award_title = (award_data.get("title") or "").strip()
-        category = (award_data.get("category") or "").strip()
-        year = award_data.get("year")
-        winner_name = (award_data.get("name") or "").strip()
-
-        # Create unique name combining award title, winner, and year
-        unique_name = f"{award_title} - {winner_name} ({year})"
-
-        try:
-            award = Award.objects.get(name=unique_name)
-        except Award.DoesNotExist:
-            award = Award(
-                name=unique_name,
-                category=category,
-                year=year,
-                winner_name=winner_name,
-            )
-        except Award.MultipleObjectsReturned:
-            award = Award.objects.filter(name=unique_name).order_by("id").first()
+        award_title = (award_data.get("title") or "").strip()
+        category = (award_data.get("category") or "").strip()
+        year = award_data.get("year")
+        winner_name = (award_data.get("name") or "").strip()
+
+        try:
+            award = Award.objects.get(
+                name=award_title,
+                category=category,
+                year=year,
+                winner_name=winner_name,
+            )
+        except Award.DoesNotExist:
+            award = Award(
+                name=award_title,
+                category=category,
+                year=year,
+                winner_name=winner_name,
+            )
+        except Award.MultipleObjectsReturned:
+            award = (
+                Award.objects.filter(
+                    name=award_title,
+                    category=category,
+                    year=year,
+                    winner_name=winner_name,
+                )
+                .order_by("id")
+                .first()
+            )

Follow-up migration required: drop the unique index on name and add the composite UniqueConstraint.

Want me to generate the migration diff as well?

Also applies to: 24-28, 145-165

🤖 Prompt for AI Agents
In backend/apps/owasp/models/award.py around lines 24-28, 36-41 and 145-165, the
model currently synthesizes a unique "name" value ("title - winner (year)") and
sets unique=True on name; instead, revert name to store only the real award
title (remove unique=True), stop synthesizing name in model
methods/serializers/update_data, and add a composite UniqueConstraint on (name,
year, category, winner_name) in the model's Meta; update update_data logic to
populate separate fields (title/name, year, category, winner_name) rather than
composing a unique name, and plan a migration that drops the unique index on
name and adds the composite UniqueConstraint.

@trucodd trucodd marked this pull request as ready for review August 23, 2025 07:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Sync OWASP Awards data and integrate with user profiles
2 participants