Coverage for dcim/forms/object_create.py: 71%
145 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from django import forms
2from django.utils.translation import gettext_lazy as _
4from dcim.models import *
5from netbox.forms import NetBoxModelForm
6from netbox.forms.mixins import OwnerMixin
7from utilities.forms.fields import (
8 DynamicModelChoiceField,
9 DynamicModelMultipleChoiceField,
10 ExpandableNameField,
11 ExpandableNumericField,
12)
13from utilities.forms.rendering import FieldSet, TabbedGroups
14from utilities.forms.widgets import APISelect
16from . import model_forms
18__all__ = (
19 'ComponentCreateForm',
20 'ConsolePortCreateForm',
21 'ConsolePortTemplateCreateForm',
22 'ConsoleServerPortCreateForm',
23 'ConsoleServerPortTemplateCreateForm',
24 'CoolingIntakeCreateForm',
25 'CoolingIntakeTemplateCreateForm',
26 'CoolingOutflowCreateForm',
27 'CoolingOutflowTemplateCreateForm',
28 'DeviceBayCreateForm',
29 'DeviceBayTemplateCreateForm',
30 'FrontPortCreateForm',
31 'FrontPortTemplateCreateForm',
32 'InterfaceCreateForm',
33 'InterfaceTemplateCreateForm',
34 'InventoryItemCreateForm',
35 'InventoryItemTemplateCreateForm',
36 'ModuleBayCreateForm',
37 'ModuleBayTemplateCreateForm',
38 'PowerOutletCreateForm',
39 'PowerOutletTemplateCreateForm',
40 'PowerPortCreateForm',
41 'PowerPortTemplateCreateForm',
42 'RearPortCreateForm',
43 'RearPortTemplateCreateForm',
44 'VirtualChassisCreateForm',
45)
48class ComponentCreateForm(forms.Form):
49 """
50 Subclass this form when facilitating the creation of one or more component or component template objects based on
51 a name pattern.
52 """
53 name = ExpandableNameField(
54 label=_('Name'),
55 )
56 label = ExpandableNameField(
57 label=_('Label'),
58 required=False,
59 help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
60 )
62 # Identify the fields which support replication (i.e. ExpandableNameFields). This is referenced by
63 # ComponentCreateView when creating objects.
64 replication_fields = ('name', 'label')
66 def clean(self):
67 super().clean()
69 # Validate that all replication fields generate an equal number of values (or a single value)
70 if not (patterns := self.cleaned_data.get(self.replication_fields[0])):
71 return
72 pattern_count = len(patterns)
73 for field_name in self.replication_fields:
74 # A field is absent from cleaned_data if it failed its own validation, e.g. an inverted numeric range
75 if not (values := self.cleaned_data.get(field_name)):
76 continue
77 value_count = len(values)
78 if value_count == 1:
79 # If the field resolves to a single value (because no pattern was used), multiply it by the number
80 # of expected values. This allows us to reuse the same label when creating multiple components.
81 self.cleaned_data[field_name] = values * pattern_count
82 elif value_count != pattern_count:
83 raise forms.ValidationError({
84 field_name: _(
85 "The provided pattern specifies {value_count} values, but {pattern_count} are expected."
86 ).format(value_count=value_count, pattern_count=pattern_count)
87 }, code='label_pattern_mismatch')
90#
91# Device component templates
92#
94class ConsolePortTemplateCreateForm(ComponentCreateForm, model_forms.ConsolePortTemplateForm):
96 class Meta(model_forms.ConsolePortTemplateForm.Meta):
97 exclude = ('name', 'label')
100class ConsoleServerPortTemplateCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortTemplateForm):
102 class Meta(model_forms.ConsoleServerPortTemplateForm.Meta):
103 exclude = ('name', 'label')
106class PowerPortTemplateCreateForm(ComponentCreateForm, model_forms.PowerPortTemplateForm):
108 class Meta(model_forms.PowerPortTemplateForm.Meta):
109 exclude = ('name', 'label')
112class PowerOutletTemplateCreateForm(ComponentCreateForm, model_forms.PowerOutletTemplateForm):
114 class Meta(model_forms.PowerOutletTemplateForm.Meta):
115 exclude = ('name', 'label')
118class CoolingIntakeTemplateCreateForm(ComponentCreateForm, model_forms.CoolingIntakeTemplateForm):
120 class Meta(model_forms.CoolingIntakeTemplateForm.Meta):
121 exclude = ('name', 'label')
124class CoolingOutflowTemplateCreateForm(ComponentCreateForm, model_forms.CoolingOutflowTemplateForm):
126 class Meta(model_forms.CoolingOutflowTemplateForm.Meta):
127 exclude = ('name', 'label')
130class InterfaceTemplateCreateForm(ComponentCreateForm, model_forms.InterfaceTemplateForm):
131 channel_id = ExpandableNumericField(
132 label=_('Channel ID'),
133 required=False
134 )
135 replication_fields = ('name', 'label', 'channel_id')
137 class Meta(model_forms.InterfaceTemplateForm.Meta):
138 exclude = ('name', 'label', 'channel_id')
141class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm):
143 # Override fieldsets from FrontPortTemplateForm
144 fieldsets = (
145 FieldSet(
146 TabbedGroups(
147 FieldSet('device_type', name=_('Device Type')),
148 FieldSet('module_type', name=_('Module Type')),
149 ),
150 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'description',
151 ),
152 )
154 class Meta:
155 model = FrontPortTemplate
156 fields = (
157 'device_type', 'module_type', 'type', 'color', 'positions', 'description',
158 )
160 def get_iterative_data(self, iteration):
161 positions = self.cleaned_data['positions']
162 offset = positions * iteration
164 return {
165 'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions]
166 }
169class RearPortTemplateCreateForm(ComponentCreateForm, model_forms.RearPortTemplateForm):
171 class Meta(model_forms.RearPortTemplateForm.Meta):
172 exclude = ('name', 'label')
175class DeviceBayTemplateCreateForm(ComponentCreateForm, model_forms.DeviceBayTemplateForm):
177 class Meta(model_forms.DeviceBayTemplateForm.Meta):
178 exclude = ('name', 'label')
181class ModuleBayTemplateCreateForm(ComponentCreateForm, model_forms.ModuleBayTemplateForm):
182 position = ExpandableNameField(
183 label=_('Position'),
184 required=False,
185 help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
186 )
187 replication_fields = ('name', 'label', 'position')
189 class Meta(model_forms.ModuleBayTemplateForm.Meta):
190 exclude = ('name', 'label', 'position')
193class InventoryItemTemplateCreateForm(ComponentCreateForm, model_forms.InventoryItemTemplateForm):
195 class Meta(model_forms.InventoryItemTemplateForm.Meta):
196 exclude = ('name', 'label')
199#
200# Device components
201#
203class ConsolePortCreateForm(ComponentCreateForm, model_forms.ConsolePortForm):
205 class Meta(model_forms.ConsolePortForm.Meta):
206 exclude = ('name', 'label')
209class ConsoleServerPortCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortForm):
211 class Meta(model_forms.ConsoleServerPortForm.Meta):
212 exclude = ('name', 'label')
215class PowerPortCreateForm(ComponentCreateForm, model_forms.PowerPortForm):
217 class Meta(model_forms.PowerPortForm.Meta):
218 exclude = ('name', 'label')
221class PowerOutletCreateForm(ComponentCreateForm, model_forms.PowerOutletForm):
223 class Meta(model_forms.PowerOutletForm.Meta):
224 exclude = ('name', 'label')
227class CoolingIntakeCreateForm(ComponentCreateForm, model_forms.CoolingIntakeForm):
229 class Meta(model_forms.CoolingIntakeForm.Meta):
230 exclude = ('name', 'label')
233class CoolingOutflowCreateForm(ComponentCreateForm, model_forms.CoolingOutflowForm):
235 class Meta(model_forms.CoolingOutflowForm.Meta):
236 exclude = ('name', 'label')
239class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
240 channel_id = ExpandableNumericField(
241 label=_('Channel ID'),
242 required=False
243 )
244 replication_fields = ('name', 'label', 'channel_id')
246 class Meta(model_forms.InterfaceForm.Meta):
247 exclude = ('name', 'label', 'channel_id')
250class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
251 device = DynamicModelChoiceField(
252 label=_('Device'),
253 queryset=Device.objects.all(),
254 selector=True,
255 widget=APISelect(
256 # TODO: Clean up the application of HTMXSelect attributes
257 attrs={
258 'hx-get': '.',
259 'hx-include': '#form_fields',
260 'hx-target': '#form_fields',
261 }
262 )
263 )
265 # Override fieldsets from FrontPortForm to omit rear_port_position
266 fieldsets = (
267 FieldSet(
268 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected',
269 'description', 'tags',
270 ),
271 )
273 class Meta:
274 model = FrontPort
275 fields = [
276 'device', 'module', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner', 'tags',
277 ]
279 def get_iterative_data(self, iteration):
280 positions = self.cleaned_data['positions']
281 offset = positions * iteration
282 return {
283 'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions]
284 }
287class RearPortCreateForm(ComponentCreateForm, model_forms.RearPortForm):
289 class Meta(model_forms.RearPortForm.Meta):
290 exclude = ('name', 'label')
293class DeviceBayCreateForm(ComponentCreateForm, model_forms.DeviceBayForm):
295 class Meta(model_forms.DeviceBayForm.Meta):
296 exclude = ('name', 'label')
299class ModuleBayCreateForm(ComponentCreateForm, model_forms.ModuleBayForm):
300 position = ExpandableNameField(
301 label=_('Position'),
302 required=False,
303 help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
304 )
305 replication_fields = ('name', 'label', 'position')
307 class Meta(model_forms.ModuleBayForm.Meta):
308 exclude = ('name', 'label', 'position')
311class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm):
313 class Meta(model_forms.InventoryItemForm.Meta):
314 exclude = ('name', 'label')
317#
318# Virtual chassis
319#
321class VirtualChassisCreateForm(OwnerMixin, NetBoxModelForm):
322 region = DynamicModelChoiceField(
323 label=_('Region'),
324 queryset=Region.objects.all(),
325 required=False,
326 initial_params={
327 'sites': '$site'
328 }
329 )
330 site_group = DynamicModelChoiceField(
331 label=_('Site group'),
332 queryset=SiteGroup.objects.all(),
333 required=False,
334 initial_params={
335 'sites': '$site'
336 }
337 )
338 site = DynamicModelChoiceField(
339 label=_('Site'),
340 queryset=Site.objects.all(),
341 required=False,
342 query_params={
343 'region_id': '$region',
344 'group_id': '$site_group',
345 }
346 )
347 rack = DynamicModelChoiceField(
348 label=_('Rack'),
349 queryset=Rack.objects.all(),
350 required=False,
351 null_option='None',
352 query_params={
353 'site_id': '$site'
354 }
355 )
356 members = DynamicModelMultipleChoiceField(
357 label=_('Members'),
358 queryset=Device.objects.all(),
359 required=False,
360 query_params={
361 'virtual_chassis_id': 'null',
362 'site_id': '$site',
363 'rack_id': '$rack',
364 }
365 )
366 initial_position = forms.IntegerField(
367 label=_('Initial position'),
368 initial=1,
369 required=False,
370 help_text=_('Position of the first member device. Increases by one for each additional member.')
371 )
373 fieldsets = (
374 FieldSet('name', 'domain', 'description', 'tags', name=_('Virtual Chassis')),
375 FieldSet('region', 'site_group', 'site', 'rack', 'members', 'initial_position', name=_('Member Devices')),
376 )
378 class Meta:
379 model = VirtualChassis
380 fields = [
381 'name', 'domain', 'description', 'region', 'site_group', 'site', 'rack', 'owner', 'members',
382 'initial_position', 'tags',
383 ]
385 def clean(self):
386 super().clean()
388 if self.cleaned_data['members'] and self.cleaned_data['initial_position'] is None:
389 raise forms.ValidationError({
390 'initial_position': _("A position must be specified for the first VC member.")
391 })
393 def save(self, *args, **kwargs):
394 instance = super().save(*args, **kwargs)
396 # Assign VC members
397 if instance.pk and self.cleaned_data['members']:
398 initial_position = self.cleaned_data.get('initial_position', 1)
399 for i, member in enumerate(self.cleaned_data['members'], start=initial_position):
400 member.snapshot()
401 member.virtual_chassis = instance
402 member.vc_position = i
403 member.save()
405 return instance