Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/formflow/routes/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ def preview_form(form_id: str):
# Entwurf automatisch speichern/aktualisieren
draft_id = request.form.get('draft_id', '')
if draft_id:
update_draft('drafts', draft_id, form_id, form_data)
update_draft('drafts', draft_id, form_id, form_data, form_def)
else:
draft_id = save_draft('drafts', form_id, form_data)
draft_id = save_draft('drafts', form_id, form_data, form_def)

# PDF generieren
file_id = uuid.uuid4().hex
Expand Down Expand Up @@ -160,9 +160,9 @@ def save_draft_route(form_id: str):
form_data = collect_form_data(form_def, request.form)
draft_id = request.form.get('draft_id', '')
if draft_id:
update_draft('drafts', draft_id, form_id, form_data)
update_draft('drafts', draft_id, form_id, form_data, form_def)
else:
save_draft('drafts', form_id, form_data)
save_draft('drafts', form_id, form_data, form_def)
return redirect(url_for('main.list_forms'))

@bp.route('/draft/<form_id>/<draft_id>/load', methods=['GET'])
Expand Down
52 changes: 34 additions & 18 deletions src/formflow/services/draft_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,39 @@ def collect_form_data(form_def: Dict[str, Any], request_form: MultiDict) -> Dict
return form_data


def save_draft(drafts_dir: str, form_id: str, form_data: dict) -> str:
def _build_draft_subtitle(form_def: Dict[str, Any], form_data: dict) -> str:
"""Berechnet den Draft-Subtitle aus Feldern mit in_draft_title: true."""
subtitle_parts = []
for field in form_def.get('fields', []):
if not field.get('in_draft_title'):
continue
value = form_data.get(field['name'])
if not value:
continue
if isinstance(value, list):
subtitle_parts.append(", ".join(value))
else:
subtitle_parts.append(value)
return ", ".join(subtitle_parts)


def save_draft(drafts_dir: str, form_id: str, form_data: dict, form_def: Dict[str, Any] = None) -> str:
"""Speichert einen neuen Entwurf als JSON und gibt die draft_id zurück."""
draft_id = uuid.uuid4().hex
return update_draft(drafts_dir, draft_id, form_id, form_data)
return update_draft(drafts_dir, draft_id, form_id, form_data, form_def)


