Coverage for gws-app/gws/plugin/model_field/text/__init__.py: 85%
47 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""Text field.
3A scalar field for strings. Without a configured widget, the field uses an
4``input`` widget.
6With ``textSearch``, the field takes part in keyword searches of database
7models. The keyword is compared with the column value cast to a string:
9- ``exact``: the value equals the keyword,
10- ``any``, ``begin``, ``end``: the keyword occurs anywhere, at the start or at
11 the end of the value; ``%`` and ``_`` in the keyword are matched literally,
12- ``like``: the keyword is used as an SQL ``LIKE`` pattern as is.
14The pattern match is case-insensitive (``ILIKE``) unless ``caseSensitive`` is set.
15Keywords shorter than ``minLength`` are ignored.
17Example::
19 fields+ {
20 name "name"
21 type "text"
22 textSearch { type "begin" minLength 2 }
23 }
24"""
26from typing import Optional, cast
28import gws
29import gws.base.database.model
30import gws.base.model.scalar_field
31import gws.lib.sa as sa
34@gws.ext.config.modelField('text')
35class Config(gws.base.model.scalar_field.Config):
36 """Field for text values."""
38 textSearch: Optional[gws.TextSearchOptions]
39 """Keyword search options for the field."""
42@gws.ext.props.modelField('text')
43class Props(gws.base.model.scalar_field.Props):
44 pass
47@gws.ext.object.modelField('text')
48class Object(gws.base.model.scalar_field.Object):
49 """Text field object."""
51 attributeType = gws.AttributeType.str
52 textSearch: Optional[gws.TextSearchOptions]
53 """Keyword search options, or None if the field is not searchable."""
55 def configure(self):
56 self.textSearch = self.cfg('textSearch')
57 if self.textSearch:
58 self.supportsKeywordSearch = True
60 def configure_widget(self):
61 if not super().configure_widget():
62 self.widget = self.root.create_shared(gws.ext.object.modelWidget, type='input')
63 return True
65 ##
67 def before_select(self, mc):
68 super().before_select(mc)
70 kw = mc.search.keyword
71 ts = self.textSearch
73 if not kw or not ts or (ts.minLength and len(kw) < ts.minLength):
74 return
76 model = cast(gws.base.database.model.Object, self.model)
77 col = sa.cast(model.column(self.name), sa.String)
79 if ts.type == gws.TextSearchType.exact:
80 mc.dbSelect.keywordWhere.append(col == kw)
81 return
83 if ts.type == gws.TextSearchType.any:
84 kw = '%' + _escape_like(kw) + '%'
85 elif ts.type == gws.TextSearchType.begin:
86 kw = _escape_like(kw) + '%'
87 elif ts.type == gws.TextSearchType.end:
88 kw = '%' + _escape_like(kw)
89 elif ts.type == gws.TextSearchType.like:
90 pass
92 if ts.caseSensitive:
93 mc.dbSelect.keywordWhere.append(col.like(kw, escape='\\'))
94 else:
95 mc.dbSelect.keywordWhere.append(col.ilike(kw, escape='\\'))
98def _escape_like(s, escape='\\'):
99 """Escape the ``LIKE`` wildcards and the escape character in a string."""
100 return s.replace(escape, escape + escape).replace('%', escape + '%').replace('_', escape + '_')