Welcome | Get started | Dive into Lino | Contribute | Topics | Reference | More

Introduction to choicelists

A choicelist is an ordered in-memory list of choices. Each choice has a value, a text and a optionally a name. The value of a choice is what is stored in the database. The text is what the user sees. It is usually translatable. The name can be used to refer to a given choice from program code.

A choicelist looks like a database table to the end user, but they exist in the memory of the server process and are not stored in a database.

Whenever in plain Django you use a choices attribute on a database field, in Lino you probably prefer using a ChoiceList instead.

You can use a choicelist for much more than filling the choices attribute of a database field. You can display a choicelist as a table (using show in a doctest or by adding it to the main menu). You can refer to individual choices programmatically using their name. You can subclass the choices and add application logic.

This page is a tested document and the following instructions are used for initialization:

>>> from lino import startup
>>> startup('lino_book.projects.min9.settings')
>>> from lino.api.doctest import *
>>> from django.utils import translation

Defining your own ChoiceList

>>> from lino.api import _
>>> class MyColors(dd.ChoiceList):
...     verbose_name_plural = _("My colors")
>>> MyColors.add_item('01', _("Red"), 'red')
<core.MyColors.red:01>
>>> MyColors.add_item('02', _("Green"), 'green')
<core.MyColors.green:02>

add_item takes at least 2 and optionally a third positional argument:

  • The first argument (value) is used to store this Choice in a database.

  • The second argument (text) is what the user sees. It should be translatable.

  • The optional third argument (names) is used to install this choice as a class attribute on its ChoiceList.

