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

1from django import forms 

2from django.utils.translation import gettext_lazy as _ 

3 

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 

15 

16from . import model_forms 

17 

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) 

46 

47 

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 ) 

61 

62 # Identify the fields which support replication (i.e. ExpandableNameFields). This is referenced by 

63 # ComponentCreateView when creating objects. 

64 replication_fields = ('name', 'label') 

65 

66 def clean(self): 

67 super().clean() 

68 

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') 

88 

89 

90# 

91# Device component templates 

92# 

93 

94class ConsolePortTemplateCreateForm(ComponentCreateForm, model_forms.ConsolePortTemplateForm): 

95 

96 class Meta(model_forms.ConsolePortTemplateForm.Meta): 

97 exclude = ('name', 'label') 

98 

99 

100class ConsoleServerPortTemplateCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortTemplateForm): 

101 

102 class Meta(model_forms.ConsoleServerPortTemplateForm.Meta): 

103 exclude = ('name', 'label') 

104 

105 

106class PowerPortTemplateCreateForm(ComponentCreateForm, model_forms.PowerPortTemplateForm): 

107 

108 class Meta(model_forms.PowerPortTemplateForm.Meta): 

109 exclude = ('name', 'label') 

110 

111 

112class PowerOutletTemplateCreateForm(ComponentCreateForm, model_forms.PowerOutletTemplateForm): 

113 

114 class Meta(model_forms.PowerOutletTemplateForm.Meta): 

115 exclude = ('name', 'label') 

116 

117 

118class CoolingIntakeTemplateCreateForm(ComponentCreateForm, model_forms.CoolingIntakeTemplateForm): 

119 

120 class Meta(model_forms.CoolingIntakeTemplateForm.Meta): 

121 exclude = ('name', 'label') 

122 

123 

124class CoolingOutflowTemplateCreateForm(ComponentCreateForm, model_forms.CoolingOutflowTemplateForm): 

125 

126 class Meta(model_forms.CoolingOutflowTemplateForm.Meta): 

127 exclude = ('name', 'label') 

128 

129 

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') 

136 

137 class Meta(model_forms.InterfaceTemplateForm.Meta): 

138 exclude = ('name', 'label', 'channel_id') 

139 

140 

141class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm): 

142 

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 ) 

153 

154 class Meta: 

155 model = FrontPortTemplate 

156 fields = ( 

157 'device_type', 'module_type', 'type', 'color', 'positions', 'description', 

158 ) 

159 

160 def get_iterative_data(self, iteration): 

161 positions = self.cleaned_data['positions'] 

162 offset = positions * iteration 

163 

164 return { 

165 'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions] 

166 } 

167 

168 

169class RearPortTemplateCreateForm(ComponentCreateForm, model_forms.RearPortTemplateForm): 

170 

171 class Meta(model_forms.RearPortTemplateForm.Meta): 

172 exclude = ('name', 'label') 

173 

174 

175class DeviceBayTemplateCreateForm(ComponentCreateForm, model_forms.DeviceBayTemplateForm): 

176 

177 class Meta(model_forms.DeviceBayTemplateForm.Meta): 

178 exclude = ('name', 'label') 

179 

180 

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') 

188 

189 class Meta(model_forms.ModuleBayTemplateForm.Meta): 

190 exclude = ('name', 'label', 'position') 

191 

192 

193class InventoryItemTemplateCreateForm(ComponentCreateForm, model_forms.InventoryItemTemplateForm): 

194 

195 class Meta(model_forms.InventoryItemTemplateForm.Meta): 

196 exclude = ('name', 'label') 

197 

198 

199# 

200# Device components 

201# 

202 

203class ConsolePortCreateForm(ComponentCreateForm, model_forms.ConsolePortForm): 

204 

205 class Meta(model_forms.ConsolePortForm.Meta): 

206 exclude = ('name', 'label') 

207 

208 

209class ConsoleServerPortCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortForm): 

210 

211 class Meta(model_forms.ConsoleServerPortForm.Meta): 

212 exclude = ('name', 'label') 

213 

214 

215class PowerPortCreateForm(ComponentCreateForm, model_forms.PowerPortForm): 

216 

217 class Meta(model_forms.PowerPortForm.Meta): 

218 exclude = ('name', 'label') 

219 

220 

221class PowerOutletCreateForm(ComponentCreateForm, model_forms.PowerOutletForm): 

222 

223 class Meta(model_forms.PowerOutletForm.Meta): 

224 exclude = ('name', 'label') 

225 

226 

227class CoolingIntakeCreateForm(ComponentCreateForm, model_forms.CoolingIntakeForm): 

228 

229 class Meta(model_forms.CoolingIntakeForm.Meta): 

230 exclude = ('name', 'label') 

231 

232 

233class CoolingOutflowCreateForm(ComponentCreateForm, model_forms.CoolingOutflowForm): 

234 

235 class Meta(model_forms.CoolingOutflowForm.Meta): 

236 exclude = ('name', 'label') 

237 

238 

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') 

245 

246 class Meta(model_forms.InterfaceForm.Meta): 

247 exclude = ('name', 'label', 'channel_id') 

248 

249 

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 ) 

264 

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 ) 

272 

273 class Meta: 

274 model = FrontPort 

275 fields = [ 

276 'device', 'module', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner', 'tags', 

277 ] 

278 

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 } 

285 

286 

287class RearPortCreateForm(ComponentCreateForm, model_forms.RearPortForm): 

288 

289 class Meta(model_forms.RearPortForm.Meta): 

290 exclude = ('name', 'label') 

291 

292 

293class DeviceBayCreateForm(ComponentCreateForm, model_forms.DeviceBayForm): 

294 

295 class Meta(model_forms.DeviceBayForm.Meta): 

296 exclude = ('name', 'label') 

297 

298 

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') 

306 

307 class Meta(model_forms.ModuleBayForm.Meta): 

308 exclude = ('name', 'label', 'position') 

309 

310 

311class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm): 

312 

313 class Meta(model_forms.InventoryItemForm.Meta): 

314 exclude = ('name', 'label') 

315 

316 

317# 

318# Virtual chassis 

319# 

320 

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 ) 

372 

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 ) 

377 

378 class Meta: 

379 model = VirtualChassis 

380 fields = [ 

381 'name', 'domain', 'description', 'region', 'site_group', 'site', 'rack', 'owner', 'members', 

382 'initial_position', 'tags', 

383 ] 

384 

385 def clean(self): 

386 super().clean() 

387 

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 }) 

392 

393 def save(self, *args, **kwargs): 

394 instance = super().save(*args, **kwargs) 

395 

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() 

404 

405 return instance