Based on the git diff provided, here's a concise and descriptive commit message:

feat: add passkey authentication and enhance user preferences

- Add passkey login security event type with fingerprint icon
- Include request and site context in email confirmation for backend
- Add user_id exact match filter to prevent incorrect user lookups
- Enable PATCH method for updating user preferences via API
- Add moderation_preferences support to user settings
- Optimize ticket queries with select_related and prefetch_related

This commit introduces passkey authentication tracking, improves user
profile filtering accuracy, and extends the preferences API to support
updates. Query optimizations reduce database hits for ticket listings.
This commit is contained in:
pacnpal
2026-01-12 19:13:05 -05:00
parent 2b66814d82
commit d631f3183c
56 changed files with 5860 additions and 264 deletions

174
scripts/lint_choices.py Executable file
View File

@@ -0,0 +1,174 @@
#!/usr/bin/env python
"""
RichChoiceField Enforcement Lint Script
This script checks for prohibited choice patterns in the codebase.
Exit code 0 = no violations found, exit code 1 = violations found.
Usage:
python scripts/lint_choices.py
python scripts/lint_choices.py --fix # Show fix suggestions
Add to CI:
python scripts/lint_choices.py || exit 1
"""
import argparse
import re
import sys
from pathlib import Path
# Patterns to detect prohibited choice usage
PROHIBITED_PATTERNS = [
# TextChoices / IntegerChoices class definitions
(r'class\s+\w+\s*\(\s*models\.(TextChoices|IntegerChoices)\s*\)',
'models.TextChoices/IntegerChoices class definition'),
# Inline tuple choices in CharField
(r'choices\s*=\s*\[\s*\(\s*["\']',
'Inline tuple choices'),
# Direct reference to .choices attribute
(r'choices\s*=\s*\w+\.choices',
'Reference to inner TextChoices.choices'),
]
# Directories/files to exclude
EXCLUDE_PATTERNS = [
'*/migrations/*',
'*/.venv/*',
'*/node_modules/*',
'*/__pycache__/*',
'*.pyc',
'lint_choices.py', # Exclude this script
]
# Files allowed to define TextChoices (infrastructure only)
ALLOWED_EXCEPTION_FILES = [
# Core choice infrastructure files can reference these patterns
'apps/core/choices/',
]
def should_exclude(path: Path) -> bool:
"""Check if path should be excluded from linting."""
path_str = str(path)
for pattern in EXCLUDE_PATTERNS:
if pattern.startswith('*/'):
if pattern[2:].rstrip('/*') in path_str:
return True
elif pattern.endswith('/*'):
if path_str.startswith(pattern[:-2]):
return True
elif pattern in path_str:
return True
return False
def is_exception_file(path: Path) -> bool:
"""Check if file is an allowed exception."""
path_str = str(path)
for exception in ALLOWED_EXCEPTION_FILES:
if exception in path_str:
return True
return False
def scan_file(filepath: Path) -> list[tuple[int, str, str]]:
"""Scan a file for prohibited patterns. Returns list of (line_num, line, pattern_name)."""
violations = []
if should_exclude(filepath) or is_exception_file(filepath):
return violations
try:
content = filepath.read_text(encoding='utf-8')
lines = content.split('\n')
for i, line in enumerate(lines, 1):
# Skip comments
stripped = line.strip()
if stripped.startswith('#'):
continue
for pattern, description in PROHIBITED_PATTERNS:
if re.search(pattern, line):
violations.append((i, line.strip(), description))
break # Only report one violation per line
except Exception as e:
print(f"Warning: Could not read {filepath}: {e}", file=sys.stderr)
return violations
def scan_directory(root_dir: Path) -> dict[Path, list]:
"""Scan all Python files in directory for violations."""
all_violations = {}
for filepath in root_dir.rglob('*.py'):
violations = scan_file(filepath)
if violations:
all_violations[filepath] = violations
return all_violations
def print_violations(violations: dict, show_fix: bool = False):
"""Print violations in a readable format."""
total = 0
for filepath, file_violations in sorted(violations.items()):
print(f"\n\033[1;31m{filepath}\033[0m")
for line_num, line_content, description in file_violations:
print(f" Line {line_num}: [{description}]")
print(f" {line_content[:80]}{'...' if len(line_content) > 80 else ''}")
total += 1
if show_fix:
print(f" \033[1;33mFix:\033[0m Use RichChoiceField(choice_group='...', domain='...')")
return total
def main():
parser = argparse.ArgumentParser(description='Lint for prohibited choice patterns')
parser.add_argument('--fix', action='store_true', help='Show fix suggestions')
parser.add_argument('path', nargs='?', default='apps', help='Path to scan (default: apps)')
args = parser.parse_args()
# Find backend directory
script_dir = Path(__file__).parent
backend_dir = script_dir.parent
if (backend_dir / 'apps').exists():
scan_path = backend_dir / args.path
elif (backend_dir / 'backend' / 'apps').exists():
scan_path = backend_dir / 'backend' / args.path
else:
print("Error: Could not find apps directory", file=sys.stderr)
sys.exit(1)
print(f"Scanning {scan_path} for prohibited choice patterns...")
print("=" * 60)
violations = scan_directory(scan_path)
if violations:
total = print_violations(violations, show_fix=args.fix)
print("\n" + "=" * 60)
print(f"\033[1;31mFound {total} violation(s) in {len(violations)} file(s)\033[0m")
print("\nProhibited patterns:")
print(" - models.TextChoices / models.IntegerChoices classes")
print(" - Inline choices=[(value, label), ...]")
print(" - References to InnerClass.choices")
print("\nRequired pattern:")
print(" RichChoiceField(choice_group='group_name', domain='domain_name')")
print("\nSee: .agent/workflows/enforce-richchoice.md for migration guide")
sys.exit(1)
else:
print("\n\033[1;32m✓ No prohibited choice patterns found!\033[0m")
sys.exit(0)
if __name__ == '__main__':
main()