diff --git a/CHANGELOG b/CHANGELOG index f526b4b4bf..0bd0ff6d3e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add help for each value to selection and multiselection * Use safe_join in SharedDataMiddlewareIndex (issue10068) * Use menu name for window opened from menu * Add trigonometric functions to sqlite backend diff --git a/doc/ref/models/fields.rst b/doc/ref/models/fields.rst index 47dcb0b134..0f4707a6e2 100644 --- a/doc/ref/models/fields.rst +++ b/doc/ref/models/fields.rst @@ -446,7 +446,7 @@ A binary field. It will be represented in Python by a ``bytes`` instance. Selection --------- -.. class:: Selection(selection, string[, sort[, selection_change_with[, translate[, \**options]]]]) +.. class:: Selection(selection, string[, sort[, selection_change_with[, translate[, help_selection[, \**options]]]]]) A string field with limited values to choose from. @@ -494,6 +494,10 @@ A string field with limited values to choose from. If true, the human-readable values will be translated. Default value is ``True``. +.. attribute:: Selection.help_selection + + A dictionary mapping the selection value with its help string. + Instance methods: .. method:: Selection.translated([name]) @@ -506,7 +510,7 @@ Instance methods: MultiSelection -------------- -.. class:: MultiSelection(selection, string[, sort[, translate[, \**options]]]) +.. class:: MultiSelection(selection, string[, sort[, translate[, help_selection[, \**options]]]]) A list field with limited values to choose from. @@ -526,6 +530,10 @@ A list field with limited values to choose from. Same as :attr:`Selection.translate_selection` +.. attribute:: MultiSelection.help_selection + + Same as :attr:`Selection.help_selection` + Instance methods: .. method:: MultiSelection.translated([name]) @@ -536,7 +544,7 @@ Instance methods: Reference --------- -.. class:: Reference(string[, selection[, sort[, selection_change_with[, translated[,search_order[, search_context[, \**options]]]]]]]) +.. class:: Reference(string[, selection[, sort[, selection_change_with[, translate[, help_selection[,search_order[, search_context[, \**options]]]]]]]]) A field that refers to a record of a model. It will be represented in Python by a ``str`` instance like this:: @@ -563,6 +571,10 @@ But a ``tuple`` can be used to search or set value. Same as :attr:`Selection.translate_selection`. +.. attribute:: Reference.help_selection + + Same as :attr:`Selection.help_selection`. + .. attribute:: Reference.datetime_field Same as :attr:`Many2One.datetime_field` diff --git a/doc/topics/views/index.rst b/doc/topics/views/index.rst index 65cf247281..56a00e8e9f 100644 --- a/doc/topics/views/index.rst +++ b/doc/topics/views/index.rst @@ -167,6 +167,11 @@ List of attributes shared by many form elements: * ``symbol``: Only on numerical fields, the name of field which provides the symbol to display. + .. _common-attributes-help_field: + + * ``help_field``: The name of Dict field mapping the Selection value with + its help string. + form ^^^^ @@ -289,6 +294,8 @@ Display a field of the object with the value of the current record. * ``symbol``: see in common-attributes-symbol_. + * ``help_field``: see in common-attributes-help_field_. + image ^^^^^ @@ -609,6 +616,8 @@ field * ``symbol``: see in common-attributes-symbol_. + * ``help_field``: see in common-attributes-help_field_. + prefix or suffix ^^^^^^^^^^^^^^^^ diff --git a/trytond/ir/message.xml b/trytond/ir/message.xml index f64364dc47..e3bda1061d 100644 --- a/trytond/ir/message.xml +++ b/trytond/ir/message.xml @@ -108,9 +108,18 @@ this repository contains the full copyright notices and license terms. --> If the selection must be sorted on label. + + Help Selection + + + The key followed by the help text separated by a ":", one per line. + Selection JSON + + Help Selection JSON + Sequence @@ -199,6 +208,9 @@ this repository contains the full copyright notices and license terms. --> Invalid selection in schema "%(schema)s". + + Invalid help selection in schema "%(schema)s". + Recursion error: Record "%(rec_name)s" with parent "%(parent_rec_name)s" was configured as ancestor of itself. diff --git a/trytond/ir/ui/form.rnc b/trytond/ir/ui/form.rnc index 458b95f146..f22a637b76 100644 --- a/trytond/ir/ui/form.rnc +++ b/trytond/ir/ui/form.rnc @@ -105,6 +105,7 @@ attlist.field &= [a:defaultValue = "1"] attribute completion { "0" | "1" }? attlist.field &= attribute string { text }? attlist.field &= [a:defaultValue = "1"] attribute factor { text }? attlist.field &= attribute filename { text }? +attlist.field &= attribute help_field { text }? attlist.field &= [a:defaultValue = "0"] attribute toolbar { "0" | "1" }? attlist.field &= attribute symbol { text }? diff --git a/trytond/ir/ui/form.rng b/trytond/ir/ui/form.rng index 0567c3e455..af0e3b5c07 100644 --- a/trytond/ir/ui/form.rng +++ b/trytond/ir/ui/form.rng @@ -465,6 +465,14 @@ + + + + help_field + + + + diff --git a/trytond/ir/ui/tree.rnc b/trytond/ir/ui/tree.rnc index 9e72de412a..a24cb8a3ee 100644 --- a/trytond/ir/ui/tree.rnc +++ b/trytond/ir/ui/tree.rnc @@ -63,6 +63,7 @@ attlist.field &= [a:defaultValue = "1"] attribute completion { "0" | "1" }? attlist.field &= attribute string { text }? attlist.field &= [a:defaultValue = "1"] attribute factor { text }? attlist.field &= attribute filename { text }? +attlist.field &= attribute help_field { text }? attlist.field &= attribute view_ids { text }? attlist.field &= attribute symbol { text }? prefix = element prefix { attlist.affix, empty } diff --git a/trytond/ir/ui/tree.rng b/trytond/ir/ui/tree.rng index 674d77d23a..132b320efe 100644 --- a/trytond/ir/ui/tree.rng +++ b/trytond/ir/ui/tree.rng @@ -248,6 +248,14 @@ + + + + help_field + + + + diff --git a/trytond/model/dictschema.py b/trytond/model/dictschema.py index d468f6aac7..a0ca8bb332 100644 --- a/trytond/model/dictschema.py +++ b/trytond/model/dictschema.py @@ -63,6 +63,13 @@ class DictSchemaMixin(object): 'invisible': ~Eval('type_').in_(['selection', 'multiselection']), }, depends=['type_'], help=lazy_gettext('ir.msg_dict_schema_selection_sorted_help')) + help_selection = fields.Text( + lazy_gettext('ir.msg_dict_schema_help_selection'), translate=True, + states={ + 'invisible': ~Eval('type_').in_(['selection', 'multiselection']), + }, + depends=['type_'], + help=lazy_gettext('is.msg_dict_schema_help_selection_help')) selection_json = fields.Function(fields.Char( lazy_gettext('ir.msg_dict_schema_selection_json'), states={ @@ -70,6 +77,13 @@ class DictSchemaMixin(object): ['selection', 'multiselection']), }, depends=['type_']), 'get_selection_json') + help_selection_json = fields.Function(fields.Char( + lazy_gettext('ir.msg_dict_schema_help_selection_json'), + states={ + 'invisible': ~Eval('type_').in_( + ['selection', 'multiselection']), + }, + depends=['type_']), 'get_selection_json') _relation_fields_cache = Cache('_dict_schema_mixin.get_relation_fields') @classmethod @@ -119,15 +133,17 @@ class DictSchemaMixin(object): for schema in schemas: if schema.type_ not in {'selection', 'multiselection'}: continue - try: - dict(json.loads(schema.get_selection_json())) - except Exception: - raise SelectionError( - gettext('ir.msg_dict_schema_invalid_selection', - schema=schema.rec_name)) + for name in ['selection', 'help_selection']: + try: + dict(json.loads(schema.get_selection_json(name + '_json'))) + except Exception: + raise SelectionError( + gettext('ir.msg_dict_schema_invalid_%s' % name, + schema=schema.rec_name)) - def get_selection_json(self, name=None): - db_selection = self.selection or '' + def get_selection_json(self, name): + field = name[:-len('_json')] + db_selection = getattr(self, field) or '' selection = [[w.strip() for w in v.split(':', 1)] for v in db_selection.splitlines() if v] return json.dumps(selection, separators=(',', ':')) @@ -154,6 +170,8 @@ class DictSchemaMixin(object): english_key.selection_json)) selection.update(dict(json.loads(record.selection_json))) new_key['selection'] = list(selection.items()) + new_key['help_selection'] = dict( + json.loads(record.help_selection_json)) new_key['sort'] = record.selection_sorted elif record.type_ in ('float', 'numeric'): new_key['digits'] = (16, record.digits) diff --git a/trytond/model/fields/multiselection.py b/trytond/model/fields/multiselection.py index 4d1c3fbb68..060b6e57a4 100644 --- a/trytond/model/fields/multiselection.py +++ b/trytond/model/fields/multiselection.py @@ -21,9 +21,9 @@ class MultiSelection(SelectionMixin, Field): _py_type = list def __init__(self, selection, string='', sort=True, translate=True, - help='', required=False, readonly=False, domain=None, states=None, - select=False, on_change=None, on_change_with=None, depends=None, - context=None, loading='eager'): + help='', help_selection=None, required=False, readonly=False, + domain=None, states=None, select=False, on_change=None, + on_change_with=None, depends=None, context=None, loading='eager'): """ :param selection: A list or a function name that returns a list. The list must be a list of tuples. First member is the value @@ -41,6 +41,7 @@ class MultiSelection(SelectionMixin, Field): self.selection_change_with = set() self.sort = sort self.translate_selection = translate + self.help_selection = help_selection __init__.__doc__ += Field.__init__.__doc__ def set_rpc(self, model): diff --git a/trytond/model/fields/reference.py b/trytond/model/fields/reference.py index fec7bdc2ce..c71e1a07d2 100644 --- a/trytond/model/fields/reference.py +++ b/trytond/model/fields/reference.py @@ -24,10 +24,10 @@ class Reference(SelectionMixin, Field): def __init__(self, string='', selection=None, sort=True, selection_change_with=None, translate=True, search_order=None, - search_context=None, help='', required=False, readonly=False, - domain=None, states=None, select=False, on_change=None, - on_change_with=None, depends=None, context=None, loading='lazy', - datetime_field=None): + search_context=None, help='', help_selection=None, required=False, + readonly=False, domain=None, states=None, select=False, + on_change=None, on_change_with=None, depends=None, context=None, + loading='lazy', datetime_field=None): ''' :param selection: A list or a function name that returns a list. The list must be a list of tuples. First member is an internal name @@ -57,6 +57,7 @@ class Reference(SelectionMixin, Field): self.selection_change_with |= set(selection_change_with) self.sort = sort self.translate_selection = translate + self.help_selection = help_selection self.__search_order = None self.search_order = search_order self.__search_context = None diff --git a/trytond/model/fields/selection.py b/trytond/model/fields/selection.py index c6d5c63141..be887038ef 100644 --- a/trytond/model/fields/selection.py +++ b/trytond/model/fields/selection.py @@ -1,6 +1,7 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import warnings +from itertools import chain from sql.conditionals import Case @@ -40,19 +41,33 @@ class SelectionMixin(Field): selection = self.selection.copy() else: selection = self.selection + if self.help_selection: + help_selection = {} + for key, source in self.help_selection.items(): + if not isinstance(source, LazyString): + source = Translation.get_source( + name, 'selection', language, source) or source + help_selection[key] = str(source) + else: + help_selection = None definition['selection'] = selection definition['selection_change_with'] = list(self.selection_change_with) definition['sort'] = self.sort + definition['help_selection'] = help_selection return definition def definition_translations(self, model, language): name = '%s,%s' % (model.__name__, self.name) selection = [] + sources = [] if not isinstance(self.selection, str) and self.translate_selection: - for key, source in self.selection: - if not isinstance(source, LazyString): - selection.append( - (name, 'selection', language, source)) + sources.append(self.selection) + if self.help_selection: + sources.append(self.help_selection.items()) + for key, source in chain(*sources): + if not isinstance(source, LazyString): + selection.append( + (name, 'selection', language, source)) return super().definition_translations(model, language) + selection @@ -66,9 +81,9 @@ class Selection(SelectionMixin, Field): def __init__(self, selection, string='', sort=True, selection_change_with=None, translate=True, help='', - required=False, readonly=False, domain=None, states=None, - select=False, on_change=None, on_change_with=None, depends=None, - context=None, loading='eager'): + help_selection=None, required=False, readonly=False, domain=None, + states=None, select=False, on_change=None, on_change_with=None, + depends=None, context=None, loading='eager'): ''' :param selection: A list or a function name that returns a list. The list must be a list of tuples. First member is the value @@ -91,6 +106,7 @@ class Selection(SelectionMixin, Field): self.selection_change_with |= set(selection_change_with) self.sort = sort self.translate_selection = translate + self.help_selection = help_selection __init__.__doc__ += Field.__init__.__doc__ def set_rpc(self, model):