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

1"""Text field. 

2 

3A scalar field for strings. Without a configured widget, the field uses an 

4``input`` widget. 

5 

6With ``textSearch``, the field takes part in keyword searches of database 

7models. The keyword is compared with the column value cast to a string: 

8 

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. 

13 

14The pattern match is case-insensitive (``ILIKE``) unless ``caseSensitive`` is set. 

15Keywords shorter than ``minLength`` are ignored. 

16 

17Example:: 

18 

19 fields+ { 

20 name "name" 

21 type "text" 

22 textSearch { type "begin" minLength 2 } 

23 } 

24""" 

25 

26from typing import Optional, cast 

27 

28import gws 

29import gws.base.database.model 

30import gws.base.model.scalar_field 

31import gws.lib.sa as sa 

32 

33 

34@gws.ext.config.modelField('text') 

35class Config(gws.base.model.scalar_field.Config): 

36 """Field for text values.""" 

37 

38 textSearch: Optional[gws.TextSearchOptions] 

39 """Keyword search options for the field.""" 

40 

41 

42@gws.ext.props.modelField('text') 

43class Props(gws.base.model.scalar_field.Props): 

44 pass 

45 

46 

47@gws.ext.object.modelField('text') 

48class Object(gws.base.model.scalar_field.Object): 

49 """Text field object.""" 

50 

51 attributeType = gws.AttributeType.str 

52 textSearch: Optional[gws.TextSearchOptions] 

53 """Keyword search options, or None if the field is not searchable.""" 

54 

55 def configure(self): 

56 self.textSearch = self.cfg('textSearch') 

57 if self.textSearch: 

58 self.supportsKeywordSearch = True 

59 

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 

64 

65 ## 

66 

67 def before_select(self, mc): 

68 super().before_select(mc) 

69 

70 kw = mc.search.keyword 

71 ts = self.textSearch 

72 

73 if not kw or not ts or (ts.minLength and len(kw) < ts.minLength): 

74 return 

75 

76 model = cast(gws.base.database.model.Object, self.model) 

77 col = sa.cast(model.column(self.name), sa.String) 

78 

79 if ts.type == gws.TextSearchType.exact: 

80 mc.dbSelect.keywordWhere.append(col == kw) 

81 return 

82 

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 

91 

92 if ts.caseSensitive: 

93 mc.dbSelect.keywordWhere.append(col.like(kw, escape='\\')) 

94 else: 

95 mc.dbSelect.keywordWhere.append(col.ilike(kw, escape='\\')) 

96 

97 

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 + '_')