Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ google-credentials.json


.env
.DS_Store
7 changes: 7 additions & 0 deletions blt/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
)
from website.feeds import ActivityFeed
from website.views.adventure import AdventureDetailView, AdventureListView, start_adventure, submit_task
from website.views.api.trademark import organization_trademark_check, trademark_report_api
from website.views.banned_apps import BannedAppsView, search_banned_apps
from website.views.bitcoin import (
BaconSubmissionView,
Expand Down Expand Up @@ -339,6 +340,7 @@
leave_team,
search_users,
)
from website.views.trademark import trademark_report_view
from website.views.user import (
CustomObtainAuthToken,
EachmonthLeaderboardView,
Expand Down Expand Up @@ -1251,6 +1253,11 @@
path("security/incidents/add/", SecurityIncidentCreateView.as_view(), name="security_incident_add"),
path("security/incidents/<int:pk>/", SecurityIncidentDetailView.as_view(), name="security_incident_detail"),
path("security/incidents/<int:pk>/edit/", SecurityIncidentUpdateView.as_view(), name="security_incident_edit"),
path("api/trademark-report/", trademark_report_view, name="trademark_report"),
path("api/trademark-report/", trademark_report_api, name="trademark_report_api"),
Comment on lines +1256 to +1257
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 | 🔴 Critical

Duplicate URL paths cause unreachable route.

Both lines define path("api/trademark-report/", ...) with different views. Django's URL resolver uses first-match-wins, so trademark_report_api on line 1257 will never be reached.

Looking at the view implementations in website/views/trademark.py and website/views/api/trademark.py, both trademark_report_view and trademark_report_api appear functionally identical (both return JSON trademark reports for a name query parameter).

Either:

  1. Remove one of the duplicate routes if they serve the same purpose
  2. Use distinct paths if both endpoints are needed (e.g., api/v1/trademark-report/ vs api/v2/trademark-report/)
🔎 Suggested fix: Remove the duplicate route
     path("api/trademark-report/", trademark_report_view, name="trademark_report"),
-    path("api/trademark-report/", trademark_report_api, name="trademark_report_api"),
     path(
         "api/organization/<int:org_id>/trademarks/", organization_trademark_check, name="organization_trademark_check"
     ),
📝 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
path("api/trademark-report/", trademark_report_view, name="trademark_report"),
path("api/trademark-report/", trademark_report_api, name="trademark_report_api"),
path("api/trademark-report/", trademark_report_view, name="trademark_report"),
🤖 Prompt for AI Agents
In blt/urls.py around lines 1256-1257 there are duplicate routes both using
path("api/trademark-report/", ...) which makes the second route unreachable;
remove the duplicate by either deleting the redundant entry (keep the correct
view) or, if both handlers are required, change one route to a distinct path
(e.g., "api/v2/trademark-report/" or "api/trademark-report/api/") and update any
callers/tests accordingly so each view has a unique URL.

path(
"api/organization/<int:org_id>/trademarks/", organization_trademark_check, name="organization_trademark_check"
),
]

if settings.DEBUG:
Expand Down
144 changes: 144 additions & 0 deletions website/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
Thread,
TimeLog,
Trademark,
TrademarkMatch,
TrademarkOwner,
Transaction,
UserAdventureProgress,
Expand All @@ -101,6 +102,149 @@
)


@admin.register(TrademarkMatch)
class TrademarkMatchAdmin(admin.ModelAdmin):
"""Admin interface for trademark matches."""

list_display = [
"checked_name",
"matched_trademark_name",
"risk_level_badge",
"similarity_score_display",
"status_badge",
"checked_at",
"is_reviewed",
]

list_filter = [
"risk_level",
"status",
"is_reviewed",
"checked_at",
]

search_fields = [
"checked_name",
"matched_trademark_name",
"organization__name",
"website__name",
]
Comment on lines +126 to +131
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 | 🟠 Major

search_fields references non-existent website field.

