Introduction to Combo boxes¶
A combo box is a widget used to edit a field that can have a number of choices. See also Radio buttons.
This unit uses the
lino_book.projects.combo project to show how
to define dynamic lists of choices for a combobox field in a Lino
All related fields and choice lists are rendered in lino using a combo box. This component allows for a selection of a single choice from a dropdown. It also supports tying of a query to filter the choices.
Filtering is done via setting
Model.quick_search_fields, or by overriding
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 hack because Django has no built-in API for producing this behaviour.
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
When the model also defines a method
create_<fieldname>_choice, then the
chooser will become "learning": the ComboBox will be told to accept
also new values, and the server will handle these cases accordingly.
In the example application you can create new cities by simply typing them into the combobox.
class Person(dd.Model): ... def create_city_choice(self, text): """ Called when an unknown city name was given. Try to auto-create it. """ if self.country is not None: return rt.models.countries.Place.lookup_or_create( 'name', text, country=self.country) raise ValidationError( "Cannot auto-create city %r if country is empty", text)
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): """ 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.
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
__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)
The files we are going to use in this tutorial are already on your
hard disk in the
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.
This is inspired by Vitor Freitas' blog post How to Implement Dependent/Chained Dropdown List with Django.
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 ==== ========= ==========