The value must be a string (or None, but that's a special usage).

>>> MyColors.add_item(3, _("Blue"), 'blue')
Traceback (most recent call last):
...
Exception: value must be a string

Lino protects you from accidentally adding a choice with the same value of an existing choice.

>>> MyColors.add_item("02", _("Blue"), 'blue')
Traceback (most recent call last):
...
Exception: Duplicate value '02' in core.MyColors.

Lino protects you from accidentally adding duplicate entries.

>>> MyColors.add_item("03", _("Blue"), 'green')
Traceback (most recent call last):
...
Exception: An attribute named 'green' is already defined in MyColors
>>> MyColors.add_item("03", _("Blue"), 'verbose_name_plural')
Traceback (most recent call last):
...
Exception: An attribute named 'verbose_name_plural' is already defined in MyColors

You may give multiple names (synonyms) to a choice by specifying them as a space-separated list of names. In that case the first name will be the default name.

>>> MyColors.add_item("03", _("Blue"), 'blue blau bleu')
<core.MyColors.blue:03>
>>> MyColors.blue is MyColors.blau
True
>>> rt.show(MyColors)
======= ================ =======
 value   name             text
------- ---------------- -------
 01      red              Red
 02      green            Green
 03      blue blau bleu   Blue
======= ================ =======

The items are sorted by their order of creation, not by their value. This is visible e.g. in lino.modlib.system.DurationUnits.

Examples

For example Lino has a Weekdays choicelist, which has 7 choices, one for each day of the week.

>>> rt.show('system.Weekdays')
======= =========== ===========
 value   name        text
------- ----------- -----------
 1       monday      Monday
 2       tuesday     Tuesday
 3       wednesday   Wednesday
 4       thursday    Thursday
 5       friday      Friday
 6       saturday    Saturday
 7       sunday      Sunday
======= =========== ===========

Another example is the Genders choicelist, used for the gender field of the lino.mixins.human.Human model mixin:

>>> rt.show('system.Genders')
======= =========== ===========
 value   name        text
------- ----------- -----------
 M       male        Male
 F       female      Female
 N       nonbinary   Nonbinary
======= =========== ===========

Both examples, among others, are defined in the lino.modlib.system plugin.

Accessing choicelists

ChoiceLists are actors. Like every actor, choicelists are never instantiated. They are just the class object itself and as such globally available

You can either import them or use lino.api.rt.models to access them (see Accessing plugins for the difference):

>>> rt.models.system.Weekdays
lino.modlib.system.choicelists.Weekdays
>>> from lino.modlib.system.choicelists import Weekdays
>>> Weekdays
lino.modlib.system.choicelists.Weekdays
>>> Weekdays is rt.models.system.Weekdays
True
>>> from lino.modlib.system.choicelists import Genders
>>> Genders is rt.models.system.Genders
True

You can also write code that dynamically resolves a string of type `app_label.ListName to resolve them:

>>> rt.models.resolve('system.Weekdays') is Weekdays
True

Defining choicelists

Here is how the lino.modlib.system.Weekdays choicelist has been defined:

class Weekdays(dd.ChoiceList):
    verbose_name = _("Weekday")

add = Weekdays.add_item
add('1', _('Monday'), 'monday')
add('2', _('Tuesday'), 'tuesday')
...

This is the easiest case.

More complex examples, including choicelists with extended choices:

Accessing individual choices

Each row of a choicelist is a choice, more precisely an instance of lino.core.choicelists.Choice or a subclass thereof.

Each choice has a "value", a "text" and (optionally) a "name".

The value is what gets stored when this choice is assigned to a database field. It must be unique because it is the analog of primary key.

>>> [g.value for g in Genders.objects()]
['M', 'F', 'N']

The text is what the user sees. It is a translatable string, implemented using Django's i18n machine:

>>> Genders.male.text.__class__  
<class 'django.utils.functional....__proxy__'>

Calling str() of a choice is (usually) the same as calling str() on its text attribute:

>>> [str(g) for g in Genders.objects()]
['Male', 'Female', 'Nonbinary']

The text of a choice depends on the current user language.

>>> with translation.override('fr'):
...     [str(g) for g in Genders.objects()]
['Masculin', 'Féminin', 'Non binaire']
>>> with translation.override('de'):
...     [str(g) for g in Genders.objects()]
['Männlich', 'Weiblich', 'Nichtbinär']
>>> with translation.override('et'):
...     [str(g) for g in Genders.objects()]
['Mees', 'Naine', 'Mittebinaarne']

The text of a choice is a translatable string, while value and name remain unchanged:

>>> with translation.override('fr'):
...     rt.show('system.Weekdays')
======= =========== ==========
 value   name        text
------- ----------- ----------
 1       monday      Lundi
 2       tuesday     Mardi
 3       wednesday   Mercredi
 4       thursday    Jeudi
 5       friday      Vendredi
 6       saturday    Samedi
 7       sunday      Dimanche
======= =========== ==========

Named choices

A choice can optionally have a name, which makes it accessible as class attribute on its choicelist so that application code can refer to this particular choice.

>>> Weekdays.monday
<system.Weekdays.monday:1>
>>> Genders.male
<system.Genders.male:M>
>>> [g.name for g in Genders.objects()]
['male', 'female', 'nonbinary']
>>> [d.name for d in Weekdays.objects()]
['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']

Choicelist fields

You use the Weekdays choicelist in a model definition as follows:

from lino.modlib.system.choicelists import Weekdays

class WeeklyEvent(dd.Model):
    ...
    day_of_week = Weekdays.field(default=Weekdays.monday)

This adds a database field whose value is an instance of lino.core.choicelists.Choice.

A choicelist field is similar to a ForeignKey field in that it uses a combo box as widget, but instead of pointing to a database object it points to a Choice. For the underlying database it is actually a CharField which contains the value (not the name) of its choice.

The lino.mixins.human.Human mixin uses the Genders choicelist as follows:

class Human(Model):
    ...
    gender = Genders.field(blank=True)

Because lino_xl.lib.contacts.Person inherits from Human, you can use this when you want to select all men:

>>> Person = rt.models.contacts.Person
>>> list(Person.objects.filter(gender=Genders.male))
... 
[Person #211 ('Mr Albert Adam'), Person #215 ('Mr Ilja Adam'), Person #114 ('Mr Hans Altenberg'), ...]

Here is a list of all male first names in our contacts database:

>>> sorted({p.first_name for p in Person.objects.filter(gender=Genders.male)})
['Albert', 'Alfons', 'Andreas', 'Bernd', 'Bruno', 'Christian', 'Daniel', 'David', 'Denis', 'Dennis', 'Didier', 'Eberhart', 'Edgar', 'Edgard', 'Emil', 'Erich', 'Erwin', 'Fritz', 'Gregory', 'Guido', 'Hans', 'Henri', 'Hubert', 'Ilja', 'Jan', 'Jean', 'Johann', 'Josef', 'Jérémy', 'Jérôme', 'Karl', 'Kevin', 'Lars', 'Laurent', 'Luc', 'Ludwig', 'Marc', 'Mark', 'Michael', 'Otto', 'Paul', 'Peter', 'Philippe', 'Rik', 'Robin', 'Vincent']

The same for the ladies:

>>> sorted({p.first_name for p in Person.objects.filter(gender=Genders.female)})
['Alice', 'Annette', 'Berta', 'Charlotte', 'Clara', 'Daniela', 'Dora', 'Dorothée', 'Erna', 'Eveline', 'Françoise', 'Gaby', 'Germaine', 'Hedi', 'Hildegard', 'Inge', 'Irene', 'Irma', 'Jacqueline', 'Josefine', 'Laura', 'Line', 'Lisa', 'Marie-Louise', 'Melba', 'Melissa', 'Monique', 'Noémie', 'Odette', 'Pascale', 'Paula', 'Petra', 'Ulrike', 'Õie']

A ChoiceList has an get_list_items() method which returns an iterator over its choices:

>>> print(Genders.get_list_items())
[<system.Genders.male:M>, <system.Genders.female:F>, <system.Genders.nonbinary:N>]

Customizing choicelists

When we say that choicelists are "constant" or "hard-coded", then we should add "for a given Lino site". They can be modified either by a child application or locally by the system administrator.

See workflows_module and user_types_module.

Sorting choicelists

Lino displays the choices of a choicelist in a combobox in their natural order of how they have been added to the list.

You can explicitly call Choicelist.sort() to sort them. This makes sense e.g. in lino_presto.lib.ledger where we add a new journal group "Orders" which we want to come before any other journal groups.

Miscellaneous

Comparing Choices uses their value (not the name nor text):

>>> UserTypes = rt.models.users.UserTypes
>>> UserTypes.admin > UserTypes.user
True
>>> UserTypes.admin == '900'
True
>>> UserTypes.admin == 'manager'
False
>>> UserTypes.admin == ''
False

Seeing all choicelists in your application

>>> from lino.core.kernel import choicelist_choices
>>> pprint(choicelist_choices())
... 
[('about.DateFormats', 'about.DateFormats (Date formats)'),
 ('about.TimeZones', 'about.TimeZones (Time zones)'),
 ('addresses.AddressTypes', 'addresses.AddressTypes (Address types)'),
 ('addresses.DataSources', 'addresses.DataSources (Data sources)'),
 ('cal.DisplayColors', 'cal.DisplayColors (Display colors)'),
 ('cal.EntryStates', 'cal.EntryStates (Entry states)'),
 ('cal.EventEvents', 'cal.EventEvents (Observed events)'),
 ('cal.GuestStates', 'cal.GuestStates (Presence states)'),
 ('cal.NotifyBeforeUnits', 'cal.NotifyBeforeUnits (Notify Units)'),
 ('cal.PlannerColumns', 'cal.PlannerColumns (Planner columns)'),
 ('cal.ReservationStates', 'cal.ReservationStates (States)'),
 ('cal.TaskStates', 'cal.TaskStates (Task states)'),
 ('cal.YearMonths', 'cal.YearMonths'),
 ('calview.Planners', 'calview.Planners'),
 ('changes.ChangeTypes', 'changes.ChangeTypes (Change Types)'),
 ('checkdata.Checkers', 'checkdata.Checkers (Data checkers)'),
 ('comments.CommentEvents', 'comments.CommentEvents (Observed events)'),
 ('comments.Emotions', 'comments.Emotions (Emotions)'),
 ('contacts.CivilStates', 'contacts.CivilStates (Civil states)'),
 ('contacts.PartnerEvents', 'contacts.PartnerEvents (Observed events)'),
 ('countries.PlaceTypes', 'countries.PlaceTypes'),
 ('courses.ActivityLayouts', 'courses.ActivityLayouts (Course layouts)'),
 ('courses.CourseStates', 'courses.CourseStates (Activity states)'),
 ('courses.EnrolmentStates', 'courses.EnrolmentStates (Enrolment states)'),
 ('cv.CefLevel', 'cv.CefLevel (CEF levels)'),
 ('cv.EducationEntryStates', 'cv.EducationEntryStates'),
 ('cv.HowWell', 'cv.HowWell'),
 ('excerpts.Shortcuts', 'excerpts.Shortcuts (Excerpt shortcuts)'),
 ('households.MemberDependencies',
  'households.MemberDependencies (Household Member Dependencies)'),
 ('households.MemberRoles', 'households.MemberRoles (Household member roles)'),
 ('humanlinks.LinkTypes', 'humanlinks.LinkTypes (Parency types)'),
 ('ledger.CommonAccounts', 'ledger.CommonAccounts (Common accounts)'),
 ('ledger.DC', 'ledger.DC (Booking directions)'),
 ('ledger.JournalGroups', 'ledger.JournalGroups (Journal groups)'),
 ('ledger.PeriodStates', 'ledger.PeriodStates (States)'),
 ('ledger.TradeTypes', 'ledger.TradeTypes (Trade types)'),
 ('ledger.VoucherStates', 'ledger.VoucherStates (Voucher states)'),
 ('ledger.VoucherTypes', 'ledger.VoucherTypes (Voucher types)'),
 ('linod.LogLevels', 'linod.LogLevels (Logging levels)'),
 ('linod.Procedures', 'linod.Procedures (Background procedures)'),
 ('notes.SpecialTypes', 'notes.SpecialTypes (Special note types)'),
 ('notify.MailModes', 'notify.MailModes (Notification modes)'),
 ('notify.MessageTypes', 'notify.MessageTypes (Message Types)'),
 ('pages.PageFillers', 'pages.PageFillers (Page fillers)'),
 ('phones.ContactDetailTypes',
  'phones.ContactDetailTypes (Contact detail types)'),
 ('printing.BuildMethods', 'printing.BuildMethods'),
 ('products.BarcodeDrivers', 'products.BarcodeDrivers (Barcode drivers)'),
 ('products.DeliveryUnits', 'products.DeliveryUnits (Delivery units)'),
 ('products.PriceFactors', 'products.PriceFactors (Price factors)'),
 ('products.ProductTypes', 'products.ProductTypes (Product types)'),
 ('properties.DoYouLike', 'properties.DoYouLike'),
 ('properties.HowWell', 'properties.HowWell'),
 ('properties.PropertyAreas', 'properties.PropertyAreas (Property areas)'),
 ('publisher.PublishingStates',
  'publisher.PublishingStates (Publishing states)'),
 ('system.DashboardLayouts', 'system.DashboardLayouts'),
 ('system.DurationUnits', 'system.DurationUnits'),
 ('system.Genders', 'system.Genders'),
 ('system.PeriodEvents', 'system.PeriodEvents (Observed events)'),
 ('system.Recurrences', 'system.Recurrences (Recurrences)'),
 ('system.Weekdays', 'system.Weekdays'),
 ('system.YesNo', 'system.YesNo (Yes or no)'),
 ('uploads.Shortcuts', 'uploads.Shortcuts (Upload shortcuts)'),
 ('uploads.UploadAreas', 'uploads.UploadAreas (Upload areas)'),
 ('users.UserTypes', 'users.UserTypes (User types)'),
 ('vat.DeclarationFieldsBase',
  'vat.DeclarationFieldsBase (Declaration fields)'),
 ('vat.VatAreas', 'vat.VatAreas (VAT areas)'),
 ('vat.VatClasses', 'vat.VatClasses (VAT classes)'),
 ('vat.VatColumns', 'vat.VatColumns (VAT columns)'),
 ('vat.VatRegimes', 'vat.VatRegimes (VAT regimes)'),
 ('vat.VatRules', 'vat.VatRules (VAT rules)'),
 ('xl.Priorities', 'xl.Priorities (Priorities)')]

The lino_xl.lib.properties.PropType.choicelist field uses this function for its choices.

ChoiceListField

Example on how to use a ChoiceList in your model:

from django.db import models
from lino.modlib.properties.models import HowWell

class KnownLanguage(models.Model):
    spoken = HowWell.field(verbose_name=_("spoken"))
    written = HowWell.field(verbose_name=_("written"))