Skip to content

Base Mixins

lariv.mixins

Core View Mixins for Django.

These mixins intercept standard Django HTTP rendering and interface with the custom UIRegistry to construct and return Component-driven HTML trees, often intercepted for partial HTMX updates.

BaseView

Bases: View

The root view class enabling the Python UI component model. Rather than rendering standard Django templates, this view orchestrates the building and rendering of python Component objects via UIRegistry.

Source code in lariv/mixins.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
class BaseView(View):
    """
    The root view class enabling the Python UI component model.
    Rather than rendering standard Django templates, this view orchestrates
    the building and rendering of python `Component` objects via `UIRegistry`.
    """
    def get_queryset(self):
        """Returns the base queryset for the targeted model."""
        return getattr(self, "model").objects.all()

    def get_component(self, request) -> str:
        """Returns the registered string key for the root UI component."""
        return getattr(self, "component")

    def get_key(self) -> str:
        """Returns the primary key identifier mapping data to component attributes."""
        return getattr(self, "key")

    def prepare_data(self, request, **kwargs):
        """
        Hook for injecting dynamic parameters (e.g., loaded models or pagination)
        into the component rendering context.
        """
        return {}

    def render_component(self, request, **kwargs) -> HttpResponse:
        """
        Dynamically builds the UI tree from the registry.

        If the request is via HTMX and specifies a target, it performs a surgical 
        partial render on that node. Otherwise, it renders the entire UI tree.
        """
        component_cls = UIRegistry.get(self.get_component(request))
        tree = component_cls().build()

        render_kwargs = {**kwargs, "view": self, "request": request}

        if request.htmx and request.htmx.target:
            html = tree.render_partial(request.htmx.target, **render_kwargs)
        else:
            html = tree.render(**render_kwargs)

        return HttpResponse(
            Template(html).render(
                RequestContext(request, kwargs),
            ),
        )

    def get(self, request, *args, **kwargs):
        """Standard HTTP GET handler invoking the component rendering pipeline."""
        data = self.prepare_data(request, **kwargs) or {}
        return self.render_component(request=request, **data)

get(request, *args, **kwargs)

Standard HTTP GET handler invoking the component rendering pipeline.

Source code in lariv/mixins.py
72
73
74
75
def get(self, request, *args, **kwargs):
    """Standard HTTP GET handler invoking the component rendering pipeline."""
    data = self.prepare_data(request, **kwargs) or {}
    return self.render_component(request=request, **data)

get_component(request)

Returns the registered string key for the root UI component.

Source code in lariv/mixins.py
34
35
36
def get_component(self, request) -> str:
    """Returns the registered string key for the root UI component."""
    return getattr(self, "component")

get_key()

Returns the primary key identifier mapping data to component attributes.

Source code in lariv/mixins.py
38
39
40
def get_key(self) -> str:
    """Returns the primary key identifier mapping data to component attributes."""
    return getattr(self, "key")

get_queryset()

Returns the base queryset for the targeted model.

Source code in lariv/mixins.py
30
31
32
def get_queryset(self):
    """Returns the base queryset for the targeted model."""
    return getattr(self, "model").objects.all()

prepare_data(request, **kwargs)

Hook for injecting dynamic parameters (e.g., loaded models or pagination) into the component rendering context.

Source code in lariv/mixins.py
42
43
44
45
46
47
def prepare_data(self, request, **kwargs):
    """
    Hook for injecting dynamic parameters (e.g., loaded models or pagination)
    into the component rendering context.
    """
    return {}

render_component(request, **kwargs)

Dynamically builds the UI tree from the registry.

If the request is via HTMX and specifies a target, it performs a surgical partial render on that node. Otherwise, it renders the entire UI tree.

