base.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. from django import forms
  2. from django.contrib.contenttypes.models import ContentType
  3. from django.db.models import Q
  4. from django.utils.translation import gettext_lazy as _
  5. from core.models import ObjectType
  6. from extras.choices import *
  7. from extras.models import CustomField, Tag
  8. from utilities.forms import CSVModelForm
  9. from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField
  10. from utilities.forms.mixins import CheckLastUpdatedMixin
  11. from .mixins import CustomFieldsMixin, SavedFiltersMixin, TagsMixin
  12. __all__ = (
  13. 'NetBoxModelForm',
  14. 'NetBoxModelImportForm',
  15. 'NetBoxModelBulkEditForm',
  16. 'NetBoxModelFilterSetForm',
  17. )
  18. class NetBoxModelForm(CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms.ModelForm):
  19. """
  20. Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields.
  21. Attributes:
  22. fieldsets: An iterable of FieldSets which define a name and set of fields to display per section of
  23. the rendered form (optional). If not defined, the all fields will be rendered as a single section.
  24. """
  25. fieldsets = ()
  26. def _get_content_type(self):
  27. return ContentType.objects.get_for_model(self._meta.model)
  28. def _get_form_field(self, customfield):
  29. if self.instance.pk:
  30. form_field = customfield.to_form_field(set_initial=False)
  31. form_field.initial = self.instance.custom_field_data.get(customfield.name, None)
  32. return form_field
  33. return customfield.to_form_field()
  34. def clean(self):
  35. # Save custom field data on instance
  36. for cf_name, customfield in self.custom_fields.items():
  37. if cf_name not in self.fields:
  38. # Custom fields may be absent when performing bulk updates via import
  39. continue
  40. key = cf_name[3:] # Strip "cf_" from field name
  41. value = self.cleaned_data.get(cf_name)
  42. # Convert "empty" values to null
  43. if value in self.fields[cf_name].empty_values:
  44. self.instance.custom_field_data[key] = None
  45. else:
  46. self.instance.custom_field_data[key] = customfield.serialize(value)
  47. return super().clean()
  48. def _post_clean(self):
  49. """
  50. Override BaseModelForm's _post_clean() to store many-to-many field values on the model instance.
  51. """
  52. self.instance._m2m_values = {}
  53. for field in self.instance._meta.local_many_to_many:
  54. if field.name in self.cleaned_data:
  55. self.instance._m2m_values[field.name] = list(self.cleaned_data[field.name])
  56. return super()._post_clean()
  57. class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
  58. """
  59. Base form for creating a NetBox objects from CSV data. Used for bulk importing.
  60. """
  61. id = forms.IntegerField(
  62. label=_('Id'),
  63. required=False,
  64. help_text='Numeric ID of an existing object to update (if not creating a new object)'
  65. )
  66. tags = CSVModelMultipleChoiceField(
  67. label=_('Tags'),
  68. queryset=Tag.objects.all(),
  69. required=False,
  70. to_field_name='slug',
  71. help_text='Tag slugs separated by commas, encased with double quotes (e.g. "tag1,tag2,tag3")'
  72. )
  73. def _get_custom_fields(self, content_type):
  74. return CustomField.objects.filter(
  75. object_types=content_type,
  76. ui_editable=CustomFieldUIEditableChoices.YES
  77. )
  78. def _get_form_field(self, customfield):
  79. return customfield.to_form_field(for_csv_import=True)
  80. class NetBoxModelBulkEditForm(CustomFieldsMixin, forms.Form):
  81. """
  82. Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom
  83. fields and adding/removing tags.
  84. Attributes:
  85. fieldsets: An iterable of two-tuples which define a heading and field set to display per section of
  86. the rendered form (optional). If not defined, the all fields will be rendered as a single section.
  87. nullable_fields: A list of field names indicating which fields support being set to null/empty
  88. """
  89. nullable_fields = ()
  90. pk = forms.ModelMultipleChoiceField(
  91. queryset=None, # Set from self.model on init
  92. widget=forms.MultipleHiddenInput
  93. )
  94. add_tags = DynamicModelMultipleChoiceField(
  95. label=_('Add tags'),
  96. queryset=Tag.objects.all(),
  97. required=False
  98. )
  99. remove_tags = DynamicModelMultipleChoiceField(
  100. label=_('Remove tags'),
  101. queryset=Tag.objects.all(),
  102. required=False
  103. )
  104. def __init__(self, *args, **kwargs):
  105. super().__init__(*args, **kwargs)
  106. self.fields['pk'].queryset = self.model.objects.all()
  107. # Restrict tag fields by model
  108. object_type = ObjectType.objects.get_for_model(self.model)
  109. self.fields['add_tags'].widget.add_query_param('for_object_type_id', object_type.pk)
  110. self.fields['remove_tags'].widget.add_query_param('for_object_type_id', object_type.pk)
  111. self._extend_nullable_fields()
  112. def _get_form_field(self, customfield):
  113. return customfield.to_form_field(set_initial=False, enforce_required=False)
  114. def _extend_nullable_fields(self):
  115. nullable_custom_fields = [
  116. name for name, customfield in self.custom_fields.items()
  117. if (not customfield.required and customfield.ui_editable == CustomFieldUIEditableChoices.YES)
  118. ]
  119. self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields)
  120. class NetBoxModelFilterSetForm(CustomFieldsMixin, SavedFiltersMixin, forms.Form):
  121. """
  122. Base form for FilerSet forms. These are used to filter object lists in the NetBox UI. Note that the
  123. corresponding FilterSet *must* provide a `q` filter.
  124. Attributes:
  125. model: The model class associated with the form
  126. fieldsets: An iterable of two-tuples which define a heading and field set to display per section of
  127. the rendered form (optional). If not defined, the all fields will be rendered as a single section.
  128. selector_fields: An iterable of names of fields to display by default when rendering the form as
  129. a selector widget
  130. """
  131. q = forms.CharField(
  132. required=False,
  133. label=_('Search')
  134. )
  135. selector_fields = ('filter_id', 'q')
  136. def __init__(self, *args, **kwargs):
  137. super().__init__(*args, **kwargs)
  138. # Limit saved filters to those applicable to the form's model
  139. object_type = ObjectType.objects.get_for_model(self.model)
  140. self.fields['filter_id'].widget.add_query_params({
  141. 'object_type_id': object_type.pk,
  142. })
  143. def _get_custom_fields(self, content_type):
  144. return super()._get_custom_fields(content_type).exclude(
  145. Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |
  146. Q(type=CustomFieldTypeChoices.TYPE_JSON)
  147. )
  148. def _get_form_field(self, customfield):
  149. return customfield.to_form_field(set_initial=False, enforce_required=False, enforce_visibility=False)