-
-
Notifications
You must be signed in to change notification settings - Fork 325
feat: complete trademark matching for #1926 #5358
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,3 +25,4 @@ google-credentials.json | |
|
|
||
|
|
||
| .env | ||
| .DS_Store | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -88,6 +88,7 @@ | |||||||||||||||||||||||
| Thread, | ||||||||||||||||||||||||
| TimeLog, | ||||||||||||||||||||||||
| Trademark, | ||||||||||||||||||||||||
| TrademarkMatch, | ||||||||||||||||||||||||
| TrademarkOwner, | ||||||||||||||||||||||||
| Transaction, | ||||||||||||||||||||||||
| UserAdventureProgress, | ||||||||||||||||||||||||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The 🔎 Proposed fix search_fields = [
"checked_name",
"matched_trademark_name",
"organization__name",
- "website__name",
]📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fieldset references non-existent The 🔎 Proposed fix (
"Related Objects",
{
- "fields": ("organization", "website"),
+ "fields": ("organization",),
},
),🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| ( | ||||||||||||||||||||||||
| "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 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing import and invalid function parameter cause runtime errors. Two issues will crash the command when processing websites:
🔎 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_checkAnd fix the function call (assuming - _perform_trademark_check(name=website.name, website=website, organization=org)
+ _perform_trademark_check(name=website.name, organization=org)
🤖 Prompt for AI Agents |
||
|
|
||
| self.stdout.write( | ||
| self.style.SUCCESS( | ||
| f"\n✅ Bulk check complete!\n" | ||
| f" Organizations checked: {org_count}\n" | ||
| f" Websites checked: {website_count}" | ||
| ) | ||
| ) | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unused The 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| 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("") | ||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate URL paths cause unreachable route.
Both lines define
path("api/trademark-report/", ...)with different views. Django's URL resolver uses first-match-wins, sotrademark_report_apion line 1257 will never be reached.Looking at the view implementations in
website/views/trademark.pyandwebsite/views/api/trademark.py, bothtrademark_report_viewandtrademark_report_apiappear functionally identical (both return JSON trademark reports for anamequery parameter).Either:
api/v1/trademark-report/vsapi/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
🤖 Prompt for AI Agents