Source code in lariv/mixins.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def render_component(self, request, **kwargs) -> HttpResponse:
    """
    Dynamically builds the UI tree from the registry.

    If the request is via HTMX and specifies a target, it performs a surgical 
    partial render on that node. Otherwise, it renders the entire UI tree.
    """
    component_cls = UIRegistry.get(self.get_component(request))
    tree = component_cls().build()

    render_kwargs = {**kwargs, "view": self, "request": request}

    if request.htmx and request.htmx.target:
        html = tree.render_partial(request.htmx.target, **render_kwargs)
    else:
        html = tree.render(**render_kwargs)

    return HttpResponse(
        Template(html).render(
            RequestContext(request, kwargs),
        ),
    )

ChartViewMixin

Bases: BaseView

A view mixin designed to work seamlessly with the Chart component. It returns a JsonResponse of chart data if the request accepts application/json. Otherwise, it falls back to rendering the standard BaseView HTML component logic.

Source code in lariv/mixins.py
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
class ChartViewMixin(BaseView):
    """
    A view mixin designed to work seamlessly with the Chart component.
    It returns a JsonResponse of chart data if the request accepts application/json.
    Otherwise, it falls back to rendering the standard BaseView HTML component logic.
    """

    def get_chart_data(self, request, **kwargs):
        """
        Override this method to return the dataset for the chart.
        Must return a dict compatible with ApexCharts options.
        e.g., {'series': [{'name': 'Data', 'data': [1,2,3]}], 'xaxis': {'categories': ['A','B','C']}}
        """
        return {}

    def get(self, request, *args, **kwargs):
        # Check if the request explicitly asks for JSON (e.g., from the Chart JS fetch)
        accept_header = request.headers.get("Accept", "")
        if "application/json" in accept_header:
            from django.http import JsonResponse

            data = self.get_chart_data(request, **kwargs)
            return JsonResponse(data)

        # Fallback to the standard HTML component rendering
        return super().get(request, *args, **kwargs)

get_chart_data(request, **kwargs)

Override this method to return the dataset for the chart. Must return a dict compatible with ApexCharts options. e.g., {'series': [{'name': 'Data', 'data': [1,2,3]}], 'xaxis': {'categories': ['A','B','C']}}

Source code in lariv/mixins.py
602
603
604
605
606
607
608
def get_chart_data(self, request, **kwargs):
    """
    Override this method to return the dataset for the chart.
    Must return a dict compatible with ApexCharts options.
    e.g., {'series': [{'name': 'Data', 'data': [1,2,3]}], 'xaxis': {'categories': ['A','B','C']}}
    """
    return {}

DeleteViewMixin

Bases: LarivHtmxMixin, BaseView

Mixin for handling dynamic database record deletion operations, gracefully handling HTMX redirects on success.

Source code in lariv/mixins.py
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
class DeleteViewMixin(LarivHtmxMixin, BaseView):
    """
    Mixin for handling dynamic database record deletion operations, gracefully
    handling HTMX redirects on success.
    """

    def get_object(self, pk):
        return self.get_queryset().get(id=pk)

    def get_success_url(self):
        return getattr(self, "success_url", "/")

    def prepare_data(self, request, **kwargs):
        pk = kwargs["pk"]
        return {
            self.get_key(): self.get_object(pk),
        }

    def post(self, request, *args, **kwargs):
        pk = kwargs["pk"]
        obj = self.get_object(pk)
        obj.delete()

        success_url = self.get_success_url()
        if request.htmx:
            return HttpResponse(
                status=200,
                headers={"HX-Redirect": success_url},
            )
        from django.shortcuts import redirect

        return redirect(success_url)

DetailViewMixin

Bases: LarivHtmxMixin, BaseView

Mixin for detail/read views rendering a single database record into a component.

Source code in lariv/mixins.py
307
308
309
310
311
312
313
314
315
316
class DetailViewMixin(LarivHtmxMixin, BaseView):
    """
    Mixin for detail/read views rendering a single database record into a component.
    """
    def prepare_data(self, request, **kwargs):
        pk = kwargs["pk"]

        return {
            self.get_key(): self.get_queryset().get(id=pk),
        }

EnvironmentMixin

Mixin that integrates Environment into views. - Filters querysets based on environment session values - Pre-fills forms with environment values - Marks filterset fields as disabled for environment fields

