Coverage for src/lektor_ng/types/multi.py: 22%
102 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-08 00:39 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-08 00:39 +0000
1import traceback
3from lektor_ng.constants import PRIMARY_ALT
4from lektor_ng.environment.expressions import Expression, FormatExpression
5from lektor_ng.i18n import get_i18n_block
6from lektor_ng.types.base import Type
9def _reflow_and_split_labels(labels):
10 rv = []
11 for lang, string in labels.items():
12 for idx, item in enumerate(string.split(",")):
13 try:
14 d = rv[idx]
15 except LookupError:
16 d = {}
17 rv.append(d)
18 d[lang] = item.strip()
19 return rv
22def _parse_choices(options):
23 s = options.get("choices")
24 if not s:
25 return None
27 choices = []
28 items = s.split(",")
29 user_labels = get_i18n_block(options, "choice_labels")
30 implied_labels = []
32 for item in items:
33 if "=" in item:
34 choice, value = item.split("=", 1)
35 choice = choice.strip()
36 if choice.isdigit():
37 choice = int(choice)
38 implied_labels.append(value.strip())
39 choices.append(choice)
40 else:
41 choices.append(item.strip())
42 implied_labels.append(item.strip())
44 if user_labels:
45 rv = list(zip(choices, _reflow_and_split_labels(user_labels), strict=False))
46 else:
47 rv = [(key, {"en": label}) for key, label in zip(choices, implied_labels, strict=False)]
49 return rv
52class ChoiceSource:
53 def __init__(self, env, options):
54 source = options.get("source")
55 if source is not None:
56 self.source = Expression(env, source)
57 self.choices = None
58 item_key = options.get("item_key") or "{{ this._id }}"
59 item_label = options.get("item_label")
60 else:
61 self.source = None
62 self.choices = _parse_choices(options)
63 item_key = options.get("item_key") or "{{ this.0 }}"
64 item_label = options.get("item_label")
65 self.item_key = FormatExpression(env, item_key)
66 if item_label is not None:
67 item_label = FormatExpression(env, item_label)
68 self.item_label = item_label
70 @property
71 def has_choices(self):
72 return self.source is not None or self.choices is not None
74 def iter_choices(self, pad, record=None, alt=PRIMARY_ALT):
75 values = {}
76 if record is not None:
77 values["record"] = record
78 if self.choices is not None:
79 iterable = self.choices
80 else:
81 try:
82 iterable = self.source.evaluate(pad, alt=alt, values=values)
83 except Exception:
84 traceback.print_exc()
85 iterable = ()
87 for item in iterable or ():
88 key = self.item_key.evaluate(pad, this=item, alt=alt, values=values)
90 # If there is a label expression, use it. Since in that case
91 # we only have one language to fill in, we fill it in for the
92 # default language
93 if self.item_label is not None:
94 label = {"en": self.item_label.evaluate(pad, this=item, alt=alt, values=values)}
96 # Otherwise we create a proper internationalized key out of
97 # our target label
98 else:
99 if isinstance(item, (tuple, list)) and len(item) == 2:
100 label = item[1]
101 elif hasattr(item, "get_record_label_i18n"):
102 label = item.get_record_label_i18n()
103 else:
104 label = {"en": item["_id"]}
106 yield key, label
109class MultiType(Type):
110 def __init__(self, env, options):
111 Type.__init__(self, env, options)
112 self.source = ChoiceSource(env, options)
114 def get_labels(self, pad, record=None, alt=PRIMARY_ALT):
115 return dict(self.source.iter_choices(pad, record, alt))
117 def to_json(self, pad, record=None, alt=PRIMARY_ALT):
118 rv = Type.to_json(self, pad, record, alt)
119 if self.source.has_choices:
120 rv["choices"] = list(self.source.iter_choices(pad, record, alt))
121 return rv
124class SelectType(MultiType):
125 widget = "select"
127 def value_from_raw(self, raw):
128 if raw.value is None:
129 return raw.missing_value("Missing select value")
130 return raw.value
133class CheckboxesType(MultiType):
134 widget = "checkboxes"
136 def value_from_raw(self, raw):
137 rv = [x.strip() for x in (raw.value or "").split(",")]
138 if rv == [""]:
139 rv = []
140 return rv