fix: correct tojson|safe usage in templates and update validator

- Remove |safe from |tojson in HTML attributes (x-data) - quotes must
  become " for browsers to parse correctly
- Update LANG-002 and LANG-003 architecture rules to document correct
  |tojson usage patterns:
  - HTML attributes: |tojson (no |safe)
  - Script blocks: |tojson|safe
- Fix validator to warn when |tojson|safe is used in x-data (breaks
  HTML attribute parsing)
- Improve code quality across services, APIs, and tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-13 22:59:51 +01:00
parent 94d268f330
commit 9920430b9e
123 changed files with 1408 additions and 840 deletions

View File

@@ -151,9 +151,7 @@ router.include_router(
)
# Include test runner endpoints
router.include_router(
tests.router, prefix="/tests", tags=["admin-tests"]
)
router.include_router(tests.router, prefix="/tests", tags=["admin-tests"])
# Export the router
__all__ = ["router"]

View File

@@ -63,7 +63,9 @@ def _convert_import_to_response(job) -> BackgroundTaskResponse:
started_at=job.started_at.isoformat() if job.started_at else None,
completed_at=job.completed_at.isoformat() if job.completed_at else None,
duration_seconds=duration,
description=f"Import from {job.marketplace}: {job.source_url[:50]}..." if len(job.source_url) > 50 else f"Import from {job.marketplace}: {job.source_url}",
description=f"Import from {job.marketplace}: {job.source_url[:50]}..."
if len(job.source_url) > 50
else f"Import from {job.marketplace}: {job.source_url}",
triggered_by=job.user.username if job.user else None,
error_message=job.error_message,
details={
@@ -108,7 +110,9 @@ def _convert_test_run_to_response(run) -> BackgroundTaskResponse:
@router.get("/tasks", response_model=list[BackgroundTaskResponse])
async def list_background_tasks(
status: str | None = Query(None, description="Filter by status"),
task_type: str | None = Query(None, description="Filter by type (import, test_run)"),
task_type: str | None = Query(
None, description="Filter by type (import, test_run)"
),
limit: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_api),
@@ -122,12 +126,16 @@ async def list_background_tasks(
# Get import jobs
if task_type is None or task_type == "import":
import_jobs = background_tasks_service.get_import_jobs(db, status=status, limit=limit)
import_jobs = background_tasks_service.get_import_jobs(
db, status=status, limit=limit
)
tasks.extend([_convert_import_to_response(job) for job in import_jobs])
# Get test runs
if task_type is None or task_type == "test_run":
test_runs = background_tasks_service.get_test_runs(db, status=status, limit=limit)
test_runs = background_tasks_service.get_test_runs(
db, status=status, limit=limit
)
tasks.extend([_convert_test_run_to_response(run) for run in test_runs])
# Sort by start time (most recent first)

View File

@@ -329,9 +329,7 @@ async def assign_violation(
"user_id": assignment.user_id,
"assigned_at": assignment.assigned_at.isoformat(),
"assigned_by": assignment.assigned_by,
"due_date": (
assignment.due_date.isoformat() if assignment.due_date else None
),
"due_date": (assignment.due_date.isoformat() if assignment.due_date else None),
"priority": assignment.priority,
}

View File

@@ -75,7 +75,7 @@ def create_company_with_owner(
owner_username=owner_user.username,
owner_email=owner_user.email,
temporary_password=temp_password or "N/A (Existing user)",
login_url=f"http://localhost:8000/admin/login",
login_url="http://localhost:8000/admin/login",
)

View File

@@ -46,9 +46,13 @@ def get_admin_dashboard(
users=UserStatsResponse(**user_stats),
vendors=VendorStatsResponse(
total=vendor_stats.get("total", vendor_stats.get("total_vendors", 0)),
verified=vendor_stats.get("verified", vendor_stats.get("verified_vendors", 0)),
verified=vendor_stats.get(
"verified", vendor_stats.get("verified_vendors", 0)
),
pending=vendor_stats.get("pending", vendor_stats.get("pending_vendors", 0)),
inactive=vendor_stats.get("inactive", vendor_stats.get("inactive_vendors", 0)),
inactive=vendor_stats.get(
"inactive", vendor_stats.get("inactive_vendors", 0)
),
),
recent_vendors=admin_service.get_recent_vendors(db, limit=5),
recent_imports=admin_service.get_recent_import_jobs(db, limit=10),
@@ -109,9 +113,13 @@ def get_platform_statistics(
users=UserStatsResponse(**user_stats),
vendors=VendorStatsResponse(
total=vendor_stats.get("total", vendor_stats.get("total_vendors", 0)),
verified=vendor_stats.get("verified", vendor_stats.get("verified_vendors", 0)),
verified=vendor_stats.get(
"verified", vendor_stats.get("verified_vendors", 0)
),
pending=vendor_stats.get("pending", vendor_stats.get("pending_vendors", 0)),
inactive=vendor_stats.get("inactive", vendor_stats.get("inactive_vendors", 0)),
inactive=vendor_stats.get(
"inactive", vendor_stats.get("inactive_vendors", 0)
),
),
products=ProductStatsResponse(**product_stats),
orders=OrderStatsBasicResponse(**order_stats),

View File

@@ -68,7 +68,9 @@ def get_credentials_service(db: Session) -> LetzshopCredentialsService:
def list_vendors_letzshop_status(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
configured_only: bool = Query(False, description="Only show vendors with Letzshop configured"),
configured_only: bool = Query(
False, description="Only show vendors with Letzshop configured"
),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
@@ -119,8 +121,9 @@ def get_vendor_credentials(
credentials = creds_service.get_credentials_or_raise(vendor_id)
except CredentialsNotFoundError:
raise ResourceNotFoundException(
"LetzshopCredentials", str(vendor_id),
message=f"Letzshop credentials not configured for vendor {vendor.name}"
"LetzshopCredentials",
str(vendor_id),
message=f"Letzshop credentials not configured for vendor {vendor.name}",
)
return LetzshopCredentialsResponse(
@@ -215,8 +218,9 @@ def update_vendor_credentials(
db.commit()
except CredentialsNotFoundError:
raise ResourceNotFoundException(
"LetzshopCredentials", str(vendor_id),
message=f"Letzshop credentials not configured for vendor {vendor.name}"
"LetzshopCredentials",
str(vendor_id),
message=f"Letzshop credentials not configured for vendor {vendor.name}",
)
return LetzshopCredentialsResponse(
@@ -255,8 +259,9 @@ def delete_vendor_credentials(
deleted = creds_service.delete_credentials(vendor_id)
if not deleted:
raise ResourceNotFoundException(
"LetzshopCredentials", str(vendor_id),
message=f"Letzshop credentials not configured for vendor {vendor.name}"
"LetzshopCredentials",
str(vendor_id),
message=f"Letzshop credentials not configured for vendor {vendor.name}",
)
db.commit()
@@ -445,7 +450,9 @@ def trigger_vendor_sync(
orders_imported += 1
except Exception as e:
errors.append(f"Error processing shipment {shipment.get('id')}: {e}")
errors.append(
f"Error processing shipment {shipment.get('id')}: {e}"
)
db.commit()

View File

@@ -12,7 +12,7 @@ Provides endpoints for:
import logging
from fastapi import APIRouter, Depends, Query, Response
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api
@@ -190,9 +190,10 @@ def download_log_file(
"""
from pathlib import Path
from app.core.config import settings
from fastapi.responses import FileResponse
from app.core.config import settings
# Determine log file path
log_file_path = settings.log_file
if log_file_path:

View File

@@ -149,7 +149,9 @@ class AdminProductDetail(BaseModel):
def get_products(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=500),
search: str | None = Query(None, description="Search by title, GTIN, SKU, or brand"),
search: str | None = Query(
None, description="Search by title, GTIN, SKU, or brand"
),
marketplace: str | None = Query(None, description="Filter by marketplace"),
vendor_name: str | None = Query(None, description="Filter by vendor name"),
availability: str | None = Query(None, description="Filter by availability"),

View File

@@ -79,9 +79,7 @@ def get_setting(
setting = admin_settings_service.get_setting_by_key(db, key)
if not setting:
raise ResourceNotFoundException(
resource_type="Setting", identifier=key
)
raise ResourceNotFoundException(resource_type="Setting", identifier=key)
return AdminSettingResponse.model_validate(setting)

View File

@@ -65,7 +65,9 @@ class RunTestsRequest(BaseModel):
"""Request model for running tests"""
test_path: str = Field("tests", description="Path to tests to run")
extra_args: list[str] | None = Field(None, description="Additional pytest arguments")
extra_args: list[str] | None = Field(
None, description="Additional pytest arguments"
)
class TestDashboardStatsResponse(BaseModel):
@@ -205,6 +207,7 @@ async def get_run(
if not run:
from app.exceptions.base import ResourceNotFoundException
raise ResourceNotFoundException("TestRun", str(run_id))
return TestRunResponse(
@@ -231,7 +234,9 @@ async def get_run(
@router.get("/runs/{run_id}/results", response_model=list[TestResultResponse])
async def get_run_results(
run_id: int,
outcome: str | None = Query(None, description="Filter by outcome (passed, failed, error, skipped)"),
outcome: str | None = Query(
None, description="Filter by outcome (passed, failed, error, skipped)"
),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_api),
):

View File

@@ -99,7 +99,9 @@ def create_user(
full_name=user.full_name,
is_email_verified=user.is_email_verified,
owned_companies_count=len(user.owned_companies) if user.owned_companies else 0,
vendor_memberships_count=len(user.vendor_memberships) if user.vendor_memberships else 0,
vendor_memberships_count=len(user.vendor_memberships)
if user.vendor_memberships
else 0,
)
@@ -151,7 +153,9 @@ def get_user_details(
full_name=user.full_name,
is_email_verified=user.is_email_verified,
owned_companies_count=len(user.owned_companies) if user.owned_companies else 0,
vendor_memberships_count=len(user.vendor_memberships) if user.vendor_memberships else 0,
vendor_memberships_count=len(user.vendor_memberships)
if user.vendor_memberships
else 0,
)
@@ -192,7 +196,9 @@ def update_user(
full_name=user.full_name,
is_email_verified=user.is_email_verified,
owned_companies_count=len(user.owned_companies) if user.owned_companies else 0,
vendor_memberships_count=len(user.vendor_memberships) if user.vendor_memberships else 0,
vendor_memberships_count=len(user.vendor_memberships)
if user.vendor_memberships
else 0,
)

View File

@@ -229,4 +229,6 @@ async def delete_vendor_theme(
# Global exception handler converts them to proper HTTP responses
result = vendor_theme_service.delete_theme(db, vendor_code)
db.commit()
return ThemeDeleteResponse(message=result.get("message", "Theme deleted successfully"))
return ThemeDeleteResponse(
message=result.get("message", "Theme deleted successfully")
)

View File

@@ -318,7 +318,9 @@ def delete_vendor(
@router.get("/{vendor_identifier}/export/letzshop")
def export_vendor_products_letzshop(
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
language: str = Query("en", description="Language for title/description (en, fr, de)"),
language: str = Query(
"en", description="Language for title/description (en, fr, de)"
),
include_inactive: bool = Query(False, description="Include inactive products"),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),