Source code in lariv/mixins.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
class EnvironmentMixin:
    """
    Mixin that integrates Environment into views.
    - Filters querysets based on environment session values
    - Pre-fills forms with environment values
    - Marks filterset fields as disabled for environment fields
    """

    def get_environment_class(self):
        """Get the Environment class for the current app."""
        if hasattr(self, "environment_class"):
            return self.environment_class

        # Auto-detect from app_name
        app_name = getattr(self.request.resolver_match, "app_name", None)
        if app_name:
            try:
                return EnvironmentRegistry.get_for_app(app_name)
            except Exception:
                return None
        return None

    def get_environment(self):
        """Get an instance of the Environment for this view."""
        env_class = self.get_environment_class()
        if env_class:
            return env_class(self.request)
        return None

    def get_environment_filter_kwargs(self):
        """Get filter kwargs from environment."""
        env = self.get_environment()
        if env:
            return env.get_filter_kwargs()
        return {}

    def get_queryset(self):
        """Filter queryset based on environment values."""
        qs = super().get_queryset()
        filter_kwargs = self.get_environment_filter_kwargs()

        # Only apply filters for fields that exist on the model
        model = getattr(self, "model", None)
        if model and filter_kwargs:
            valid_kwargs = {}
            for key, value in filter_kwargs.items():
                # Check if the field exists on the model
                try:
                    model._meta.get_field(key)
                    valid_kwargs[key] = value
                except Exception:
                    pass
            if valid_kwargs:
                qs = qs.filter(**valid_kwargs)

        return qs

    def get_filterset(self, *args, **kwargs):
        """Mark environment fields as disabled in the filterset."""
        filterset = super().get_filterset(*args, **kwargs)

        if filterset is None:
            return None

        env = self.get_environment()
        if env:
            env_field_names = list(env.get_fields().keys())
            for field_name in env_field_names:
                if field_name in filterset.form.fields:
                    field = filterset.form.fields[field_name]
                    field.disabled = True
                    # Set the value from environment
                    env_values = env.get_field_values()
                    if field_name in env_values and env_values[field_name]:
                        filterset.form.initial[field_name] = env_values[field_name]

        return filterset

    def get_form(self, form_class=None):
        """Pre-fill form with environment values."""
        form = super().get_form(form_class)

        env = self.get_environment()
        if env:
            env_initial = env.get_form_initial()
            for field_name, value in env_initial.items():
                if field_name in form.fields:
                    form.fields[field_name].initial = value
                    # For create views, also set as disabled since it's from environment
                    if not getattr(self, "object", None):
                        form.fields[field_name].disabled = True

        return form

    def get_initial(self):
        """Include environment values in form initial data."""
        initial = super().get_initial()

        env = self.get_environment()
        if env:
            env_initial = env.get_form_initial()
            initial.update(env_initial)

        return initial

get_environment()

Get an instance of the Environment for this view.

Source code in lariv/mixins.py
104
105
106
107
108
109
def get_environment(self):
    """Get an instance of the Environment for this view."""
    env_class = self.get_environment_class()
    if env_class:
        return env_class(self.request)
    return None

get_environment_class()

Get the Environment class for the current app.

Source code in lariv/mixins.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def get_environment_class(self):
    """Get the Environment class for the current app."""
    if hasattr(self, "environment_class"):
        return self.environment_class

    # Auto-detect from app_name
    app_name = getattr(self.request.resolver_match, "app_name", None)
    if app_name:
        try:
            return EnvironmentRegistry.get_for_app(app_name)
        except Exception:
            return None
    return None

get_environment_filter_kwargs()

Get filter kwargs from environment.

Source code in lariv/mixins.py
111
112
113
114
115
116
def get_environment_filter_kwargs(self):
    """Get filter kwargs from environment."""
    env = self.get_environment()
    if env:
        return env.get_filter_kwargs()
    return {}

get_filterset(*args, **kwargs)

