1from typing import Any, TYPE_CHECKING, Type
2
3from ormar.queryset.actions import OrderAction
4from ormar.queryset.actions.filter_action import METHODS_TO_OPERATORS
5from ormar.queryset.clause import FilterGroup
6
7if TYPE_CHECKING:  # pragma: no cover
8    from ormar import BaseField, Model
9
10
11class FieldAccessor:
12    """
13    Helper to access ormar fields directly from Model class also for nested
14    models attributes.
15    """
16
17    def __init__(
18        self,
19        source_model: Type["Model"],
20        field: "BaseField" = None,
21        model: Type["Model"] = None,
22        access_chain: str = "",
23    ) -> None:
24        self._source_model = source_model
25        self._field = field
26        self._model = model
27        self._access_chain = access_chain
28
29    def __bool__(self) -> bool:
30        """
31        Hack to avoid pydantic name check from parent model, returns false
32
33        :return: False
34        :rtype: bool
35        """
36        return False
37
38    def __getattr__(self, item: str) -> Any:
39        """
40        Accessor return new accessor for each field and nested models.
41        Thanks to that operator overload is possible to use in filter.
42
43        :param item: attribute name
44        :type item: str
45        :return: FieldAccessor for field or nested model
46        :rtype: ormar.queryset.field_accessor.FieldAccessor
47        """
48        if self._field and item == self._field.name:
49            return self._field
50
51        if self._model and item in self._model.Meta.model_fields:
52            field = self._model.Meta.model_fields[item]
53            if field.is_relation:
54                return FieldAccessor(
55                    source_model=self._source_model,
56                    model=field.to,
57                    access_chain=self._access_chain + f"__{item}",
58                )
59            else:
60                return FieldAccessor(
61                    source_model=self._source_model,
62                    field=field,
63                    access_chain=self._access_chain + f"__{item}",
64                )
65        return object.__getattribute__(self, item)  # pragma: no cover
66
67    def _check_field(self) -> None:
68        if not self._field:
69            raise AttributeError(
70                "Cannot filter by Model, you need to provide model name"
71            )
72
73    def _select_operator(self, op: str, other: Any) -> FilterGroup:
74        self._check_field()
75        filter_kwg = {self._access_chain + f"__{METHODS_TO_OPERATORS[op]}": other}
76        return FilterGroup(**filter_kwg)
77
78    def __eq__(self, other: Any) -> FilterGroup:  # type: ignore
79        """
80        overloaded to work as sql `column = <VALUE>`
81
82        :param other: value to check agains operator
83        :type other: str
84        :return: FilterGroup for operator
85        :rtype: ormar.queryset.clause.FilterGroup
86        """
87        return self._select_operator(op="__eq__", other=other)
88
89    def __ge__(self, other: Any) -> FilterGroup:
90        """
91        overloaded to work as sql `column >= <VALUE>`
92
93        :param other: value to check agains operator
94        :type other: str
95        :return: FilterGroup for operator
96        :rtype: ormar.queryset.clause.FilterGroup
97        """
98        return self._select_operator(op="__ge__", other=other)
99
100    def __gt__(self, other: Any) -> FilterGroup:
101        """
102        overloaded to work as sql `column > <VALUE>`
103
104        :param other: value to check agains operator
105        :type other: str
106        :return: FilterGroup for operator
107        :rtype: ormar.queryset.clause.FilterGroup
108        """
109        return self._select_operator(op="__gt__", other=other)
110
111    def __le__(self, other: Any) -> FilterGroup:
112        """
113        overloaded to work as sql `column <= <VALUE>`
114
115        :param other: value to check agains operator
116        :type other: str
117        :return: FilterGroup for operator
118        :rtype: ormar.queryset.clause.FilterGroup
119        """
120        return self._select_operator(op="__le__", other=other)
121
122    def __lt__(self, other: Any) -> FilterGroup:
123        """
124        overloaded to work as sql `column < <VALUE>`
125
126        :param other: value to check agains operator
127        :type other: str
128        :return: FilterGroup for operator
129        :rtype: ormar.queryset.clause.FilterGroup
130        """
131        return self._select_operator(op="__lt__", other=other)
132
133    def __mod__(self, other: Any) -> FilterGroup:
134        """
135        overloaded to work as sql `column LIKE '%<VALUE>%'`
136
137        :param other: value to check agains operator
138        :type other: str
139        :return: FilterGroup for operator
140        :rtype: ormar.queryset.clause.FilterGroup
141        """
142        return self._select_operator(op="__mod__", other=other)
143
144    def __lshift__(self, other: Any) -> FilterGroup:
145        """
146        overloaded to work as sql `column IN (<VALUE1>, <VALUE2>,...)`
147
148        :param other: value to check agains operator
149        :type other: str
150        :return: FilterGroup for operator
151        :rtype: ormar.queryset.clause.FilterGroup
152        """
153        return self._select_operator(op="in", other=other)
154
155    def __rshift__(self, other: Any) -> FilterGroup:
156        """
157        overloaded to work as sql `column IS NULL`
158
159        :param other: value to check agains operator
160        :type other: str
161        :return: FilterGroup for operator
162        :rtype: ormar.queryset.clause.FilterGroup
163        """
164        return self._select_operator(op="isnull", other=True)
165
166    def in_(self, other: Any) -> FilterGroup:
167        """
168        works as sql `column IN (<VALUE1>, <VALUE2>,...)`
169
170        :param other: value to check agains operator
171        :type other: str
172        :return: FilterGroup for operator
173        :rtype: ormar.queryset.clause.FilterGroup
174        """
175        return self._select_operator(op="in", other=other)
176
177    def iexact(self, other: Any) -> FilterGroup:
178        """
179        works as sql `column = <VALUE>` case-insensitive
180
181        :param other: value to check agains operator
182        :type other: str
183        :return: FilterGroup for operator
184        :rtype: ormar.queryset.clause.FilterGroup
185        """
186        return self._select_operator(op="iexact", other=other)
187
188    def contains(self, other: Any) -> FilterGroup:
189        """
190        works as sql `column LIKE '%<VALUE>%'`
191
192        :param other: value to check agains operator
193        :type other: str
194        :return: FilterGroup for operator
195        :rtype: ormar.queryset.clause.FilterGroup
196        """
197        return self._select_operator(op="contains", other=other)
198
199    def icontains(self, other: Any) -> FilterGroup:
200        """
201        works as sql `column LIKE '%<VALUE>%'` case-insensitive
202
203        :param other: value to check agains operator
204        :type other: str
205        :return: FilterGroup for operator
206        :rtype: ormar.queryset.clause.FilterGroup
207        """
208        return self._select_operator(op="icontains", other=other)
209
210    def startswith(self, other: Any) -> FilterGroup:
211        """
212        works as sql `column LIKE '<VALUE>%'`
213
214        :param other: value to check agains operator
215        :type other: str
216        :return: FilterGroup for operator
217        :rtype: ormar.queryset.clause.FilterGroup
218        """
219        return self._select_operator(op="startswith", other=other)
220
221    def istartswith(self, other: Any) -> FilterGroup:
222        """
223        works as sql `column LIKE '%<VALUE>'` case-insensitive
224
225        :param other: value to check agains operator
226        :type other: str
227        :return: FilterGroup for operator
228        :rtype: ormar.queryset.clause.FilterGroup
229        """
230        return self._select_operator(op="istartswith", other=other)
231
232    def endswith(self, other: Any) -> FilterGroup:
233        """
234        works as sql `column LIKE '%<VALUE>'`
235
236        :param other: value to check agains operator
237        :type other: str
238        :return: FilterGroup for operator
239        :rtype: ormar.queryset.clause.FilterGroup
240        """
241        return self._select_operator(op="endswith", other=other)
242
243    def iendswith(self, other: Any) -> FilterGroup:
244        """
245        works as sql `column LIKE '%<VALUE>'` case-insensitive
246
247        :param other: value to check agains operator
248        :type other: str
249        :return: FilterGroup for operator
250        :rtype: ormar.queryset.clause.FilterGroup
251        """
252        return self._select_operator(op="iendswith", other=other)
253
254    def isnull(self, other: Any) -> FilterGroup:
255        """
256        works as sql `column IS NULL` or `IS NOT NULL`
257
258        :param other: value to check agains operator
259        :type other: str
260        :return: FilterGroup for operator
261        :rtype: ormar.queryset.clause.FilterGroup
262        """
263        return self._select_operator(op="isnull", other=other)
264
265    def asc(self) -> OrderAction:
266        """
267        works as sql `column asc`
268
269        :return: OrderGroup for operator
270        :rtype: ormar.queryset.actions.OrderGroup
271        """
272        return OrderAction(order_str=self._access_chain, model_cls=self._source_model)
273
274    def desc(self) -> OrderAction:
275        """
276        works as sql `column desc`
277
278        :return: OrderGroup for operator
279        :rtype: ormar.queryset.actions.OrderGroup
280        """
281        return OrderAction(
282            order_str="-" + self._access_chain, model_cls=self._source_model
283        )
284