|
1 | 1 | from django import forms |
| 2 | +from django.conf import settings |
2 | 3 | from django.db import models |
3 | 4 | from django.http import QueryDict |
4 | 5 | from django.template import Context |
|
14 | 15 | from utilities.forms.mixins import FilterModifierMixin |
15 | 16 | from utilities.forms.widgets import FilterModifierWidget |
16 | 17 | from utilities.templatetags.helpers import applied_filters |
| 18 | +from tenancy.models import Tenant |
17 | 19 |
|
18 | 20 |
|
19 | 21 | # Test model for FilterModifierMixin tests |
@@ -99,6 +101,51 @@ def test_get_context_includes_original_widget_and_lookups(self): |
99 | 101 | self.assertEqual(context['widget']['current_modifier'], 'exact') # Defaults to exact, JS updates from URL |
100 | 102 | self.assertEqual(context['widget']['current_value'], 'test') |
101 | 103 |
|
| 104 | + def test_get_context_handles_null_selection(self): |
| 105 | + """Widget should preserve the 'null' choice when rendering.""" |
| 106 | + |
| 107 | + null_value = settings.FILTERS_NULL_CHOICE_VALUE |
| 108 | + null_label = settings.FILTERS_NULL_CHOICE_LABEL |
| 109 | + |
| 110 | + # Simulate a query for objects with no tenant assigned (?tenant_id=null) |
| 111 | + query_params = QueryDict(f'tenant_id={null_value}') |
| 112 | + form = DeviceFilterForm(query_params) |
| 113 | + |
| 114 | + # Rendering the field triggers FilterModifierWidget.get_context() |
| 115 | + try: |
| 116 | + html = form['tenant_id'].as_widget() |
| 117 | + except ValueError as e: |
| 118 | + # ValueError: Field 'id' expected a number but got 'null' |
| 119 | + self.fail(f"FilterModifierWidget raised ValueError on 'null' selection: {e}") |
| 120 | + |
| 121 | + # Verify the "None" option is rendered so user selection is preserved in the UI |
| 122 | + self.assertIn(f'value="{null_value}"', html) |
| 123 | + self.assertIn(null_label, html) |
| 124 | + |
| 125 | + def test_get_context_handles_mixed_selection(self): |
| 126 | + """Widget should preserve both real objects and the 'null' choice together.""" |
| 127 | + |
| 128 | + null_value = settings.FILTERS_NULL_CHOICE_VALUE |
| 129 | + |
| 130 | + # Create a tenant to simulate a real object |
| 131 | + tenant = Tenant.objects.create(name='Tenant A', slug='tenant-a') |
| 132 | + |
| 133 | + # Simulate a selection containing both a real PK and the null sentinel |
| 134 | + query_params = QueryDict('', mutable=True) |
| 135 | + query_params.setlist('tenant_id', [str(tenant.pk), null_value]) |
| 136 | + form = DeviceFilterForm(query_params) |
| 137 | + |
| 138 | + # Rendering the field triggers FilterModifierWidget.get_context() |
| 139 | + try: |
| 140 | + html = form['tenant_id'].as_widget() |
| 141 | + except ValueError as e: |
| 142 | + # ValueError: Field 'id' expected a number but got 'null' |
| 143 | + self.fail(f"FilterModifierWidget raised ValueError on 'null' selection: {e}") |
| 144 | + |
| 145 | + # Verify both the real object and the null option are present in the output |
| 146 | + self.assertIn(f'value="{tenant.pk}"', html) |
| 147 | + self.assertIn(f'value="{null_value}"', html) |
| 148 | + |
102 | 149 | def test_widget_renders_modifier_dropdown_and_input(self): |
103 | 150 | """Widget should render modifier dropdown alongside original input.""" |
104 | 151 | widget = FilterModifierWidget( |
|
0 commit comments