Mark environment fields as disabled in the filterset.

Source code in lariv/mixins.py
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def get_filterset(self, *args, **kwargs):
    """Mark environment fields as disabled in the filterset."""
    filterset = super().get_filterset(*args, **kwargs)

    if filterset is None:
        return None

    env = self.get_environment()
    if env:
        env_field_names = list(env.get_fields().keys())
        for field_name in env_field_names:
            if field_name in filterset.form.fields:
                field = filterset.form.fields[field_name]
                field.disabled = True
                # Set the value from environment
                env_values = env.get_field_values()
                if field_name in env_values and env_values[field_name]:
                    filterset.form.initial[field_name] = env_values[field_name]

    return filterset

get_form(form_class=None)

Pre-fill form with environment values.

Source code in lariv/mixins.py
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def get_form(self, form_class=None):
    """Pre-fill form with environment values."""
    form = super().get_form(form_class)

    env = self.get_environment()
    if env:
        env_initial = env.get_form_initial()
        for field_name, value in env_initial.items():
            if field_name in form.fields:
                form.fields[field_name].initial = value
                # For create views, also set as disabled since it's from environment
                if not getattr(self, "object", None):
                    form.fields[field_name].disabled = True

    return form

get_initial()

Include environment values in form initial data.

Source code in lariv/mixins.py
176
177
178
179
180
181
182
183
184
185
def get_initial(self):
    """Include environment values in form initial data."""
    initial = super().get_initial()

    env = self.get_environment()
    if env:
        env_initial = env.get_form_initial()
        initial.update(env_initial)

    return initial

get_queryset()

Filter queryset based on environment values.

Source code in lariv/mixins.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
def get_queryset(self):
    """Filter queryset based on environment values."""
    qs = super().get_queryset()
    filter_kwargs = self.get_environment_filter_kwargs()

    # Only apply filters for fields that exist on the model
    model = getattr(self, "model", None)
    if model and filter_kwargs:
        valid_kwargs = {}
        for key, value in filter_kwargs.items():
            # Check if the field exists on the model
            try:
                model._meta.get_field(key)
                valid_kwargs[key] = value
            except Exception:
                pass
        if valid_kwargs:
            qs = qs.filter(**valid_kwargs)

    return qs

FormDataWrapper

Wrapper to prioritize submitted form data over database instance data.

Source code in lariv/mixins.py
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
class FormDataWrapper:
    """Wrapper to prioritize submitted form data over database instance data."""
    def __init__(self, instance, data):
        self._instance = instance
        self._data = data

    def __getattr__(self, name):
        if name in self._data:
            return self._data[name]
        if self._instance:
            return getattr(self._instance, name)
        raise AttributeError(name)

    @property
    def pk(self):
        return self._instance.pk if self._instance else None

    @property
    def id(self):
        return self._instance.id if self._instance else None

    def __bool__(self):
        return bool(self._instance)

    def __str__(self):
        return str(self._instance) if self._instance else ""

FormViewMixin

Bases: LarivHtmxMixin, BaseView

Mixin abstracting HTML form operations dynamically based on instantiated Python Input components defined in the UI tree.

