Coverage for src / lexigram / admin / controllers / form_validation.py: 0%
71 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-11 17:07 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-11 17:07 +0800
1"""Form validation and autocomplete controller for real-time features."""
3from __future__ import annotations
5import asyncio
6from typing import Any
8from starlette.responses import HTMLResponse, JSONResponse
10from lexigram.admin.controllers.base import AdminController
11from lexigram.contracts.web import get, post
12from lexigram.di.decorators import inject
15@inject
16class FormValidationController(AdminController):
17 """Controller for real-time form validation and autocomplete."""
19 def __init__(self, renderer: Any) -> None:
20 super().__init__(renderer)
22 @post("/api/forms/validate/{field_name}")
23 async def validate_field(self, field_name: str, request) -> Any:
24 """Validate a single field in real-time."""
25 try:
26 # Get form data from request
27 if request.headers.get("content-type") == "application/json":
28 data = await request.json()
29 else:
30 form_data = await request.form()
31 data = dict(form_data)
33 field_value = data.get(field_name)
35 # For now, return success - in a real implementation,
36 # you'd look up the field from a form registry and validate it
37 # This is a placeholder for the real-time validation endpoint
39 # Simulate async validation delay
40 if field_name == "email":
41 await asyncio.sleep(0.1) # Simulate API call
42 if field_value and "@" not in field_value:
43 return HTMLResponse(
44 content='<span class="text-destructive text-sm">Invalid email address</span>',
45 status_code=200,
46 )
48 return HTMLResponse(content="", status_code=200) # Clear any previous error
50 except Exception as e: # noqa: BLE001 — controller boundary; all errors become HTTP 400 responses
51 from lexigram.logging import get_logger
53 logger = get_logger(__name__)
54 logger.exception("Field validation error for %s", field_name)
55 return HTMLResponse(
56 content=f'<span class="text-destructive text-sm">Validation error: {e!s}</span>',
57 status_code=400,
58 )
60 @get("/api/forms/autocomplete/{field_name}")
61 async def autocomplete_field(self, field_name: str, request) -> Any:
62 """Provide autocomplete suggestions for a field."""
63 try:
64 query = request.query_params.get("q", "")
66 # Simulate autocomplete data based on field name
67 suggestions = []
69 if field_name == "country":
70 countries = [
71 "United States",
72 "United Kingdom",
73 "Canada",
74 "Australia",
75 "Germany",
76 "France",
77 ]
78 suggestions = list(
79 filter(lambda c: query.lower() in c.lower(), countries),
80 )
81 elif field_name == "city":
82 cities = ["New York", "London", "Toronto", "Sydney", "Berlin", "Paris"]
83 suggestions = list(filter(lambda c: query.lower() in c.lower(), cities))
84 elif field_name == "tags":
85 tags = [
86 "urgent",
87 "important",
88 "review",
89 "draft",
90 "published",
91 "archived",
92 ]
93 suggestions = list(filter(lambda t: query.lower() in t.lower(), tags))
95 # Return HTML for HTMX to swap in
96 if suggestions:
97 items_html = "".join(
98 [
99 f'<div class="px-3 py-2 hover:bg-muted cursor-pointer" '
100 f"onclick=\"selectAutocomplete('{field_name}', '{suggestion}')\">{suggestion}</div>"
101 for suggestion in suggestions[:5] # Limit to 5 suggestions
102 ],
103 )
104 return HTMLResponse(
105 content=f'<div class="absolute z-10 bg-card border border-border rounded-md shadow-lg max-h-40 overflow-y-auto">{items_html}</div>',
106 status_code=200,
107 )
108 return HTMLResponse(content="", status_code=200)
110 except Exception as e: # noqa: BLE001 — controller boundary; all errors become HTTP 400 responses
111 from lexigram.logging import get_logger
113 logger = get_logger(__name__)
114 logger.exception("Autocomplete error for %s", field_name)
115 return HTMLResponse(
116 content=f'<div class="text-destructive text-sm">Autocomplete error: {e!s}</div>',
117 status_code=400,
118 )
120 @post("/api/forms/async-validate/{field_name}")
121 async def async_validate_field(self, field_name: str, request) -> Any:
122 """Handle async validation for fields like username availability."""
123 try:
124 if request.headers.get("content-type") == "application/json":
125 data = await request.json()
126 else:
127 form_data = await request.form()
128 data = dict(form_data)
130 field_value = data.get(field_name)
132 # Simulate async validation (e.g., checking username availability)
133 await asyncio.sleep(0.5) # Simulate API/database call
135 if field_name == "username":
136 # Simulate checking if username is taken
137 taken_usernames = ["admin", "root", "user", "test"]
138 if field_value in taken_usernames:
139 return JSONResponse(
140 {"valid": False, "message": "Username is already taken"},
141 )
143 return JSONResponse({"valid": True, "message": "Available"})
145 except Exception as e: # noqa: BLE001 — controller boundary; all errors become HTTP 400 responses
146 from lexigram.logging import get_logger
148 logger = get_logger(__name__)
149 logger.exception("Async validation error for %s", field_name)
150 return JSONResponse(
151 {"valid": False, "message": f"Validation failed: {e!s}"},
152 status_code=400,
153 )