Coverage for lino/core/choicelists.py : 73%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
# -*- coding: UTF-8 -*- # Copyright 2008-2016 Luc Saffre # License: BSD (see file COPYING for details)
Defines the classes :class:`Choice` and :class:`ChoiceList`. See :doc:`/dev/choicelists`.
Defining your own ChoiceLists -----------------------------
>>> class MyColors(Choicelist): ... verbose_name_plural = "My colors" >>> MyColors.add_item('01', ("Red"), 'red') >>> MyColors.add_item('02', ("Green"), 'green')
`add_item` takes at least 2 and optionally a third positional argument:
- The `value` is used to store this Choice in the database and for sorting the choices. - The `text` is what the user sees. It should be translatable. - The optional `name` is used to install this choice as a class attribute on the ChoiceList.
The `value` must be a string.
>>> MyColors.add_item(1, _("Green"), 'green') >>> MyColors.add_item(1, _("Green"), 'verbose_name_plural')
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"))
Every user-defined subclass of ChoiceList is also automatically available as a property value in :mod:`lino.modlib.properties`. """
"""A constant value whose unicode representation depends on the current language at runtime. Every item of a :class:`ChoiceList` must be an instance of :class:`Choice` or a subclass thereof.
""" """(a string) The value to use e.g. when this choice is being stored in a database.""" """A translatable string containing the text to show to the user.
"""
"""A string to be used as attribute name on the choicelist for referring to this choice from application code.
If this is `None` or not specified, the choice is a nameless choice, which is a full-fledged choice object but is not accessible as a class attribute on its choicelists
"""
"""Create a new :class:`Choice` instance.
Parameters: see :attr:`value`, :attr:`text` and :attr:`name`. Any keyword arguments will become attributes on the instance.
This is also being called from :meth:`Choicelist.add_item`.
""" raise Exception("value must be a string") # if name is not None: # if self.name is None: # self.name = value # else: # self.name = name if self.text is None: self.text = self.__class__.__name__ else: # assert_pure(text)
for k, v in list(kwargs.items()): if not hasattr(self, k): raise Exception("%s has no attribute `%s`" % (self, k)) setattr(self, k, v)
return len(self.value)
#~ 20120620: removed to see where it was used #~ def __getattr__(self,name): #~ return curry(getattr(self.choicelist, name),self)
return "<%s:%s>" % (self.choicelist.__name__, self.value) else: self.choicelist.__name__, self.name, self.value)
# def __str__(self): # if self.name: # return str(self.name) # return str(self.text)
# return force_text(self.text, errors="replace") # return self.text
"""Return this as a callable so it can be used as `default` of a field. A Choice object may not be callable itself because Django 1.9 would misunderstand it.
""" return self # def f(): # return self # return f
# def __call__(self): # """Make it callable so it can be used as `default` of a field.""" # return self
def get_chooser_for_field(cls, fieldname): return None
""" Used when implementing :ref:`polymorphism`. """ return self
self.choicelist = choicelist self.value = value self.text = "Unresolved value %r for %s" % ( value, choicelist.__name__) self.name = None
#~ print '20121209 register_choicelist', cl #~ k = cl.stored_name or cl.__name__ raise Exception( "Cannot register %r : actor name '%s' " "already defined by %r" % (cl, k, CHOICELISTS[k])) # logger.warning("ChoiceList name '%s' already defined by %s", # k, CHOICELISTS[k])
return CHOICELISTS[i]
"""Return a list of all choicelists defined for this application.""" l = [] for k, v in list(CHOICELISTS.items()): if v.verbose_name_plural is None: text = v.__name__ else: text = v.verbose_name_plural l.append((k, text)) l.sort(lambda a, b: cmp(a[0], b[0])) return l
#~ if not classDict.has_key('app_label'): #~ classDict['app_label'] = cls.__module__.split('.')[-2] """ UserGroups manually sets `max_length` because the default list has only one group with value "system", but applications may want to add longer group names """ raise Exception("label replaced by verbose_name_plural")
#~ cls.max_length = 1 #~ assert not hasattr(cls,'items') 20120620 #~ for i in cls.items: #~ cls.add_item(i) #~ if settings.SITE.is_installed(cls.app_label):
#~ choicelist_column_fields = dict() #~ choicelist_column_fields['value'] = fields.VirtualField(models.CharField()) #~ choicelist_column_fields['text'] = fields.VirtualField(models.CharField()) #~ choicelist_column_fields['name'] = fields.VirtualField(models.CharField())
""" User-defined choice lists must inherit from this base class. """
""" The class of items of this list. """
""" Default preferred with for ChoiceList fields to this list. """
"""Every subclass of ChoiceList will be automatically registered. Define this if your class's name clashes with the name of an existing ChoiceList.
"""
"""Set this to `True` if the user interface should include the `value` attribute of each choice.
"""
"""Preferred width (in characters) used by :class:`fields<lino.core.fields.ChoiceListField>` that refer to this list.
If this is `None`, then Lino calculates the value at startup, taking the length of the longest choice text. The hard-coded absolute minimum in that case is 4. Note that it calculates the value using the :attr:`default site language <lino.site.Site.languages>` and thus might guess wrong if the user language is not the default site language.
Note that by this we mean the width of the bare text field, excluding any UI-specific control like the trigger button of a combobox. That's why e.g. :mod:`lino.modlib.extjs.ext_elems` adds another value for the trigger button.
"""
def get_default_action(cls):
def get_column_names(self, ar):
def get_data_elem(self, name): return getattr(self, name) #~ return _choicelist_column_fields.get(name)
def value(cls, choice, ar):
def text(cls, choice, ar):
def name(cls, choice, ar):
def remark(cls, choice, ar): return choice.remark
def get_actor_label(self): """ Compute the label of this actor. Called only if `label` is not set, and only once during site startup. """
def clear(cls): """ """ # remove previously defined choices from class dict:
def setup_field(cls, fld):
def field(cls, *args, **kw): """Create a database field (a :class:`ChoiceListField`) that holds one value of this choicelist.
"""
def multifield(cls, *args, **kw): """ Not yet implemented. Create a database field (a :class:`ChoiceListField`) that holds a set of multiple values of this choicelist. """ fld = MultiChoiceListField(cls, *args, **kw) cls._fields.append(fld) return fld
def add_item(cls, *args, **kw): """Instantiates a new choice and adds it to this list. Signature is that of the :meth:`Choice.__init__` method (which might have been overridden if you defined a customized :attr:`item_class`.
""" cls.item_class(*args, **kw))
def class_init(cls):
def add_item_instance(cls, i): #~ if cls is ChoiceList: #~ raise Exception("Cannot define items on the base class") raise Exception("Duplicate value %r in %s." % (i.value, cls)) warnings.warn("Duplicate value %r in %s." % (i.value, cls)) is_duplicate = True # cls.preferred_width = max(cls.preferred_width, len(unicode(dt))) #~ cls.items_dict[i] = i raise Exception( "%s cannot add value %r because fields exist " "and max_length is %d." % (cls, i.value, cls.max_length) + """\ When fields have been created, we cannot simply change their max_length because Django creates copies of them when inheriting models. """) #~ for fld in cls._fields: #~ fld.set_max_length(cls.max_length) raise Exception( "An item named %r is already defined in %s" % ( i.name, cls.__name__)) #~ i.name = name
def get_pk_field(self): """See :meth:`lino.core.actors.Actor.get_pk_field`. """
def get_row_by_pk(cls, ar, pk): return cls.get_by_value(pk)
def to_python(cls, value): # if isinstance(value, Choice): # return value if settings.SITE.strict_choicelist_values: raise Exception( "Unresolved value %r (%s) for %s" % ( value, value.__class__, cls)) else: return UnresolvedValue(cls, value) # Hamza, why did you replace above line by the following ones? # if hasattr(v,'value'): # return v.value # else: # return v #~ return cls.items_dict.get(value) or UnresolvedValue(cls,value) #~ return cls.items_dict[value]
#~ @classmethod #~ def get_label(cls): #~ if cls.label is None: #~ return cls.__name__ #~ return _(cls.label)
def get_choices(cls): return cls.choices
#~ @classmethod #~ def get_choices(cls): #~ """ #~ We must make it dynamic since e.g. UserProfiles can change after #~ the fields have been created.
#~ https://docs.djangoproject.com/en/dev/ref/models/fields/ #~ note that choices can be any iterable object -- not necessarily #~ a list or tuple. This lets you construct choices dynamically. #~ But if you find yourself hacking choices to be dynamic, you're #~ probably better off using a proper database table with a #~ ForeignKey. choices is meant for static data that doesn't #~ change much, if ever. #~ """ #~ for c in cls.choices: #~ yield c
def display_text(cls, bc): """Return the text to be used for representing the given choice instance `bc` to the user.
Override this to customize the display text of choices. :class:`lino.modlib.users.choicelists.UserGroups` and :class:`lino.modlib.cv.models.CefLevel` used to do this before we had the :attr:`ChoiceList.show_values` option.
This must be lazyly translatable because the result are also used to build the `choices` attribute of ChoiceListFields on this choicelist.
Note that Django's `lazy` function has a list of "resultclasses" which are used "so that the automatic forcing of the lazy evaluation code is triggered".
""" # if unicodeerror: # assert_pure(str(bc)) # str(bc)
# return "%s (%s)" % (bc.value, str(bc))
def get_by_name(self, name, *args): """ Supports the case that `name` is `None` (returns `None` then). """ else: return None
def get_by_value(self, value, *args): """Return the item (a :class:`Choice` instance) corresponding to the specified `value`.
""" raise Exception("%r is not a string" % value) #~ print "get_text_for_value" #~ return self.items_dict.get(value, None) #~ return self.items_dict.get(value)
#~ @classmethod #~ def items(self): #~ return [choice[0] for choice in self.choices]
def filter(self, **fkw): def f(item): for k, v in list(fkw.items()): if getattr(item, k) != v: return False return True return [choice[0] for choice in self.choices if f(choice[0])]
def get_list_items(self):
def get_text_for_value(self, value): """ Return the text corresponding to the specified value. """ bc = self.get_by_value(value) if bc is None: return _("%(value)r (invalid choice for %(list)s)") % dict( list=self.__name__, value=value) return self.display_text(bc)
"""A field that stores a value to be selected from a :class:`ChoiceList`.
ChoiceListField cannot be nullable since they are implemented as CharFields. Therefore when filtering on empty values in a database query you cannot use ``__isnull``. The following query won't work as expected::
for u in users.User.objects.filter(profile__isnull=False):
You must either check for an empty string::
for u in users.User.objects.exclude(profile='')
or use the ``__gte`` operator::
for u in users.User.objects.filter(profile__gte=dd.UserLevels.guest):
"""
force_selection=True, **kw): #~ choices=KNOWLEDGE_CHOICES, #~ choices=choicelist.get_choices(), max_length=choicelist.max_length, # ~ blank=choicelist.blank, # null=True, #~ validators=[validate_knowledge], #~ limit_to_choices=True, ) #~ models.SmallIntegerField.__init__(self,*args, **defaults)
# def contribute_to_class(self, cls, name): # super(ChoiceListField, self).contribute_to_class(cls, name) # # add workflow actions to the model so that we can access them # # as InstanceActions # logger.info("20150122 %s %s", cls, name) # for a in self.choicelist.workflow_actions: # logger.info("20150122 %s %s", a.action_name, a) # setattr(cls, a.action_name, a)
"""Needed for Django 1.7+, see https://docs.djangoproject.com/en/dev/howto/custom-model-fields/#custom-field-deconstruct-method
"""
name, path, args, kwargs = super(ChoiceListField, self).deconstruct() args = [self.choicelist]
# kwargs.pop('default', None) # TODO: above line is cheating in order to get makemigrations # to pass. we remove the default attribute because it is not # serializable. This means that our migrations are probably # invalid and not usable.
return name, path, args, kwargs
#~ def set_max_length(self,ml): #~ self.max_length = ml
return None
"""See Django's docs about `to_python() <https://docs.djangoproject.com/en/1.9/ref/models/fields/#django.db.models.Field.to_python>`__.
""" #~ if self.attname == 'query_register': #~ print '20120527 to_python', repr(value), '\n' return self.choicelist.to_python(value)
"""HACK: Django by default stores a copy of our list when the `choices` of a field are evaluated for the first time. We don't want that because ChoiceLists may change afterwards.
"""
# if value != []: # logger.warning("Ignoring set choices {0}".format(value))
"""Excerpt from `Django docs <https://docs.djangoproject.com/en/1.9/howto/custom-model-fields/#converting-python-objects-to-query-values>`__: "If you override `to_python() <https://docs.djangoproject.com/en/1.9/ref/models/fields/#django.db.models.Field.to_python>`__ you also have to override `get_prep_value() <https://docs.djangoproject.com/en/1.9/ref/models/fields/#django.db.models.Field.get_prep_value>`__ to convert Python objects back to query values."
""" #~ if self.attname == 'query_register': #~ print '20120527 get_prep_value()', repr(value) #~ return value.value # Hamza, why did you add the following 2 lines? # if isinstance(value,unicode): # return str(value) value = value() #~ return None
value = self._get_val_from_obj(obj) #~ if self.attname == 'query_register': #~ print '20120527 value_to_string', repr(value) return self.get_prep_value(value) #~ return self.get_db_prep_value(value,connection)
#~ def save_form_data(self, instance, data): #~ setattr(instance, self.name, data)
return self.choicelist.get_text_for_value(value.value)
""" A field whose value is a `list` of `Choice` instances. Stored in the database as a CharField using a delimiter character. """
if verbose_name is None: verbose_name = choicelist.verbose_name_plural self.max_values = max_values defaults = dict( max_length=(choicelist.max_length + 1) * max_values ) defaults.update(kw) ChoiceListField.__init__(self, verbose_name, **defaults)
#~ def set_max_length(self,ml): #~ self.max_length = (ml+1) * self.max_values
return [self.choicelist.to_python(v) for v in value.split(self.delimiter_char)]
if value is None: return [] if isinstance(value, list): return value value = [self.choicelist.to_python(v) for v in value.split(self.delimiter_char)] return value
""" This must convert the given Python value (always a list) into the value to be stored to database. """ return self.delimiter_char.join([bc.value for bc in value])
value = self._get_val_from_obj(obj) return self.get_prep_value(value)
return ', '.join([ self.choicelist.get_text_for_value(bc.value) for bc in value])
import doctest doctest.testmod()
_test() |