Source code in lariv/mixins.py
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
class FormViewMixin(LarivHtmxMixin, BaseView):
    """
    Mixin abstracting HTML form operations dynamically based on instantiated
    Python Input components defined in the UI tree.
    """
    def get_form_component(self):
        component_cls = UIRegistry.get(self.get_component(None))
        tree = component_cls().build()
        return self._find_form_component(tree)

    def _find_form_component(self, component):
        if isinstance(component, Form):
            if component.key == self.get_key():
                return component
        for child in component.children:
            found = self._find_form_component(child)
            if found:
                return found
        return None

    def get_inputs(self):
        form = self.get_form_component()
        if not form:
            return []
        return form._get_all_inputs(form.children)

    def get_success_url(self, obj):
        success_url = getattr(self, "success_url", None)
        if callable(success_url):
            return success_url(obj)
        return success_url

    def get_object(self, pk):
        if pk is None:
            return None
        return self.get_queryset().get(id=pk)

    def validate(self, data, inputs, instance=None):
        """
        Validate form data based on Input components.
        Returns (cleaned_data, errors) tuple.
        """
        errors = {}
        cleaned_data = {}

        for input_comp in inputs:
            field = input_comp.key
            value = data.get(field)
            # Gracefully catch fields that don't exist on the model
            try:
                model_field = self.model._meta.get_field(field) if self.model else None
            except FieldDoesNotExist:
                model_field = None

            # Check required fields for other input types
            if input_comp.required and not value:
                errors[field] = "This field is required."
            elif (
                not value
                and model_field
                and not isinstance(model_field, models.ManyToManyRel)
                and not model_field.blank
                and not model_field.null
            ):
                errors[field] = "This field is required."
            try:
                # Delegate format assertions and sanitization to the input component
                value = input_comp.clean(value)
            except ValueError as e:
                errors[field] = str(e)
                continue
            cleaned_data[field] = value

        return cleaned_data, errors

    def prepare_data(self, request, **kwargs):
        pk = kwargs.get("pk")
        obj = self.get_object(pk)

        return {
            self.get_key(): obj,
        }

validate(data, inputs, instance=None)

Validate form data based on Input components. Returns (cleaned_data, errors) tuple.

Source code in lariv/mixins.py
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
def validate(self, data, inputs, instance=None):
    """
    Validate form data based on Input components.
    Returns (cleaned_data, errors) tuple.
    """
    errors = {}
    cleaned_data = {}

    for input_comp in inputs:
        field = input_comp.key
        value = data.get(field)
        # Gracefully catch fields that don't exist on the model
        try:
            model_field = self.model._meta.get_field(field) if self.model else None
        except FieldDoesNotExist:
            model_field = None

        # Check required fields for other input types
        if input_comp.required and not value:
            errors[field] = "This field is required."
        elif (
            not value
            and model_field
            and not isinstance(model_field, models.ManyToManyRel)
            and not model_field.blank
            and not model_field.null
        ):
            errors[field] = "This field is required."
        try:
            # Delegate format assertions and sanitization to the input component
            value = input_comp.clean(value)
        except ValueError as e:
            errors[field] = str(e)
            continue
        cleaned_data[field] = value

    return cleaned_data, errors

LarivHtmxMixin

Bases: EnvironmentMixin

Mixin to handle HTMX partial rendering and Environment integration. If the request is an HTMX request and has a target, it attempts to render the partial matching the target ID.

Source code in lariv/mixins.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
class LarivHtmxMixin(EnvironmentMixin):
    """
    Mixin to handle HTMX partial rendering and Environment integration.
    If the request is an HTMX request and has a target, it attempts to render
    the partial matching the target ID.
    """

    def get_template_names(self):
        templates = super().get_template_names()
        if self.request.htmx and self.request.htmx.target:
            # Append the target as the partial name
            # e.g. "batch_list.html" -> "batch_list.html#batches-list"
            return [f"{t}#{self.request.htmx.target}" for t in templates]
        return templates

ListViewMixin

Bases: LarivHtmxMixin, BaseView

Mixin for index and list views. Handles basic filtering, sorting, and pagination of QuerySets out-of-the-box.

Source code in lariv/mixins.py
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
class ListViewMixin(LarivHtmxMixin, BaseView):
    """
    Mixin for index and list views. 
    Handles basic filtering, sorting, and pagination of QuerySets out-of-the-box.
    """
    def get_paginate_by(self, request):
        return getattr(self, "paginate_by", 12)

    def prepare_data(self, request, **kwargs):
        queryset = self.get_queryset()
        get_params = request.GET.dict()

        page_number = get_params.pop("page", 1)
        sort = get_params.pop("sort", None)
        if sort is not None:
            queryset = queryset.order_by(sort)

        queryset = apply_filters(queryset, get_params, self.model)

        paginator = Paginator(queryset, self.get_paginate_by(request))

        page = paginator.page(page_number)

        return {self.get_key(): page}

