Skip to content
Closed
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
21 changes: 21 additions & 0 deletions src/unfold_fobi/api/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""DRF API views for unfold_fobi."""

import datetime
from django.apps import apps as django_apps
from django.core.exceptions import ObjectDoesNotExist
from django.forms.fields import EmailField
Expand All @@ -13,6 +14,17 @@
from rest_framework.response import Response


def _serialize_initial(value):
"""Return a JSON-friendly initial value for API responses."""
if callable(value):
value = value()
if value is None:
return None
if isinstance(value, (datetime.date, datetime.datetime)):
return value.isoformat()
return value


def normalize_field_choices(field, field_name):
try:
optgroups = field.widget.optgroups(field_name, field.initial)
Expand Down Expand Up @@ -159,6 +171,7 @@ def get_form_fields(request, slug):
}

for field_name, field in fields.items():
widget_attrs = getattr(getattr(field, "widget", None), "attrs", {})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Potential AttributeError if widget.attrs exists but is None rather than a dict.

If a custom widget sets attrs = None, this expression will return None and widget_attrs.get("placeholder") will raise AttributeError. Consider normalizing to a dict, e.g.:

widget = getattr(field, "widget", None)
widget_attrs = getattr(widget, "attrs", {}) or {}

or use a type check after getattr to ensure widget_attrs is a dict before use.

field_info = {
"name": field_name,
"type": field.__class__.__name__,
Expand All @@ -168,6 +181,14 @@ def get_form_fields(request, slug):
"help_text": getattr(field, "help_text", ""),
}

placeholder = widget_attrs.get("placeholder")
if placeholder not in (None, ""):
field_info["placeholder"] = placeholder

initial = _serialize_initial(getattr(field, "initial", None))
if initial is not None:
field_info["initial"] = initial

if hasattr(field, "choices") and field.choices:
field_info["choices"] = normalize_field_choices(field, field_name)

Expand Down
43 changes: 43 additions & 0 deletions tests/api/test_form_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,49 @@ def test_select_field_has_choices(self, admin_client, multi_field_form):
values = [c["value"] for c in f["choices"]]
assert "r" in values

@pytest.fixture()
def metadata_form(self, db, admin_user):
entry = FormEntry.objects.create(
user=admin_user,
name="Metadata Form",
slug="metadata-form",
is_public=True,
)
FormElementEntry.objects.create(
form_entry=entry,
plugin_uid="text",
plugin_data=json.dumps(
{
"label": "Full Name",
"name": "full_name",
"required": True,
"placeholder": "Enter your name",
}
),
position=1,
)
FormElementEntry.objects.create(
form_entry=entry,
plugin_uid="date",
plugin_data=json.dumps(
{
"label": "Initial Date",
"name": "initial",
"required": False,
"initial": "2026-04-16",
}
),
position=2,
)
FormHandlerEntry.objects.get_or_create(form_entry=entry, plugin_uid="db_store")
return entry

def test_includes_placeholder_and_initial(self, admin_client, metadata_form):
fields = _get_fields(admin_client, metadata_form.slug)

assert fields["full_name"]["placeholder"] == "Enter your name"
assert fields["initial"]["initial"] == "2026-04-16"


class TestFormFieldsWidgetDisambiguation:
"""Widget key is correct for every registered fobi field plugin."""
Expand Down
Loading