The TrademarkMatch model does not have a website field (it's explicitly commented out in the model: "Remove the website field entirely for now"). This will cause a FieldError when searching in the admin.

🔎 Proposed fix
     search_fields = [
         "checked_name",
         "matched_trademark_name",
         "organization__name",
-        "website__name",
     ]
📝 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
search_fields = [
"checked_name",
"matched_trademark_name",
"organization__name",
"website__name",
]
search_fields = [
"checked_name",
"matched_trademark_name",
"organization__name",
]
🤖 Prompt for AI Agents
In website/admin.py around lines 126 to 131, the admin search_fields includes
"website__name" which no longer exists on the TrademarkMatch model and will
raise a FieldError; remove "website__name" from the search_fields list (or
replace it with the correct existing relation/field if there is an alternative),
ensure the list only references valid model fields or related lookups, and run
the Django dev server/admin to verify the FieldError is resolved.


readonly_fields = [
"checked_name",
"matched_trademark_name",
"similarity_score",
"checked_at",
"verified_at",
"is_high_risk",
"is_medium_risk",
]

fieldsets = (
(
"Match Information",
{
"fields": (
"checked_name",
"matched_trademark_name",
"similarity_score",
"risk_level",
)
},
),
(
"Related Objects",
{
"fields": ("organization", "website"),
},
),
Comment on lines +155 to +160
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 | 🟠 Major

Fieldset references non-existent website field.

The TrademarkMatch model has no website field, so including it in the fieldset will cause an admin error when viewing/editing records.

🔎 Proposed fix
         (
             "Related Objects",
             {
-                "fields": ("organization", "website"),
+                "fields": ("organization",),
             },
         ),
🤖 Prompt for AI Agents
In website/admin.py around lines 155 to 160, the fieldset includes a
non-existent "website" field for the TrademarkMatch model which will raise an
admin error; remove "website" from the ("organization", "website") tuple or
replace it with the actual field name defined on TrademarkMatch (e.g., "site" or
"url") if intended—update the fieldset to only reference valid model fields and
run the Django admin to verify no errors.

(
"Status & Review",
{
"fields": ("status", "is_reviewed", "notes"),
},
),
(
"Timestamps",
{
"fields": ("checked_at", "verified_at"),
"classes": ("collapse",),
},
),
)

actions = [
"mark_as_reviewed",
"mark_as_false_positive",
"mark_as_acknowledged",
]

def risk_level_badge(self, obj):
"""Display risk level as a colored badge."""
colors = {
"low": "#28a745",
"medium": "#ffc107",
"high": "#dc3545",
}
color = colors.get(obj.risk_level, "#6c757d")
return format_html(
'<span style="background-color: {}; color: white; padding: 3px 8px; '
'border-radius: 3px; font-weight: bold;">{}</span>',
color,
obj.get_risk_level_display(),
)

risk_level_badge.short_description = "Risk Level"

def similarity_score_display(self, obj):
"""Display similarity score with color coding."""
color = "red" if obj.similarity_score >= 90 else "orange" if obj.similarity_score >= 80 else "green"
return format_html('<span style="color: {}; font-weight: bold;">{:.1f}%</span>', color, obj.similarity_score)

similarity_score_display.short_description = "Score"
similarity_score_display.admin_order_field = "similarity_score"

def status_badge(self, obj):
"""Display status as a badge."""
colors = {
"pending": "#007bff",
"false_positive": "#6c757d",
"acknowledged": "#28a745",
"action_taken": "#17a2b8",
}
color = colors.get(obj.status, "#6c757d")
return format_html(
'<span style="background-color: {}; color: white; padding: 3px 8px; '
'border-radius: 3px; font-weight: bold;">{}</span>',
color,
obj.get_status_display(),
)

status_badge.short_description = "Status"
status_badge.admin_order_field = "status"

def mark_as_reviewed(self, request, queryset):
"""Mark selected matches as reviewed."""
updated = queryset.update(is_reviewed=True)
self.message_user(request, f"{updated} trademark match(es) marked as reviewed.")

mark_as_reviewed.short_description = "Mark selected as reviewed"

def mark_as_false_positive(self, request, queryset):
"""Mark selected matches as false positives."""
updated = queryset.update(status="false_positive", is_reviewed=True)
self.message_user(request, f"{updated} trademark match(es) marked as false positive.")

mark_as_false_positive.short_description = "Mark as false positive"

def mark_as_acknowledged(self, request, queryset):
"""Mark selected matches as acknowledged."""
updated = queryset.update(status="acknowledged", is_reviewed=True)
self.message_user(request, f"{updated} trademark match(es) marked as acknowledged.")

mark_as_acknowledged.short_description = "Mark as acknowledged"


class UserResource(resources.ModelResource):
class Meta:
model = User
Expand Down
2 changes: 2 additions & 0 deletions website/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

class WebsiteConfig(AppConfig):
name = "website"
default_auto_field = "django.db.models.BigAutoField"

def ready(self):
import website.challenge_signals # noqa
import website.feed_signals # noqa
import website.signals # noqa
import website.social_signals # noqa
71 changes: 71 additions & 0 deletions website/management/commands/check_all_trademarks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""
Management command to perform trademark checks on all organizations and websites.
Usage: python manage.py check_all_trademarks
"""

from django.core.management.base import BaseCommand

from website.models import Organization
from website.signals import _perform_trademark_check


class Command(BaseCommand):
"""Management command to bulk check trademarks."""

help = "Perform trademark checks on all organizations and websites"

def add_arguments(self, parser):
"""Add command arguments."""
parser.add_argument(
"--orgs-only",
action="store_true",
help="Check only organizations",
)
parser.add_argument(
"--websites-only",
action="store_true",
help="Check only websites",
)

def handle(self, *args, **options):
"""Execute the command."""
self.stdout.write(self.style.SUCCESS("🔍 Starting bulk trademark checks..."))

orgs_only = options["orgs_only"]
websites_only = options["websites_only"]

org_count = 0
website_count = 0

# Check organizations
if not websites_only:
orgs = Organization.objects.filter(name__isnull=False).exclude(name="")
for org in orgs:
try:
_perform_trademark_check(name=org.name, organization=org)
org_count += 1
self.stdout.write(f" ✓ {org.name}")
except Exception as e:
self.stdout.write(self.style.ERROR(f" ✗ {org.name}: {str(e)}"))

# Check websites
if not orgs_only:
websites = Website.objects.filter(name__isnull=False).exclude(name="")
for website in websites:
try:
org = None
if hasattr(website, "organization"):
org = website.organization
_perform_trademark_check(name=website.name, website=website, organization=org)
website_count += 1
self.stdout.write(f" ✓ {website.name}")
except Exception as e:
self.stdout.write(self.style.ERROR(f" ✗ {website.name}: {str(e)}"))
Comment on lines +53 to +63
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 | 🔴 Critical

Missing import and invalid function parameter cause runtime errors.

Two issues will crash the command when processing websites:

  1. Line 53: Website is referenced but never imported - this will raise a NameError.

  2. Line 59: _perform_trademark_check() is called with website=website, but according to website/signals.py (lines 54-84), the function signature only accepts name and organization parameters.

🔎 Proposed fix
 from django.core.management.base import BaseCommand

-from website.models import Organization
+from website.models import Organization, Website
 from website.signals import _perform_trademark_check

And fix the function call (assuming website parameter support is added to _perform_trademark_check, or remove the website parameter):

-                    _perform_trademark_check(name=website.name, website=website, organization=org)
+                    _perform_trademark_check(name=website.name, organization=org)

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

🤖 Prompt for AI Agents
In website/management/commands/check_all_trademarks.py around lines 53 to 63,
the command refers to Website but never imports it and calls
_perform_trademark_check with an unexpected website= parameter; fix by adding
the proper import (from website.models import Website or the correct module
path) at the top of the file, and update the call to match the function
signature used in website/signals.py (call
_perform_trademark_check(name=website.name, organization=org) — remove the
website= argument — unless you deliberately change the trademark function to
accept a website parameter, in which case update that function signature and its
callers consistently).


self.stdout.write(
self.style.SUCCESS(
f"\n✅ Bulk check complete!\n"
f" Organizations checked: {org_count}\n"
f" Websites checked: {website_count}"
)
)
61 changes: 61 additions & 0 deletions website/management/commands/trademark_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""
Django management command to demonstrate trademark matching.
Usage: python manage.py trademark_demo
"""

from django.core.management.base import BaseCommand

from website.services.trademark_matching import get_trademark_matches


class Command(BaseCommand):
"""Management command for trademark matching demo."""

help = "Demo trademark matching functionality for issue #1926"

def add_arguments(self, parser):
"""Add command arguments."""
parser.add_argument(
"--company",
type=str,
default="BugHeist",
help="Company name to search for (default: BugHeist)",
)
parser.add_argument(
"--threshold",
type=float,
default=85.0,
help="Match threshold 0-100 (default: 85.0)",
)
Comment on lines +24 to +29
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 | 🟡 Minor

Unused --threshold argument.

The --threshold argument is defined but never used. get_trademark_matches() internally creates a TrademarkMatcher(threshold=85.0) with a hardcoded threshold.

Either remove the unused argument or pass it to a custom matcher:

🔎 Option 1: Remove unused argument
     def add_arguments(self, parser):
         """Add command arguments."""
         parser.add_argument(
             "--company",
             type=str,
             default="BugHeist",
             help="Company name to search for (default: BugHeist)",
         )
-        parser.add_argument(
-            "--threshold",
-            type=float,
-            default=85.0,
-            help="Match threshold 0-100 (default: 85.0)",
-        )
🔎 Option 2: Use the threshold argument
+from website.services.trademark_matching import TrademarkMatcher, SAMPLE_TRADEMARKS
-from website.services.trademark_matching import get_trademark_matches

     def handle(self, *args, **options):
         """Execute the command."""
         company = options["company"]
+        threshold = options["threshold"]

         self.stdout.write(self.style.SUCCESS(f"\n🔍 Searching for trademark matches for: '{company}'"))
         self.stdout.write("=" * 60)

-        matches = get_trademark_matches(company)
+        matcher = TrademarkMatcher(threshold=threshold)
+        matches = matcher.match(company, SAMPLE_TRADEMARKS)
📝 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
parser.add_argument(
"--threshold",
type=float,
default=85.0,
help="Match threshold 0-100 (default: 85.0)",
)
parser.add_argument(
"--company",
type=str,
default="BugHeist",
help="Company name to search for (default: BugHeist)",
)
🤖 Prompt for AI Agents
In website/management/commands/trademark_demo.py around lines 24-29, the
--threshold argparse option is defined but never used; either remove the
argument or thread its value into the matcher creation. Preferred fix: add a
parameter to get_trademark_matches (or accept threshold in the call site) and
pass args.threshold through so TrademarkMatcher is constructed with
threshold=args.threshold instead of the hardcoded 85.0; alternatively, if
threshold is not needed, delete the parser.add_argument block and any references
to args.threshold.


def handle(self, *args, **options):
"""Execute the command."""
company = options["company"]

self.stdout.write(self.style.SUCCESS(f"\n🔍 Searching for trademark matches for: '{company}'"))
self.stdout.write("=" * 60)

matches = get_trademark_matches(company)

if not matches:
self.stdout.write(self.style.WARNING(f"❌ No matches found for '{company}'"))
else:
self.stdout.write(self.style.SUCCESS(f"✅ Found {len(matches)} potential match(es):\n"))
for i, match in enumerate(matches, 1):
score_color = (
self.style.SUCCESS
if match.score >= 95
else self.style.WARNING
if match.score >= 85
else self.style.ERROR
)
self.stdout.write(f" {i}. {match.name:<30} " f"Score: {score_color(f'{match.score}%')}")

self.stdout.write("\n" + "=" * 60)
self.stdout.write(
self.style.HTTP_INFO(
"💡 Tip: To check a different company, use:\n"
" python manage.py trademark_demo --company 'YourCompanyName'"
)
)
self.stdout.write("")
Loading
Loading