Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • AlekSIS/libs/django-forms-as-jsonschema
1 result
Show changes
Commits on Source (2)
from typing import Optional
from django import forms
from django_forms_as_jsonschema.layout import _Section
from .layout import _Section
class JSONSchema:
"""Encapsulation of a JSON form schema."""
def __init__(self):
self.schema = {
......@@ -14,6 +17,8 @@ class JSONSchema:
@staticmethod
def generate_field(field):
"""Generate schema for one form field."""
# Defaults for a generic field
new_field = {
"type": "string",
"title": str(field.label or ""),
......@@ -22,63 +27,43 @@ class JSONSchema:
"required": field.required,
}
# string, number, integer, boolean.
if type(field.widget) == forms.TextInput:
# Inspect field and update schema accordingly
if isinstance(field.widget, forms.TextInput):
new_field["type"] = "string"
elif type(field.widget) == forms.NumberInput:
new_field["type"] = "integer" if type(field) == forms.IntegerField else "number"
elif type(field.widget) == forms.EmailInput:
elif isinstance(field.widget, forms.NumberInput) and isinstance(field, forms.IntegerField):
new_field["type"] = "integer"
elif isinstance(field.widget, forms.NumberInput):
new_field["type"] = "number"
elif isinstance(field.widget, forms.EmailInput):
new_field["type"] = "string"
new_field["format"] = "email"
elif type(field.widget) == forms.URLInput:
elif isinstance(field.widget, forms.URLInput):
new_field["type"] = "string"
new_field["format"] = "url"
elif type(field.widget) == forms.PasswordInput:
elif isinstance(field.widget, forms.PasswordInput):
new_field["type"] = "string"
new_field["format"] = "password"
new_field["x-display"] = "password"
elif type(field.widget) == forms.HiddenInput:
elif isinstance(field.widget, forms.HiddenInput):
new_field["type"] = "string"
new_field["format"] = "hidden"
elif type(field.widget) == forms.MultipleHiddenInput:
...
elif type(field.widget) in [forms.FileInput, forms.ClearableFileInput]:
new_field |= {
"type": "string",
"contentMediaType": "image/*" if type(field) == forms.ImageField else "*",
"writeOnly": True
}
# Fixme: differentiate between clearable and non-clearable
# elif type(field.widget) == forms.ClearableFileInput:
# ...
elif type(field.widget) == forms.Textarea:
elif isinstance(field.widget, forms.FileInput):
new_field["type"] = "string"
new_field["contentMediaType"] = "image/*" if isinstance(field, forms.ImageField) else "*"
new_field["writeOnly"] = True
elif isinstance(field.widget, forms.Textarea):
new_field["x-display"] = "textarea"
elif type(field.widget) == forms.DateInput:
elif isinstance(field.widget, forms.DateInput):
new_field["type"] = "string"
new_field["format"] = "date"
elif type(field.widget) == forms.DateTimeInput:
elif isinstance(field.widget, forms.DateTimeInput):
new_field["type"] = "string"
new_field["format"] = "date-time"
elif type(field.widget) == forms.TimeInput:
elif isinstance(field.widget, forms.TimeInput):
new_field["type"] = "string"
new_field["format"] = "time"
elif type(field.widget) == forms.CheckboxInput:
elif isinstance(field.widget, forms.CheckboxInput):
new_field["type"] = "boolean"
elif type(field.widget) in [forms.Select, forms.SelectMultiple, forms.RadioSelect, forms.CheckboxSelectMultiple,
forms.NullBooleanSelect]:
one_of = []
......@@ -95,26 +80,17 @@ class JSONSchema:
new_field["type"] = "string"
new_field["oneOf"] = one_of
if type(field.widget) == forms.RadioSelect:
if isinstance(field.widget, forms.RadioSelect):
new_field["x-display"] = "radio"
elif type(field.widget) == forms.CheckboxSelectMultiple:
elif isinstance(field.widget, forms.CheckboxSelectMultiple):
new_field["x-display"] = "checkbox"
elif type(field.widget) == forms.SplitDateTimeWidget:
...
elif type(field.widget) == forms.SplitHiddenDateTimeWidget:
...
elif type(field.widget) == forms.SelectDateWidget:
...
else:
print("[Django-forms-as-jsonschema] Unsupported field/widget detected: ")
print(f"{field=}, {type(field)=}, {type(field.widget)=}")
raise TypeError(f"Unsupported field/widget: {repr(field)}, {repr(field.widget)}")
return new_field
def add_field(self, name, field, metadata: dict = None):
def add_field(self, name, field, metadata: Optional[dict] = None):
"""Add a form field to this schema."""
new_field = self.generate_field(field)
if metadata and metadata.get("section_name") and self.schema["properties"].get(
metadata["section_name"]
......@@ -127,6 +103,7 @@ class JSONSchema:
@staticmethod
def generate_section(section: _Section):
"""Generate schema for a form section."""
sec = {
"title": section.title,
"type": "object",
......@@ -139,7 +116,9 @@ class JSONSchema:
return sec
def add_section(self, section: _Section):
"""Add a form section to this schema."""
self.schema["properties"][section.codename] = self.generate_section(section)
def update_properties(self, props: dict):
"""Merge new property values into schema properties."""
self.schema["properties"].update(props)
import uuid
"""Re-implementation of django-material's form layout mechanics.
This implementation provides a drop-in replacement for django-material's
Layout, Row, and Fieldset classes, which allow rendering into a JSON schema.
cf. http://docs.viewflow.io/material_forms.html
"""
from abc import ABC
from dataclasses import dataclass
from typing import Union
from typing import Optional, Union
from django.utils.text import slugify
class LayoutNode(ABC):
"""Abstract node in a form layout."""
def __init__(self, *elements):
self.elements = elements
def build_schema(self, schema, form_fields) -> dict:
"""Render this node into a JSON schema fragment."""
props = {}
for field in self.elements:
if isinstance(field, LayoutNode):
# Recurse and add fragment of the sub-node
built_schema = field.build_schema(schema, form_fields)
props.update(built_schema["properties"])
else:
# Add verbatim field
props[field] = schema.generate_field(form_fields[field])
return {"properties": props}
@dataclass
class _Section:
"""Visual section in a form."""
codename: str
title: str
description: str = None
description: Optional[str] = None
class Row(LayoutNode):
"""Visual row in a form."""
def build_schema(self, schema, form_fields):
row_name = "row-" + str(uuid.uuid4())
"""Render this row as a JSON schema fragment."""
row_name = f"row-{id(self)}"
fields = super().build_schema(schema, form_fields)["properties"]
for k in fields.keys():
fields[k]["x-options"] = {
"fieldColProps": {
"cols": 12,
"md": ""
}
}
# Make row responsive by extending each field to full width on smaller viewports
fields[k].setdefault("x-options", {}).setdefault("fieldColProps", {}).update({"cols": 12, "md": ""})
return {
"properties": {
row_name: {
......@@ -50,15 +63,18 @@ class Row(LayoutNode):
class Fieldset(LayoutNode):
"""Visual set of fields in a form."""
def __init__(self, name: Union[tuple[str, str], str], *elements):
super().__init__(*elements)
codename = str(slugify(name))
if type(name) == tuple:
self.section = _Section(codename, *map(str, name))
if isinstance(name, tuple):
self.section = _Section(codename, *name)
else:
self.section = _Section(codename, str(name))
self.section = _Section(codename, name)
def build_schema(self, schema, form_fields) -> dict:
"""Render this fieldset as a JSON schema fragment."""
section = schema.generate_section(self.section)
section["properties"].update(super().build_schema(schema, form_fields)["properties"])
return {
......@@ -69,4 +85,4 @@ class Fieldset(LayoutNode):
class Layout(LayoutNode):
...
"""Full form layout as the root node."""