def update_draft(drafts_dir: str, draft_id: str, form_id: str, form_data: dict) -> str:
def update_draft(drafts_dir: str, draft_id: str, form_id: str, form_data: dict, form_def: Dict[str, Any] = None) -> str:
"""Aktualisiert einen bestehenden Entwurf. Gibt die draft_id zurück."""
draft = {
draft: Dict[str, Any] = {
"draft_id": draft_id,
"form_id": form_id,
"saved_at": datetime.now(timezone.utc).isoformat(),
"form_data": form_data,
}
if form_def is not None:
draft["form_title"] = form_def.get('title', form_id)
draft["draft_subtitle"] = _build_draft_subtitle(form_def, form_data)
path = os.path.join(drafts_dir, f"draft_{draft_id}.json")
with open(path, 'w', encoding='utf-8') as f:
json.dump(draft, f, ensure_ascii=False)
Expand All @@ -65,23 +84,20 @@ def list_drafts(drafts_dir: str, forms: dict) -> list:
with open(path, 'r', encoding='utf-8') as f:
draft = json.load(f)
form_def = forms.get(draft.get('form_id'), {})
form_data = draft.get('form_data', {})
subtitle_parts = []
for field in form_def.get('fields', []):
if not field.get('in_draft_title'):
continue
value = form_data.get(field['name'])
if not value:
continue
if isinstance(value, list):
subtitle_parts.append(", ".join(value))
else:
subtitle_parts.append(value)
# Use persisted values when available; fall back to runtime calculation for old drafts
if 'draft_subtitle' in draft:
draft_subtitle = draft['draft_subtitle']
else:
draft_subtitle = _build_draft_subtitle(form_def, draft.get('form_data', {}))
if 'form_title' in draft:
form_title = draft['form_title']
else:
form_title = form_def.get('title', draft.get('form_id', ''))
drafts.append({
"draft_id": draft.get('draft_id'),
"form_id": draft.get('form_id'),
"form_title": form_def.get('title', draft.get('form_id', '')),
"draft_subtitle": ", ".join(subtitle_parts),
"form_title": form_title,
"draft_subtitle": draft_subtitle,
"saved_at": draft.get('saved_at'),
})
except (OSError, json.JSONDecodeError) as e:
Expand Down
126 changes: 125 additions & 1 deletion tests/test_draft_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytest
from werkzeug.datastructures import MultiDict

from formflow.services.draft_service import collect_form_data, save_draft, load_draft, list_drafts, delete_draft, update_draft
from formflow.services.draft_service import collect_form_data, save_draft, load_draft, list_drafts, delete_draft, update_draft, _build_draft_subtitle


@pytest.fixture
Expand Down Expand Up @@ -351,3 +351,127 @@ def test_update_draft_creates_file_if_not_exists(drafts_dir):
data = json.load(f)
assert data["draft_id"] == "brand_new_id"
assert data["form_data"]["key"] == "value"


# ---------------------------------------------------------------------------
# _build_draft_subtitle
# ---------------------------------------------------------------------------

def test_build_draft_subtitle_returns_joined_values():
"""_build_draft_subtitle joins in_draft_title field values with ', '."""
form_def = {
"fields": [
{"name": "user", "in_draft_title": True},
{"name": "notebook", "in_draft_title": True},
{"name": "service_tag"},
]
}
result = _build_draft_subtitle(form_def, {"user": "Max", "notebook": "ThinkPad", "service_tag": "X1"})
assert result == "Max, ThinkPad"


def test_build_draft_subtitle_skips_empty_values():
"""_build_draft_subtitle skips fields whose value is empty or missing."""
form_def = {"fields": [{"name": "a", "in_draft_title": True}, {"name": "b", "in_draft_title": True}]}
assert _build_draft_subtitle(form_def, {"a": "Hello", "b": ""}) == "Hello"


def test_build_draft_subtitle_handles_list_values():
"""_build_draft_subtitle joins list values with ', '."""
form_def = {"fields": [{"name": "items", "in_draft_title": True}]}
assert _build_draft_subtitle(form_def, {"items": ["A", "B"]}) == "A, B"


def test_build_draft_subtitle_empty_when_no_in_draft_title_fields():
"""_build_draft_subtitle returns '' when no field has in_draft_title: true."""
form_def = {"fields": [{"name": "x"}, {"name": "y"}]}
assert _build_draft_subtitle(form_def, {"x": "foo", "y": "bar"}) == ""


# ---------------------------------------------------------------------------
# draft_subtitle / form_title persisted in JSON
# ---------------------------------------------------------------------------

def test_save_draft_persists_draft_subtitle(drafts_dir):
"""save_draft stores draft_subtitle in the JSON when form_def is provided."""
form_def = {
"title": "My Form",
"fields": [{"name": "user", "in_draft_title": True}],
}
draft_id = save_draft(drafts_dir, "my_form", {"user": "Erika"}, form_def)

path = os.path.join(drafts_dir, f"draft_{draft_id}.json")
with open(path, encoding="utf-8") as f:
data = json.load(f)

assert data["draft_subtitle"] == "Erika"
assert data["form_title"] == "My Form"


def test_save_draft_without_form_def_omits_subtitle(drafts_dir):
"""save_draft does not add draft_subtitle when form_def is not provided."""
draft_id = save_draft(drafts_dir, "my_form", {"user": "Erika"})

path = os.path.join(drafts_dir, f"draft_{draft_id}.json")
with open(path, encoding="utf-8") as f:
data = json.load(f)

assert "draft_subtitle" not in data
assert "form_title" not in data


def test_update_draft_persists_draft_subtitle(drafts_dir):
"""update_draft stores draft_subtitle in the JSON when form_def is provided."""
form_def = {
"title": "Updated Form",
"fields": [{"name": "device", "in_draft_title": True}],
}
draft_id = save_draft(drafts_dir, "my_form", {"device": "old"})
update_draft(drafts_dir, draft_id, "my_form", {"device": "ThinkPad"}, form_def)

path = os.path.join(drafts_dir, f"draft_{draft_id}.json")
with open(path, encoding="utf-8") as f:
data = json.load(f)

assert data["draft_subtitle"] == "ThinkPad"
assert data["form_title"] == "Updated Form"


def test_list_drafts_uses_persisted_draft_subtitle(drafts_dir):
"""list_drafts reads draft_subtitle from the JSON (not from forms dict)."""
form_def = {
"title": "Übergabe",
"fields": [{"name": "user", "in_draft_title": True}],
}
save_draft(drafts_dir, "handover", {"user": "Max"}, form_def)

# Pass an empty forms dict – subtitle must still come from persisted value
drafts = list_drafts(drafts_dir, {})

assert drafts[0]["draft_subtitle"] == "Max"
assert drafts[0]["form_title"] == "Übergabe"


def test_list_drafts_falls_back_for_old_drafts_without_persisted_subtitle(drafts_dir):
"""list_drafts falls back to runtime calculation for old drafts without draft_subtitle."""
forms = {
"legacy_form": {
"title": "Legacy",
"fields": [{"name": "user", "in_draft_title": True}],
}
}
# Write a draft JSON without draft_subtitle (simulates old format)
old_draft = {
"draft_id": "old123",
"form_id": "legacy_form",
"saved_at": "2024-01-01T00:00:00+00:00",
"form_data": {"user": "OldUser"},
}
path = os.path.join(drafts_dir, "draft_old123.json")
with open(path, "w", encoding="utf-8") as f:
json.dump(old_draft, f)

drafts = list_drafts(drafts_dir, forms)

assert drafts[0]["draft_subtitle"] == "OldUser"
assert drafts[0]["form_title"] == "Legacy"