How to customise a sample form#
This guide writes a form of your own for a sample type, with extra validation, a field order, a grouped layout and a custom widget. You would do this when the form the registry generates for the type is not enough.
Before you start#
You need a registered sample type. This guide uses CoreSample, a sample type of your portal with two fields of its own. To add one, see How to add a sample type.
from fairdm.core.sample.models import Sample
from fairdm.db import models
class CoreSample(Sample):
depth_m = models.FloatField("Depth (m)", null=True, blank=True)
lithology = models.CharField("Lithology", max_length=100, blank=True)
Steps#
Write a form that combines
SampleFormMixinwithforms.ModelForm. The mixin gives the form its dataset field, its location input and its crispy-forms helper. See Form and filter mixins.from django import forms from fairdm.core.sample.forms import SampleFormMixin from myportal.models import CoreSample class CoreSampleForm(SampleFormMixin, forms.ModelForm): class Meta: model = CoreSample fields = ["name", "dataset", "local_id", "status", "location", "depth_m", "lithology"]
The form lists
dataset,statusandlocationinMeta.fieldsbecause the mixin configures a field only when the form has it.Set the order of the fields with
Meta.fields. The form draws them in that order.Add validation for one field with a
clean_<field>method, and across fields withclean().from django.core.exceptions import ValidationError class CoreSampleForm(SampleFormMixin, forms.ModelForm): class Meta: model = CoreSample fields = ["name", "dataset", "local_id", "status", "location", "depth_m", "lithology"] def clean_depth_m(self): depth = self.cleaned_data.get("depth_m") if depth is not None and depth < 0: raise ValidationError("Depth cannot be negative.") return depth def clean(self): cleaned_data = super().clean() if cleaned_data.get("depth_m") and not cleaned_data.get("lithology"): self.add_error("lithology", "Say what the core is made of.") return cleaned_data
Change a widget in
Meta.widgets.class Meta: model = CoreSample fields = ["name", "dataset", "local_id", "status", "location", "depth_m", "lithology"] widgets = { "depth_m": forms.NumberInput(attrs={"step": "0.01", "min": "0"}), }
To group the fields, set the helper’s layout after the mixin has built it. List every field you want drawn, because a crispy layout draws only the fields it names. Leave the first group without a legend and give legends to the groups after it.
from crispy_forms.layout import Fieldset, Layout class CoreSampleForm(SampleFormMixin, forms.ModelForm): class Meta: model = CoreSample fields = ["name", "dataset", "local_id", "status", "location", "depth_m", "lithology"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # The mixin leaves out `dataset` for someone who cannot manage a saved sample. first = [n for n in ("name", "dataset", "local_id") if n in self.fields] self.helper.layout = Layout( *first, Fieldset("Core details", "depth_m", "lithology"), Fieldset("Where and in what state", "location", "status"), )
The mixin sets
form_tag = Falseon the helper, so the page supplies the<form>tag.Tell the registry to use the form, so the type’s edit page uses it. Put the configuration in your app’s
config.py.import fairdm from fairdm.core.sample.config import BaseSampleConfiguration from myportal.models import CoreSample @fairdm.register class CoreSampleConfig(BaseSampleConfiguration): model = CoreSample form_class = "myportal.forms.CoreSampleForm"
A configuration declares either
form_fieldsorform_class, never both. To use the form in a view of your own instead, see How to use a sample form in a view.
The result#
The type’s form keeps everything the mixin provides: a dataset field that offers only the datasets the person may edit, the location input, and a status drop-down that starts at “Unknown”. It also carries your validation, order, widget and layout. Always pass request when you build the form yourself, because the dataset field needs it to know who is choosing. A form built with no request offers no dataset.