Source code for imednet.form_designer.client
import json
import httpx
from .models import Layout
[docs]class FormDesignerClient:
"""
Client for the iMedNet Form Designer endpoint.
Handles the specific authentication and payload requirements of the legacy
formdez_save.php endpoint.
"""
def __init__(self, base_url: str, phpsessid: str, timeout: float = 30.0):
"""
Initialize the client.
Args:
base_url: The base URL of the iMedNet instance (e.g., https://xyz.imednet.com).
phpsessid: The active PHP session ID from the browser.
timeout: Request timeout in seconds.
"""
self.base_url = base_url.rstrip("/")
self.phpsessid = phpsessid
self.timeout = timeout
self.session = httpx.Client(timeout=timeout)
[docs] def save_form(
self,
csrf_key: str,
form_id: int,
community_id: int,
revision: int,
layout: Layout,
) -> str:
"""
Submit the form layout to the server.
Args:
csrf_key: The CSRF token (scraped from page).
form_id: The ID of the form being edited.
community_id: The study ID.
revision: The NEXT revision number.
layout: The Form Layout object.
Returns:
The raw response text from the server.
Raises:
httpx.HTTPStatusError: If the server returns a non-2xx status code.
ValueError: If the server returns an error.
"""
# --- Validation Logic Migrated from TUI ---
if not csrf_key or not csrf_key.strip():
raise ValueError("CSRF Key cannot be empty.")
if form_id <= 0:
raise ValueError(f"Invalid form_id: {form_id}. Must be a positive integer.")
if community_id <= 0:
raise ValueError(f"Invalid community_id: {community_id}. Must be a positive integer.")
if revision < 0:
raise ValueError(f"Invalid revision: {revision}. Must be non-negative.")
# ------------------------------------------
url = f"{self.base_url}/app/formdez/formdez_save.php"
# Critical Headers
headers = {
"Cookie": f"PHPSESSID={self.phpsessid}",
"X-Requested-With": "XMLHttpRequest",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"User-Agent": "iMedNet-SDK-FormBuilder/1.0",
}
# Serialize Layout
# mode='json' ensures we get a JSON string compatible output
# by_alias=True might be needed if we defined aliases,
# but we used direct names matching schema
layout_json = layout.model_dump_json(exclude_none=True)
# Construct Payload
# Note: We use a dict and let requests url-encode it
payload = {
"CSRFKey": csrf_key,
"form_id": str(form_id),
"community_id": str(community_id),
"revision": str(revision),
"layout": layout_json,
"resubmit": "0",
"quick_save": "1",
"__internal_ajax_request": "1",
}
response = self.session.post(url, data=payload, headers=headers)
response.raise_for_status()
# Check for application-level errors (often returned as 200 OK but with error text)
# However, the requirement says "Signals the backend to return a JSON response"
# So we should try to parse it.
try:
resp_data = response.json()
# If it's JSON, it usually contains status info.
# Example success: {"success": true, ...}
# Example error: {"error": "..."}
if isinstance(resp_data, dict) and resp_data.get("error"):
raise ValueError(f"Server Error: {resp_data['error']}")
except json.JSONDecodeError:
# Fallback if not JSON (legacy endpoints sometimes return HTML on error)
pass
return response.text