SelectionTableViewMixin

Bases: BaseView

Mixin for serving selection tables in modals for foreign key and many-to-many inputs.

Attributes:

Name Type Description
model

The Django model to select from

component

The table component name (should use Table with select="single" or select="multi")

key

Key to pass the queryset to the component

paginate_by

Number of items per page (default 6)

Source code in lariv/mixins.py
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
class SelectionTableViewMixin(BaseView):
    """
    Mixin for serving selection tables in modals for foreign key and many-to-many inputs.

    Attributes:
        model: The Django model to select from
        component: The table component name (should use Table with select="single" or select="multi")
        key: Key to pass the queryset to the component
        paginate_by: Number of items per page (default 6)
    """

    paginate_by = 6

    def get_paginate_by(self, request):
        return getattr(self, "paginate_by", 6)

    def prepare_data(self, request, **kwargs):
        queryset = self.get_queryset()
        get_params = request.GET.dict()

        # Extract pagination and modal params
        page_number = get_params.pop("page", 1)
        target_input = get_params.pop("target_input", "")
        modal_id = get_params.pop("modal_id", "")

        # Apply filters from remaining params
        queryset = apply_filters(queryset, get_params, self.model)

        paginator = Paginator(queryset, self.get_paginate_by(request))
        page = paginator.page(page_number)

        return {
            self.get_key(): page,
            "target_input": target_input,
            "modal_id": modal_id,
        }

apply_filters(queryset, filters, model)

Apply filters to a queryset, handling different field types appropriately.

  • Boolean fields: exact match (True/False)
  • Foreign keys: exact match on ID
  • Text fields: icontains lookup
  • Other fields: exact match
Source code in lariv/mixins.py
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
def apply_filters(queryset, filters, model):
    """
    Apply filters to a queryset, handling different field types appropriately.

    - Boolean fields: exact match (True/False)
    - Foreign keys: exact match on ID
    - Text fields: icontains lookup
    - Other fields: exact match
    """
    for field_name, value in filters.items():
        if not value and value != 0 and value is not False:
            continue

        # Get the model field to determine its type
        try:
            field = resolve_field_path(model, field_name)
        except Exception:
            # Field doesn't exist on model, skip
            continue

        # Handle boolean fields
        if (
            hasattr(field, "get_internal_type")
            and field.get_internal_type() == "BooleanField"
        ):
            if value in ("True", "true", "1", True):
                queryset = queryset.filter(**{field_name: True})
            elif value in ("False", "false", "0", False):
                queryset = queryset.filter(**{field_name: False})
            # Skip if empty string or None (show all)
            continue

        # Handle foreign key fields
        if (
            hasattr(field, "get_internal_type")
            and field.get_internal_type() == "ForeignKey"
        ):
            f_keys = [int(v.strip()) for v in value.split(",")]
            try:
                queryset = queryset.filter(**{f"{field_name}__in": f_keys})
            except (ValueError, TypeError):
                pass
            continue

        # Handle text/char fields with icontains
        if hasattr(field, "get_internal_type") and field.get_internal_type() in (
            "CharField",
            "TextField",
        ):
            queryset = queryset.filter(**{f"{field_name}__icontains": value})
            continue

        # Default: exact match
        queryset = queryset.filter(**{field_name: value})

    return queryset

resolve_field_path(model, path)

Recursively resolves a dunder path (e.g., 'user__name') to its field object.

Source code in lariv/mixins.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
def resolve_field_path(model, path):
    """
    Recursively resolves a dunder path (e.g., 'user__name') to its field object.
    """
    parts = path.split("__")
    current_model = model

    for i, part in enumerate(parts):
        field = current_model._meta.get_field(part)

        # If there are more parts, we must be looking at a relationship
        if i < len(parts) - 1:
            if not field.is_relation:
                raise FieldDoesNotExist(f"{part} is not a relationship.")
            current_model = field.related_model
        else:
            return field