Usage
Once installation is complete and the view is registered, you can start using the decorator server_choices()
to register choices. The most basic usage is:
@server_choices()
class MyForm(ModelForm):
class Meta:
model = MyModel
fields = [
"field1",
"field2",
]
Imagine field1 and field2 are both foreign keys. The server_choices decorator will automatically register those
fields, and then the frontend can fetch the available choices from ServerChoicesView
a page at a time. In this example the fields are inferred from the class being decorated using the
infer_fields() method.
Out of the box there is support for decorating Django forms, Django filter sets, and
DRF serializers. In the above example, the @server_choices decorator defers to the FormServerChoiceFieldRegistration
registration class. If the class being decorated was a FilterSet or a Serializer then the
FilterSetServerChoiceFieldRegistration or
SerializerServerChoiceFieldRegistration registration
class would be used instead. The decorator passes through the arguments to the registration class. This also means
you can add additional registration classes to handle other usages not covered by the default ones.
Note
DRF integration is optional and only activated if rest_framework is installed.
Instead of inferring fields, you can also explicitly specify the fields to register:
@server_choices(['field1])
class MyForm(ModelForm):
...
You can also decorate multiple times to configure each field individually:
@server_choices(['field1], page_size=0)
@server_choices(['field2], page_size=50)
class MyForm(ModelForm):
...
Choices can be defined as either a queryset, or as a list of 2-tuples (key, value). By default, inference will only pick up
queryset choices - for example ModelChoiceField on a Django Form. To use other choices opt in explicitly:
@server_choices(["simple"], perm="some_perm")
class TestForm(Form):
simple = ChoiceField(choices=[("choice_a", "Choice A"), ("choice_b", "Choice B")])
The rationale for this is that most often choices defined like this are fine to embed in the HTML upfront - dynamically fetching choices is only needed when working with large lists of choices.
Label & Value
Each choice returned from the view only includes, by default, a label and value. The label is the text that is displayed to the user and the value should be a unique identifier for the choice.
The default implementation of the label depends on how choices are defined. If it is a queryset then the label is the
__str__ of the object. If it is a list of tuples then the label is the second element of the tuple. For a queryset,
the value is always the primary key. For a list of tuples, the value is the first element of the tuple.
To customise the label you can pass the get_label argument to the decorator:
@server_choices(
['field2]
get_label=lambda registry, item: f"Item: {str(item)}",
)
class MyForm(ModelForm):
...
Note
The registry argument is the ServerChoicesFieldRegistration
instance, and is passed through to most of the methods you can override.
Permissions
When the endpoint is used to get the available choices permission checks are always applied. You can control what permission is
used by passing the perm kwarg. If not specified and the django Model can be inferred from the decorated
class (eg. when using a ModelSerializer, ModelForm or
FilterSet) then the create permission for that model as returned by
resolve_perm_name() will be used.
For example if you had a ModelForm for the model User which had foreign keys to Address
and Group then the choices for both models would be the create permission on User. The
rationale for this is if you weren’t using server_choices and rendering the form directly there would be no specific
check on the foreign key form fields - all the options would be embedded directly in the returned HTML. Using
create means if you can create the main record you can see the options for each field you need to save on
that record. Note that the only information exposed about the related is the pk and a label for it - you
can’t access all the data from it.
For choice fields not associated with a model you must explicitly define the permission to use.
Serialized value
If you are using the default frontend widgets you will not need to customise the serialized value. If using a custom
implementation it may be necessary to change how the values are returned from the API endpoint. The default implementation
returns each choice as a dictionary with a label and value key:
{
"label": "Item: 1",
"value": 1
}
You can change the key names by passing the label_field and value_field arguments
to the decorator:
@server_choices(
['field2]
label_field="name",
value_field="id",
)
class MyForm(ModelForm):
...
which would return:
{
"name": "Item: 1",
"id": 1
}
You can also pass serialize to completely override the serialization process. This method needs to handle a single
value, or an iterable of values. The exact implementation will depend on the choices you are using, but if dealing with
a django model you might do something like:
def serialize(registry, item, request):
if isinstance(item, MyRecord):
return {"id": item.pk, "name": str(item), "len": len(str(item))}
return [serialize(registry, item, request) for item in item]
@server_choices(
['field2]
serialize=serialize,
)
class MyForm(ModelForm):
...
Frontend Widgets
Django
The default widget renders the component ServerChoicesInput.
This is used when decorating a Django form or filterset.
You can pass extra arguments via the widget to refine choices. For example, to show only the rooms in a given building once a room has been selected, we can pass that as a query parameter.
def get_room_choices(registration, request):
building_id = request.query_params.get("buildingId")
if building_id:
return Room.objects.filter(building_id=building_id)
return Room.objects.all()
@server_choices(
["room"],
search_fields=["name"],
get_choices=get_room_choices
)
class BookingForm(ModelForm):
class Meta:
model = Booking
fields = [
"building",
"room",
"start_time",
"end_time",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and self.instance.pk:
# Filter room choices to the building of this booking
self.fields["room"].widget_attrs_update(
query={"buildingId": self.instance.room.building_id}
)
Here buildingId will come through as a query parameter. The get_choices method can retrieve this from the request
and do whatever is needed with it.
The widget is assigned automatically when the decorator is used, but you can also instantiate it directly to pass different attributes to it:
@server_choices(["pizza"])
class PizzaItemForm(ModelForm):
restaurant = models.ModelChoiceField(widget=ServerChoicesSelectWidget())
class Meta:
model = PizzaItem
fields = [
"restaurant",
]
DRF / React
When a DRF Serializer is decorated the widget that makes use of the choices is assumed to be rendered from React. This is usually done
in conjuction with a Presto ViewModel that has been codegen’d. The codegen takes care of extracting the necessary details for the AsyncChocies
definition onthe frontend. You can then use the FormField
component to render the widget to fetch the choices.
API Endpoint
Once a field has been registered the following applies:
ServerChoicesViewwill serve up the choices for this registration based on the registered name and field. Permissions are checked according to thepermproperty. SeeServerChoiceFieldRegistrationfor more details.Presto codegen will use this registration when creating the base ViewModel classes for classes decorated with
view_model_codegen()
In order for ServerChoicesView to know what to return a unique name is
generated as part of the registration for the class being registered. This is hashed to avoid exposing application
structure to the frontend. This name, along with the specific field name on that class, is passed when calling
ServerChoicesView which it then uses to look up in the global registry to
get the relevant registration instance.