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
Select Git revision

Target

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