mixins.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. from django import forms
  2. from django.contrib.contenttypes.models import ContentType
  3. from django.core.exceptions import ObjectDoesNotExist, ValidationError
  4. from django.db import connection
  5. from django.db.models.signals import post_save
  6. from django.utils.translation import gettext_lazy as _
  7. from dcim.constants import LOCATION_SCOPE_TYPES
  8. from dcim.models import PortMapping, PortTemplateMapping, Site
  9. from utilities.forms import get_field_value
  10. from utilities.forms.fields import (
  11. ContentTypeChoiceField, CSVContentTypeField, DynamicModelChoiceField,
  12. )
  13. from utilities.templatetags.builtins.filters import bettertitle
  14. from utilities.forms.widgets import HTMXSelect
  15. __all__ = (
  16. 'FrontPortFormMixin',
  17. 'ScopedBulkEditForm',
  18. 'ScopedForm',
  19. 'ScopedImportForm',
  20. )
  21. class ScopedForm(forms.Form):
  22. scope_type = ContentTypeChoiceField(
  23. queryset=ContentType.objects.filter(model__in=LOCATION_SCOPE_TYPES),
  24. widget=HTMXSelect(),
  25. required=False,
  26. label=_('Scope type')
  27. )
  28. scope = DynamicModelChoiceField(
  29. label=_('Scope'),
  30. queryset=Site.objects.none(), # Initial queryset
  31. required=False,
  32. disabled=True,
  33. selector=True
  34. )
  35. def __init__(self, *args, **kwargs):
  36. instance = kwargs.get('instance')
  37. initial = kwargs.get('initial', {})
  38. if instance is not None and instance.scope:
  39. initial['scope'] = instance.scope
  40. kwargs['initial'] = initial
  41. super().__init__(*args, **kwargs)
  42. self._set_scoped_values()
  43. def clean(self):
  44. super().clean()
  45. scope = self.cleaned_data.get('scope')
  46. scope_type = self.cleaned_data.get('scope_type')
  47. if scope_type and not scope:
  48. raise ValidationError({
  49. 'scope': _(
  50. "Please select a {scope_type}."
  51. ).format(scope_type=scope_type.model_class()._meta.model_name)
  52. })
  53. # Assign the selected scope (if any)
  54. self.instance.scope = scope
  55. def _set_scoped_values(self):
  56. if scope_type_id := get_field_value(self, 'scope_type'):
  57. try:
  58. scope_type = ContentType.objects.get(pk=scope_type_id)
  59. model = scope_type.model_class()
  60. self.fields['scope'].queryset = model.objects.all()
  61. self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower
  62. self.fields['scope'].disabled = False
  63. self.fields['scope'].label = _(bettertitle(model._meta.verbose_name))
  64. except ObjectDoesNotExist:
  65. pass
  66. if self.instance and scope_type_id != self.instance.scope_type_id:
  67. self.initial['scope'] = None
  68. else:
  69. # Clear the initial scope value if scope_type is not set
  70. self.initial['scope'] = None
  71. class ScopedBulkEditForm(forms.Form):
  72. scope_type = ContentTypeChoiceField(
  73. queryset=ContentType.objects.filter(model__in=LOCATION_SCOPE_TYPES),
  74. widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}),
  75. required=False,
  76. label=_('Scope type')
  77. )
  78. scope = DynamicModelChoiceField(
  79. label=_('Scope'),
  80. queryset=Site.objects.none(), # Initial queryset
  81. required=False,
  82. disabled=True,
  83. selector=True
  84. )
  85. def __init__(self, *args, **kwargs):
  86. super().__init__(*args, **kwargs)
  87. if scope_type_id := get_field_value(self, 'scope_type'):
  88. try:
  89. scope_type = ContentType.objects.get(pk=scope_type_id)
  90. model = scope_type.model_class()
  91. self.fields['scope'].queryset = model.objects.all()
  92. self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower
  93. self.fields['scope'].disabled = False
  94. self.fields['scope'].label = _(bettertitle(model._meta.verbose_name))
  95. except ObjectDoesNotExist:
  96. pass
  97. class ScopedImportForm(forms.Form):
  98. scope_type = CSVContentTypeField(
  99. queryset=ContentType.objects.filter(model__in=LOCATION_SCOPE_TYPES),
  100. required=False,
  101. label=_('Scope type (app & model)')
  102. )
  103. def clean(self):
  104. super().clean()
  105. scope_id = self.cleaned_data.get('scope_id')
  106. scope_type = self.cleaned_data.get('scope_type')
  107. if scope_type and not scope_id:
  108. raise ValidationError({
  109. 'scope_id': _(
  110. "Please select a {scope_type}."
  111. ).format(scope_type=scope_type.model_class()._meta.model_name)
  112. })
  113. class FrontPortFormMixin(forms.Form):
  114. rear_ports = forms.MultipleChoiceField(
  115. choices=[],
  116. label=_('Rear ports'),
  117. widget=forms.SelectMultiple(attrs={'size': 8})
  118. )
  119. def clean(self):
  120. super().clean()
  121. # Check that the total number of FrontPorts and positions matches the selected number of RearPort:position
  122. # mappings. Note that `name` will be a list under FrontPortCreateForm, in which cases we multiply the number of
  123. # FrontPorts being creation by the number of positions.
  124. positions = self.cleaned_data['positions']
  125. frontport_count = len(self.cleaned_data['name']) if type(self.cleaned_data['name']) is list else 1
  126. rearport_count = len(self.cleaned_data['rear_ports'])
  127. if frontport_count * positions != rearport_count:
  128. raise forms.ValidationError({
  129. 'rear_ports': _(
  130. "The total number of front port positions ({frontport_count}) must match the selected number of "
  131. "rear port positions ({rearport_count})."
  132. ).format(
  133. frontport_count=frontport_count,
  134. rearport_count=rearport_count
  135. )
  136. })
  137. def _save_m2m(self):
  138. super()._save_m2m()
  139. # TODO: Can this be made more efficient?
  140. # Delete existing rear port mappings
  141. self.port_mapping_model.objects.filter(front_port_id=self.instance.pk).delete()
  142. # Create new rear port mappings
  143. mappings = []
  144. if self.port_mapping_model is PortTemplateMapping:
  145. params = {
  146. 'device_type_id': self.instance.device_type_id,
  147. 'module_type_id': self.instance.module_type_id,
  148. }
  149. else:
  150. params = {
  151. 'device_id': self.instance.device_id,
  152. }
  153. for i, rp_position in enumerate(self.cleaned_data['rear_ports'], start=1):
  154. rear_port_id, rear_port_position = rp_position.split(':')
  155. mappings.append(
  156. self.port_mapping_model(**{
  157. **params,
  158. 'front_port_id': self.instance.pk,
  159. 'front_port_position': i,
  160. 'rear_port_id': rear_port_id,
  161. 'rear_port_position': rear_port_position,
  162. })
  163. )
  164. self.port_mapping_model.objects.bulk_create(mappings)
  165. # Send post_save signals
  166. for mapping in mappings:
  167. post_save.send(
  168. sender=PortMapping,
  169. instance=mapping,
  170. created=True,
  171. raw=False,
  172. using=connection,
  173. update_fields=None
  174. )
  175. def _get_rear_port_choices(self, parent_filter, front_port):
  176. """
  177. Return a list of choices representing each available rear port & position pair on the parent object (identified
  178. by a Q filter), excluding those assigned to the specified instance.
  179. """
  180. occupied_rear_port_positions = [
  181. f'{mapping.rear_port_id}:{mapping.rear_port_position}'
  182. for mapping in self.port_mapping_model.objects.filter(parent_filter).exclude(front_port=front_port.pk)
  183. ]
  184. choices = []
  185. for rear_port in self.rear_port_model.objects.filter(parent_filter):
  186. for i in range(1, rear_port.positions + 1):
  187. pair_id = f'{rear_port.pk}:{i}'
  188. if pair_id not in occupied_rear_port_positions:
  189. pair_label = f'{rear_port.name}:{i}'
  190. choices.append((pair_id, pair_label))
  191. return choices