Introduction to Combo boxes

A combo box is a widget used to edit a field that has a number of choices. It shows a drop-down list with these choices while you are editing. It also supports filtering these choices while you are typing. Unlike a radio button it allows only one choice, not more.

Lino renders two types of fields as uses combo boxes: foreign key fields and choicelist fields.

The combo box on a foreign key can optionally be "learning", i.e. it can accept new values and store them in the database. See How to define a learning foreign key.

Filtering results

Filtering is done via setting Model.quick_search_fields, or by overriding Model.quick_search_filter().

Context-sensitive Comboboxes

More examples and info can be seen here Chooser examples

The challenge is old: when you have two fields on a model (here country and city on the Person model) and you want the choices for one of these field (here city) to depend on the value of the other field (here country), then you need a context-sensitive chooser.

The Lino solution is you simply define the following function on the Person model:

@dd.chooser()
def city_choices(cls, country):
    return rt.models.combo.City.objects.filter(country=country)

Lino finds all choosers at startup that are decorated with the dd.chooser decorator (which turns it into a "chooser" object) and has a name of the form.

Lino matches it to the field using the fieldname in`<fieldname>_choices``. Lino matches the context related fields by positional argument named the same as other fields. ar is also a valid argument name for the chooser. The value will be the action request used in the API call. The request object can be used to

Then Lino then does the dirty work of generating appropriate JavaScript and HTML code and the views which respond to the AJAX calls.

Screenshots

../../_images/1.png ../../_images/2.png ../../_images/3.png

Source code

This unit uses the lino_book.projects.combo demo project.

Here is the models.py file :

from django.db import models
from django.core.exceptions import ValidationError
from lino.api import dd, rt

class Country(dd.Model):

    class Meta(object):
        verbose_name = "Country"
        verbose_name_plural = "Countries"

    name = models.CharField(max_length=30)

    def __str__(self):
        return self.name

class City(dd.Model):

    class Meta(object):
        verbose_name_plural = "Cities"

    country = dd.ForeignKey(Country, on_delete=models.CASCADE)
    name = models.CharField(max_length=30)

    def __str__(self):
        return self.name

class Person(dd.Model):

    class Meta(object):
        verbose_name_plural = "Persons"

    name = models.CharField(max_length=100)
    birthdate = models.DateField(null=True, blank=True)
    country = dd.ForeignKey(Country, on_delete=models.SET_NULL, null=True)
    city = dd.ForeignKey(City, on_delete=models.SET_NULL, null=True)

    @dd.chooser()
    def city_choices(cls, country):
        return City.objects.filter(country=country)

    def create_city_choice(self, text, ar=None):
        """
        Called when an unknown city name was given.
        """
        if self.country is None:
            raise ValidationError(
                "Cannot auto-create city %r if country is empty", text)
        return City.lookup_or_create(
            'name', text, country=self.country)

    def __str__(self):
        return self.name

Here are the other files used in this unit.

The desktop.py file specifies a table for every model:

from lino.api import dd
from .models import Person, City, Country

class Persons(dd.Table):
    model = Person
    detail_layout = dd.DetailLayout("""
    name
    country
    city
    """, window_size=(50, 'auto'))

    insert_layout = """
    name
    country
    city
    """


class Cities(dd.Table):
    model = City

class Countries(dd.Table):
    model = Country

The __init__.py file specifies how the tables are organized in the main menu:

from lino.api import ad, _


class Plugin(ad.Plugin):
    verbose_name = _("Combo")

    def setup_main_menu(self, site, profile, m):
        m = m.add_menu(self.app_label, self.verbose_name)
        m.add_action('combo.Persons')
        m.add_action('combo.Countries')
        m.add_action('combo.Cities')

    

Here is the project's settings.py file :

from lino.projects.std.settings import *
SITE = Site(globals(), 'lino_book.projects.combo')
SITE.demo_fixtures = ['demo']

DEBUG = True

And finally the fixtures/demo.py file contains the data we use to fill our database:

from lino.api import rt


def objects():
    Country = rt.models.combo.Country
    City = rt.models.combo.City
    
    be = Country(name="Belgium")
    yield be
    ee = Country(name="Estonia")
    yield ee
    
    yield City(name="Eupen", country=be)
    yield City(name="Brussels", country=be)
    yield City(name="Gent", country=be)
    yield City(name="Raeren", country=be)
    yield City(name="Namur", country=be)
    
    yield City(name="Tallinn", country=ee)
    yield City(name="Tartu", country=ee)
    yield City(name="Vigala", country=ee)

Exercise

The files we are going to use in this tutorial are already on your hard disk in the lino_book.projects.combo package.

Start your development server and your browser, and have a look at the application:

$ go combo
$ python manage.py runserver

Explore the application and try to extend it: change things in the code and see what happens.

Discussion

This is inspired by Vitor Freitas' blog post How to Implement Dependent/Chained Dropdown List with Django.

Doctests

The remaining samples are here in order to test the project.

>>> from lino import startup
>>> startup('lino_book.projects.combo.settings')
>>> from lino.api.doctest import *
>>> rt.show('combo.Cities')
==== ========= ==========
 ID   Country   name
---- --------- ----------
 1    Belgium   Eupen
 2    Belgium   Brussels
 3    Belgium   Gent
 4    Belgium   Raeren
 5    Belgium   Namur
 6    Estonia   Tallinn
 7    Estonia   Tartu
 8    Estonia   Vigala
==== ========= ==========