widgets.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import json
  2. from django import forms
  3. from django.conf import settings
  4. from django.contrib.postgres.forms import SimpleArrayField
  5. from utilities.choices import ColorChoices
  6. from .utils import add_blank_choice, parse_numeric_range
  7. __all__ = (
  8. 'APISelect',
  9. 'APISelectMultiple',
  10. 'BulkEditNullBooleanSelect',
  11. 'ColorSelect',
  12. 'ContentTypeSelect',
  13. 'DatePicker',
  14. 'DateTimePicker',
  15. 'NumericArrayField',
  16. 'SelectWithDisabled',
  17. 'SelectWithPK',
  18. 'SlugWidget',
  19. 'SmallTextarea',
  20. 'StaticSelect2',
  21. 'StaticSelect2Multiple',
  22. 'TimePicker',
  23. )
  24. class SmallTextarea(forms.Textarea):
  25. """
  26. Subclass used for rendering a smaller textarea element.
  27. """
  28. pass
  29. class SlugWidget(forms.TextInput):
  30. """
  31. Subclass TextInput and add a slug regeneration button next to the form field.
  32. """
  33. template_name = 'widgets/sluginput.html'
  34. class ColorSelect(forms.Select):
  35. """
  36. Extends the built-in Select widget to colorize each <option>.
  37. """
  38. option_template_name = 'widgets/colorselect_option.html'
  39. def __init__(self, *args, **kwargs):
  40. kwargs['choices'] = add_blank_choice(ColorChoices)
  41. super().__init__(*args, **kwargs)
  42. self.attrs['class'] = 'netbox-select2-color-picker'
  43. class BulkEditNullBooleanSelect(forms.NullBooleanSelect):
  44. """
  45. A Select widget for NullBooleanFields
  46. """
  47. def __init__(self, *args, **kwargs):
  48. super().__init__(*args, **kwargs)
  49. # Override the built-in choice labels
  50. self.choices = (
  51. ('1', '---------'),
  52. ('2', 'Yes'),
  53. ('3', 'No'),
  54. )
  55. self.attrs['class'] = 'netbox-select2-static'
  56. class SelectWithDisabled(forms.Select):
  57. """
  58. Modified the stock Select widget to accept choices using a dict() for a label. The dict for each option must include
  59. 'label' (string) and 'disabled' (boolean).
  60. """
  61. option_template_name = 'widgets/selectwithdisabled_option.html'
  62. class StaticSelect2(SelectWithDisabled):
  63. """
  64. A static content using the Select2 widget
  65. :param filter_for: (Optional) A dict of chained form fields for which this field is a filter. The key is the
  66. name of the filter-for field (child field) and the value is the name of the query param filter.
  67. """
  68. def __init__(self, filter_for=None, *args, **kwargs):
  69. super().__init__(*args, **kwargs)
  70. self.attrs['class'] = 'netbox-select2-static'
  71. if filter_for:
  72. for key, value in filter_for.items():
  73. self.add_filter_for(key, value)
  74. def add_filter_for(self, name, value):
  75. """
  76. Add details for an additional query param in the form of a data-filter-for-* attribute.
  77. :param name: The name of the query param
  78. :param value: The value of the query param
  79. """
  80. self.attrs['data-filter-for-{}'.format(name)] = value
  81. class StaticSelect2Multiple(StaticSelect2, forms.SelectMultiple):
  82. def __init__(self, *args, **kwargs):
  83. super().__init__(*args, **kwargs)
  84. self.attrs['data-multiple'] = 1
  85. class SelectWithPK(StaticSelect2):
  86. """
  87. Include the primary key of each option in the option label (e.g. "Router7 (4721)").
  88. """
  89. option_template_name = 'widgets/select_option_with_pk.html'
  90. class ContentTypeSelect(StaticSelect2):
  91. """
  92. Appends an `api-value` attribute equal to the slugified model name for each ContentType. For example:
  93. <option value="37" api-value="console-server-port">console server port</option>
  94. This attribute can be used to reference the relevant API endpoint for a particular ContentType.
  95. """
  96. option_template_name = 'widgets/select_contenttype.html'
  97. class NumericArrayField(SimpleArrayField):
  98. def to_python(self, value):
  99. value = ','.join([str(n) for n in parse_numeric_range(value)])
  100. return super().to_python(value)
  101. class APISelect(SelectWithDisabled):
  102. """
  103. A select widget populated via an API call
  104. :param api_url: API endpoint URL. Required if not set automatically by the parent field.
  105. :param display_field: (Optional) Field to display for child in selection list. Defaults to `name`.
  106. :param disabled_indicator: (Optional) Mark option as disabled if this field equates true.
  107. :param filter_for: (Optional) A dict of chained form fields for which this field is a filter. The key is the
  108. name of the filter-for field (child field) and the value is the name of the query param filter.
  109. :param additional_query_params: Optional) A dict of query params to append to the API request. The key is the
  110. name of the query param and the value if the query param's value.
  111. """
  112. def __init__(
  113. self,
  114. api_url=None,
  115. display_field=None,
  116. disabled_indicator=None,
  117. filter_for=None,
  118. additional_query_params=None,
  119. full=False,
  120. *args,
  121. **kwargs
  122. ):
  123. super().__init__(*args, **kwargs)
  124. self.attrs['class'] = 'netbox-select2-api'
  125. if api_url:
  126. self.attrs['data-url'] = '/{}{}'.format(settings.BASE_PATH, api_url.lstrip('/')) # Inject BASE_PATH
  127. if full:
  128. self.attrs['data-full'] = full
  129. if display_field:
  130. self.attrs['display-field'] = display_field
  131. if disabled_indicator:
  132. self.attrs['disabled-indicator'] = disabled_indicator
  133. if filter_for:
  134. for key, value in filter_for.items():
  135. self.add_filter_for(key, value)
  136. if additional_query_params:
  137. for key, value in additional_query_params.items():
  138. self.add_additional_query_param(key, value)
  139. def add_filter_for(self, name, value):
  140. """
  141. Add details for an additional query param in the form of a data-filter-for-* attribute.
  142. :param name: The name of the query param
  143. :param value: The value of the query param
  144. """
  145. self.attrs['data-filter-for-{}'.format(name)] = value
  146. def add_additional_query_param(self, name, value):
  147. """
  148. Add details for an additional query param in the form of a data-* JSON-encoded list attribute.
  149. :param name: The name of the query param
  150. :param value: The value of the query param
  151. """
  152. key = 'data-additional-query-param-{}'.format(name)
  153. values = json.loads(self.attrs.get(key, '[]'))
  154. values.append(value)
  155. self.attrs[key] = json.dumps(values)
  156. class APISelectMultiple(APISelect, forms.SelectMultiple):
  157. def __init__(self, *args, **kwargs):
  158. super().__init__(*args, **kwargs)
  159. self.attrs['data-multiple'] = 1
  160. class DatePicker(forms.TextInput):
  161. """
  162. Date picker using Flatpickr.
  163. """
  164. def __init__(self, *args, **kwargs):
  165. super().__init__(*args, **kwargs)
  166. self.attrs['class'] = 'date-picker'
  167. self.attrs['placeholder'] = 'YYYY-MM-DD'
  168. class DateTimePicker(forms.TextInput):
  169. """
  170. DateTime picker using Flatpickr.
  171. """
  172. def __init__(self, *args, **kwargs):
  173. super().__init__(*args, **kwargs)
  174. self.attrs['class'] = 'datetime-picker'
  175. self.attrs['placeholder'] = 'YYYY-MM-DD hh:mm:ss'
  176. class TimePicker(forms.TextInput):
  177. """
  178. Time picker using Flatpickr.
  179. """
  180. def __init__(self, *args, **kwargs):
  181. super().__init__(*args, **kwargs)
  182. self.attrs['class'] = 'time-picker'
  183. self.attrs['placeholder'] = 'hh:mm:ss'