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:

  1. ServerChoicesView will serve up the choices for this registration based on the registered name and field. Permissions are checked according to the perm property. See ServerChoiceFieldRegistration for more details.

  2. 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.