select.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. from django import forms
  2. from netbox.choices import ColorChoices
  3. from ..utils import add_blank_choice
  4. __all__ = (
  5. 'BulkEditNullBooleanSelect',
  6. 'ClearableSelect',
  7. 'ColorSelect',
  8. 'HTMXSelect',
  9. 'SelectWithPK',
  10. 'SplitMultiSelectWidget',
  11. )
  12. class BulkEditNullBooleanSelect(forms.NullBooleanSelect):
  13. """
  14. A Select widget for NullBooleanFields
  15. """
  16. def __init__(self, *args, **kwargs):
  17. super().__init__(*args, **kwargs)
  18. # Override the built-in choice labels
  19. self.choices = (
  20. ('1', '---------'),
  21. ('2', 'Yes'),
  22. ('3', 'No'),
  23. )
  24. class ClearableSelect(forms.Select):
  25. """
  26. A Select widget that will be automatically cleared when one or more required fields are cleared.
  27. Args:
  28. requires_fields: A list of field names that this field depends on. When any of these fields
  29. are cleared, this field will also be cleared automatically via JavaScript.
  30. """
  31. def __init__(self, *args, requires_fields=None, **kwargs):
  32. super().__init__(*args, **kwargs)
  33. if requires_fields:
  34. self.attrs['data-requires-fields'] = ','.join(requires_fields)
  35. class ColorSelect(forms.Select):
  36. """
  37. Extends the built-in Select widget to colorize each <option>.
  38. """
  39. option_template_name = 'widgets/colorselect_option.html'
  40. def __init__(self, *args, **kwargs):
  41. kwargs['choices'] = add_blank_choice(ColorChoices)
  42. super().__init__(*args, **kwargs)
  43. self.attrs['class'] = 'color-select'
  44. class HTMXSelect(forms.Select):
  45. """
  46. Selection widget that will re-generate the HTML form upon the selection of a new option.
  47. """
  48. def __init__(self, method='get', hx_url='.', hx_target_id='form_fields', attrs=None, **kwargs):
  49. method = method.lower()
  50. if method not in ('delete', 'get', 'patch', 'post', 'put'):
  51. raise ValueError(f"Unsupported HTTP method: {method}")
  52. _attrs = {
  53. f'hx-{method}': hx_url,
  54. 'hx-include': f'#{hx_target_id}',
  55. 'hx-target': f'#{hx_target_id}',
  56. }
  57. if attrs:
  58. _attrs.update(attrs)
  59. super().__init__(attrs=_attrs, **kwargs)
  60. class SelectWithPK(forms.Select):
  61. """
  62. Include the primary key of each option in the option label (e.g. "Router7 (4721)").
  63. """
  64. option_template_name = 'widgets/select_option_with_pk.html'
  65. class SelectMultipleBase(forms.SelectMultiple):
  66. """
  67. Base class for select widgets that filter choices based on selected values.
  68. Subclasses should set `include_selected` to control filtering behavior.
  69. """
  70. include_selected = False
  71. def optgroups(self, name, value, attrs=None):
  72. filtered_choices = []
  73. include_selected = self.include_selected
  74. for choice in self.choices:
  75. if isinstance(choice[1], (list, tuple)): # optgroup
  76. group_label, group_choices = choice
  77. filtered_group = [
  78. c for c in group_choices if (str(c[0]) in value) == include_selected
  79. ]
  80. if filtered_group: # Only include optgroup if it has choices left
  81. filtered_choices.append((group_label, filtered_group))
  82. else: # option, e.g. flat choice
  83. if (str(choice[0]) in value) == include_selected:
  84. filtered_choices.append(choice)
  85. self.choices = filtered_choices
  86. value = [] # Clear selected choices
  87. return super().optgroups(name, value, attrs)
  88. def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
  89. option = super().create_option(name, value, label, selected, index, subindex, attrs)
  90. option['attrs']['title'] = label # Add title attribute to show full text on hover
  91. return option
  92. class AvailableOptions(SelectMultipleBase):
  93. """
  94. Renders a <select multiple=true> including only choices that have been selected. (For unbound fields, this list
  95. will be empty.) Employed by SplitMultiSelectWidget.
  96. """
  97. def get_context(self, name, value, attrs):
  98. context = super().get_context(name, value, attrs)
  99. # This widget should never require a selection
  100. context['widget']['attrs']['required'] = False
  101. return context
  102. class SelectedOptions(SelectMultipleBase):
  103. """
  104. Renders a <select multiple=true> including only choices that have _not_ been selected. (For unbound fields, this
  105. will include _all_ choices.) Employed by SplitMultiSelectWidget.
  106. """
  107. include_selected = True
  108. class SplitMultiSelectWidget(forms.MultiWidget):
  109. """
  110. Renders two <select multiple=true> widgets side-by-side: one listing available choices, the other listing selected
  111. choices. Options are selected by moving them from the left column to the right.
  112. Args:
  113. ordering: If true, the selected choices list will include controls to reorder items within the list. This should
  114. be enabled only if the order of the selected choices is significant.
  115. """
  116. template_name = 'widgets/splitmultiselect.html'
  117. def __init__(self, choices, attrs=None, ordering=False):
  118. widgets = [
  119. AvailableOptions(
  120. attrs={'size': 8},
  121. choices=choices
  122. ),
  123. SelectedOptions(
  124. attrs={'size': 8, 'class': 'select-all'},
  125. choices=choices
  126. ),
  127. ]
  128. super().__init__(widgets, attrs)
  129. self.ordering = ordering
  130. def get_context(self, name, value, attrs):
  131. # Replicate value for each multi-select widget
  132. # Django bug? See django/forms/widgets.py L985
  133. value = [value, value]
  134. # Include ordering boolean in widget context
  135. context = super().get_context(name, value, attrs)
  136. context['widget']['ordering'] = self.ordering
  137. return context
  138. def value_from_datadict(self, data, files, name):
  139. # Return only the choices from the SelectedOptions widget
  140. return super().value_from_datadict(data, files, name)[1]