Coverage for extras/ui/panels.py: 92%

227 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 18:35 +0000

1from django.conf import settings 

2from django.contrib.contenttypes.models import ContentType 

3from django.utils.translation import gettext_lazy as _ 

4 

5from netbox.ui import actions, attrs, panels 

6from utilities.data import resolve_attr_path 

7 

8__all__ = ( 

9 'ConfigContextAssignmentPanel', 

10 'ConfigContextPanel', 

11 'ConfigContextProfilePanel', 

12 'ConfigTemplatePanel', 

13 'CustomFieldBehaviorPanel', 

14 'CustomFieldChoiceSetChoicesPanel', 

15 'CustomFieldChoiceSetPanel', 

16 'CustomFieldObjectTypesPanel', 

17 'CustomFieldPanel', 

18 'CustomFieldRelatedObjectsPanel', 

19 'CustomFieldValidationPanel', 

20 'CustomFieldsPanel', 

21 'CustomLinkPanel', 

22 'EventRuleActionPanel', 

23 'EventRuleEventTypesPanel', 

24 'EventRulePanel', 

25 'ExportTemplatePanel', 

26 'ImageAttachmentFilePanel', 

27 'ImageAttachmentImagePanel', 

28 'ImageAttachmentPanel', 

29 'ImageAttachmentsPanel', 

30 'JournalEntryPanel', 

31 'NotificationGroupGroupsPanel', 

32 'NotificationGroupPanel', 

33 'NotificationGroupUsersPanel', 

34 'ObjectTypesPanel', 

35 'SavedFilterObjectTypesPanel', 

36 'SavedFilterPanel', 

37 'TableConfigColumnsPanel', 

38 'TableConfigOrderingPanel', 

39 'TableConfigPanel', 

40 'TagItemTypesPanel', 

41 'TagObjectTypesPanel', 

42 'TagPanel', 

43 'TagsPanel', 

44 'WebhookHTTPPanel', 

45 'WebhookPanel', 

46 'WebhookSSLPanel', 

47) 

48 

49 

50# 

51# Generic panels 

52# 

53 

54class CustomFieldsPanel(panels.ObjectPanel): 

55 """ 

56 A panel showing the value of all custom fields defined on an object. 

57 """ 

58 template_name = 'extras/panels/custom_fields.html' 

59 title = _('Custom Fields') 

60 

61 def get_context(self, context): 

62 obj = resolve_attr_path(context, self.accessor) 

63 return { 

64 **super().get_context(context), 

65 'custom_fields': obj.get_custom_fields_by_group(), 

66 } 

67 

68 def should_render(self, context): 

69 return bool(context['custom_fields']) 

70 

71 

72class ImageAttachmentsPanel(panels.ObjectsTablePanel): 

73 """ 

74 A panel showing all images attached to the object. 

75 """ 

76 actions = [ 

77 actions.AddObject( 

78 'extras.imageattachment', 

79 url_params={ 

80 'object_type': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk, 

81 'object_id': lambda ctx: ctx['object'].pk, 

82 'return_url': lambda ctx: ctx['object'].get_absolute_url(), 

83 }, 

84 label=_('Attach an image'), 

85 ), 

86 ] 

87 

88 def __init__(self, **kwargs): 

89 super().__init__( 

90 'extras.imageattachment', 

91 filters={ 

92 'object_type_id': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk, 

93 'object_id': lambda ctx: ctx['object'].pk, 

94 }, 

95 **kwargs, 

96 ) 

97 

98 

99class TagsPanel(panels.ObjectPanel): 

100 """ 

101 A panel showing the tags assigned to the object. 

102 """ 

103 template_name = 'extras/panels/tags.html' 

104 title = _('Tags') 

105 

106 def get_context(self, context): 

107 return { 

108 **super().get_context(context), 

109 'object': resolve_attr_path(context, self.accessor), 

110 } 

111 

112 

113class ObjectTypesPanel(panels.ObjectPanel): 

114 """ 

115 A panel listing the object types assigned to the object. 

116 """ 

117 template_name = 'extras/panels/object_types.html' 

118 title = _('Object Types') 

119 

120 

121# 

122# CustomField panels 

123# 

124 

125class CustomFieldPanel(panels.ObjectAttributesPanel): 

126 title = _('Custom Field') 

127 

128 name = attrs.TextAttr('name') 

129 status = attrs.ChoiceAttr('status') 

130 type = attrs.TemplatedAttr('type', label=_('Type'), template_name='extras/customfield/attrs/type.html') 

131 label = attrs.TextAttr('label') 

132 group_name = attrs.TextAttr('group_name', label=_('Group name')) 

133 description = attrs.TextAttr('description') 

134 required = attrs.BooleanAttr('required') 

135 unique = attrs.BooleanAttr('unique', label=_('Must be unique')) 

136 is_cloneable = attrs.BooleanAttr('is_cloneable', label=_('Cloneable')) 

137 choice_set = attrs.TemplatedAttr( 

138 'choice_set', 

139 template_name='extras/customfield/attrs/choice_set.html', 

140 ) 

141 default = attrs.TextAttr('default', label=_('Default value')) 

142 related_object_filter = attrs.TemplatedAttr( 

143 'related_object_filter', 

144 template_name='extras/customfield/attrs/related_object_filter.html', 

145 ) 

146 

147 

148class CustomFieldBehaviorPanel(panels.ObjectAttributesPanel): 

149 title = _('Behavior') 

150 

151 search_weight = attrs.TemplatedAttr( 

152 'search_weight', 

153 template_name='extras/customfield/attrs/search_weight.html', 

154 ) 

155 filter_logic = attrs.ChoiceAttr('filter_logic') 

156 weight = attrs.NumericAttr('weight', label=_('Display weight')) 

157 ui_visible = attrs.ChoiceAttr('ui_visible', label=_('UI visible')) 

158 ui_editable = attrs.ChoiceAttr('ui_editable', label=_('UI editable')) 

159 nulls_first = attrs.BooleanAttr('nulls_first', label=_('Nulls first')) 

160 

161 

162class CustomFieldValidationPanel(panels.ObjectAttributesPanel): 

163 title = _('Validation Rules') 

164 

165 validation_minimum = attrs.NumericAttr('validation_minimum', label=_('Minimum value')) 

166 validation_maximum = attrs.NumericAttr('validation_maximum', label=_('Maximum value')) 

167 validation_regex = attrs.TextAttr( 

168 'validation_regex', 

169 label=_('Regular expression'), 

170 style='font-monospace', 

171 ) 

172 

173 

174class CustomFieldObjectTypesPanel(panels.ObjectPanel): 

175 template_name = 'extras/panels/object_types.html' 

176 title = _('Object Types') 

177 

178 

179class CustomFieldRelatedObjectsPanel(panels.ObjectPanel): 

180 template_name = 'extras/panels/customfield_related_objects.html' 

181 title = _('Related Objects') 

182 

183 def get_context(self, context): 

184 return { 

185 **super().get_context(context), 

186 'related_models': context.get('related_models'), 

187 } 

188 

189 

190# 

191# CustomFieldChoiceSet panels 

192# 

193 

194class CustomFieldChoiceSetPanel(panels.ObjectAttributesPanel): 

195 title = _('Custom Field Choice Set') 

196 

197 name = attrs.TextAttr('name') 

198 description = attrs.TextAttr('description') 

199 base_choices = attrs.ChoiceAttr('base_choices') 

200 order_alphabetically = attrs.BooleanAttr('order_alphabetically') 

201 choices_for = attrs.RelatedObjectListAttr('choices_for', linkify=True, label=_('Used by')) 

202 

203 

204class CustomFieldChoiceSetChoicesPanel(panels.ObjectPanel): 

205 template_name = 'extras/panels/customfieldchoiceset_choices.html' 

206 

207 def get_context(self, context): 

208 obj = context.get('object') 

209 total = len(obj.choices) if obj else 0 

210 return { 

211 **super().get_context(context), 

212 'title': f'{_("Choices")} ({total})', 

213 'choices': context.get('choices'), 

214 } 

215 

216 

217# 

218# CustomLink panels 

219# 

220 

221class CustomLinkPanel(panels.ObjectAttributesPanel): 

222 title = _('Custom Link') 

223 

224 name = attrs.TextAttr('name') 

225 enabled = attrs.BooleanAttr('enabled') 

226 group_name = attrs.TextAttr('group_name') 

227 weight = attrs.NumericAttr('weight') 

228 button_class = attrs.ChoiceAttr('button_class') 

229 new_window = attrs.BooleanAttr('new_window') 

230 

231 

232# 

233# ExportTemplate panels 

234# 

235 

236class ExportTemplatePanel(panels.ObjectAttributesPanel): 

237 title = _('Export Template') 

238 

239 name = attrs.TextAttr('name') 

240 description = attrs.TextAttr('description') 

241 mime_type = attrs.TextAttr('mime_type', label=_('MIME type')) 

242 file_name = attrs.TextAttr('file_name') 

243 file_extension = attrs.TextAttr('file_extension') 

244 as_attachment = attrs.BooleanAttr('as_attachment', label=_('Attachment')) 

245 

246 

247# 

248# SavedFilter panels 

249# 

250 

251class SavedFilterPanel(panels.ObjectAttributesPanel): 

252 title = _('Saved Filter') 

253 

254 name = attrs.TextAttr('name') 

255 description = attrs.TextAttr('description') 

256 user = attrs.TextAttr('user') 

257 enabled = attrs.BooleanAttr('enabled') 

258 shared = attrs.BooleanAttr('shared') 

259 weight = attrs.NumericAttr('weight') 

260 

261 

262class SavedFilterObjectTypesPanel(panels.ObjectPanel): 

263 template_name = 'extras/panels/savedfilter_object_types.html' 

264 title = _('Assigned Models') 

265 

266 

267# 

268# TableConfig panels 

269# 

270 

271class TableConfigPanel(panels.ObjectAttributesPanel): 

272 title = _('Table Config') 

273 

274 name = attrs.TextAttr('name') 

275 description = attrs.TextAttr('description') 

276 object_type = attrs.TextAttr('object_type') 

277 table = attrs.TextAttr('table') 

278 user = attrs.TextAttr('user') 

279 enabled = attrs.BooleanAttr('enabled') 

280 shared = attrs.BooleanAttr('shared') 

281 weight = attrs.NumericAttr('weight') 

282 

283 

284class TableConfigColumnsPanel(panels.ObjectPanel): 

285 template_name = 'extras/panels/tableconfig_columns.html' 

286 title = _('Columns Displayed') 

287 

288 def get_context(self, context): 

289 return { 

290 **super().get_context(context), 

291 'columns': context.get('columns'), 

292 } 

293 

294 

295class TableConfigOrderingPanel(panels.ObjectPanel): 

296 template_name = 'extras/panels/tableconfig_ordering.html' 

297 title = _('Ordering') 

298 

299 def get_context(self, context): 

300 return { 

301 **super().get_context(context), 

302 'columns': context.get('columns'), 

303 } 

304 

305 

306# 

307# NotificationGroup panels 

308# 

309 

310class NotificationGroupPanel(panels.ObjectAttributesPanel): 

311 title = _('Notification Group') 

312 

313 name = attrs.TextAttr('name') 

314 description = attrs.TextAttr('description') 

315 

316 

317class NotificationGroupGroupsPanel(panels.ObjectPanel): 

318 template_name = 'extras/panels/notificationgroup_groups.html' 

319 title = _('Groups') 

320 

321 

322class NotificationGroupUsersPanel(panels.ObjectPanel): 

323 template_name = 'extras/panels/notificationgroup_users.html' 

324 title = _('Users') 

325 

326 

327# 

328# Webhook panels 

329# 

330 

331class WebhookPanel(panels.ObjectAttributesPanel): 

332 title = _('Webhook') 

333 

334 name = attrs.TextAttr('name') 

335 description = attrs.TextAttr('description') 

336 

337 

338class WebhookTimeoutAttr(attrs.TextAttr): 

339 """ 

340 Render a webhook's timeout. Webhooks which do not define their own timeout fall back to the globally 

341 configured default, which is displayed (and annotated as such) in place of an empty value. 

342 """ 

343 def get_value(self, obj): 

344 if obj.timeout is not None: 

345 return _('{timeout} seconds').format(timeout=obj.timeout) 

346 return _('{timeout} seconds (default)').format(timeout=settings.WEBHOOK_DEFAULT_TIMEOUT) 

347 

348 

349class WebhookHTTPPanel(panels.ObjectAttributesPanel): 

350 title = _('HTTP Request') 

351 

352 http_method = attrs.ChoiceAttr('http_method', label=_('HTTP method')) 

353 payload_url = attrs.TextAttr('payload_url', label=_('Payload URL'), style='font-monospace') 

354 http_content_type = attrs.TextAttr('http_content_type', label=_('HTTP content type')) 

355 secret = attrs.TextAttr('secret') 

356 timeout = WebhookTimeoutAttr('timeout') 

357 

358 

359class WebhookSSLPanel(panels.ObjectAttributesPanel): 

360 title = _('SSL') 

361 

362 ssl_verification = attrs.BooleanAttr('ssl_verification', label=_('SSL verification')) 

363 ca_file_path = attrs.TextAttr('ca_file_path', label=_('CA file path')) 

364 

365 

366# 

367# EventRule panels 

368# 

369 

370class EventRulePanel(panels.ObjectAttributesPanel): 

371 title = _('Event Rule') 

372 

373 name = attrs.TextAttr('name') 

374 enabled = attrs.BooleanAttr('enabled') 

375 description = attrs.TextAttr('description') 

376 

377 

378class EventRuleEventTypesPanel(panels.ObjectPanel): 

379 template_name = 'extras/panels/eventrule_event_types.html' 

380 title = _('Event Types') 

381 

382 def get_context(self, context): 

383 return { 

384 **super().get_context(context), 

385 'registry': context.get('registry'), 

386 } 

387 

388 

389class EventRuleActionPanel(panels.ObjectAttributesPanel): 

390 title = _('Action') 

391 

392 action_type = attrs.ChoiceAttr('action_type', label=_('Type')) 

393 action_object = attrs.RelatedObjectAttr('action_object', linkify=True, label=_('Object')) 

394 action_data = attrs.TemplatedAttr( 

395 'action_data', 

396 label=_('Data'), 

397 template_name='extras/eventrule/attrs/action_data.html', 

398 ) 

399 

400 

401# 

402# Tag panels 

403# 

404 

405class TagPanel(panels.ObjectAttributesPanel): 

406 title = _('Tag') 

407 

408 name = attrs.TextAttr('name') 

409 description = attrs.TextAttr('description') 

410 color = attrs.ColorAttr('color') 

411 weight = attrs.NumericAttr('weight') 

412 tagged_items = attrs.TemplatedAttr( 

413 'extras_taggeditem_items', 

414 template_name='extras/tag/attrs/tagged_item_count.html', 

415 ) 

416 

417 

418class TagObjectTypesPanel(panels.ObjectPanel): 

419 template_name = 'extras/panels/tag_object_types.html' 

420 title = _('Allowed Object Types') 

421 

422 

423class TagItemTypesPanel(panels.ObjectPanel): 

424 template_name = 'extras/panels/tag_item_types.html' 

425 title = _('Tagged Item Types') 

426 

427 def get_context(self, context): 

428 return { 

429 **super().get_context(context), 

430 'object_types': context.get('object_types'), 

431 } 

432 

433 

434# 

435# ConfigContextProfile panels 

436# 

437 

438class ConfigContextProfilePanel(panels.ObjectAttributesPanel): 

439 title = _('Config Context Profile') 

440 

441 name = attrs.TextAttr('name') 

442 description = attrs.TextAttr('description') 

443 

444 

445# 

446# ConfigContext panels 

447# 

448 

449class ConfigContextPanel(panels.ObjectAttributesPanel): 

450 title = _('Config Context') 

451 

452 name = attrs.TextAttr('name') 

453 weight = attrs.NumericAttr('weight') 

454 profile = attrs.RelatedObjectAttr('profile', linkify=True) 

455 description = attrs.TextAttr('description') 

456 is_active = attrs.BooleanAttr('is_active', label=_('Active')) 

457 

458 

459class ConfigContextAssignmentPanel(panels.ObjectPanel): 

460 template_name = 'extras/panels/configcontext_assignment.html' 

461 title = _('Assignment') 

462 

463 def get_context(self, context): 

464 return { 

465 **super().get_context(context), 

466 'assigned_objects': context.get('assigned_objects'), 

467 } 

468 

469 

470# 

471# ConfigTemplate panels 

472# 

473 

474class ConfigTemplatePanel(panels.ObjectAttributesPanel): 

475 title = _('Config Template') 

476 

477 name = attrs.TextAttr('name') 

478 description = attrs.TextAttr('description') 

479 mime_type = attrs.TextAttr('mime_type', label=_('MIME type')) 

480 file_name = attrs.TextAttr('file_name') 

481 file_extension = attrs.TextAttr('file_extension') 

482 as_attachment = attrs.BooleanAttr('as_attachment', label=_('Attachment')) 

483 debug = attrs.BooleanAttr('debug') 

484 data_source = attrs.RelatedObjectAttr('data_source', linkify=True) 

485 data_file = attrs.TemplatedAttr( 

486 'data_path', 

487 template_name='extras/configtemplate/attrs/data_file.html', 

488 ) 

489 data_synced = attrs.DateTimeAttr('data_synced') 

490 auto_sync_enabled = attrs.BooleanAttr('auto_sync_enabled') 

491 

492 

493# 

494# ImageAttachment panels 

495# 

496 

497class ImageAttachmentPanel(panels.ObjectAttributesPanel): 

498 title = _('Image Attachment') 

499 

500 parent = attrs.RelatedObjectAttr('parent', linkify=True, label=_('Parent object')) 

501 name = attrs.TextAttr('name') 

502 description = attrs.TextAttr('description') 

503 

504 

505class ImageAttachmentFilePanel(panels.ObjectPanel): 

506 template_name = 'extras/panels/imageattachment_file.html' 

507 title = _('File') 

508 

509 

510class ImageAttachmentImagePanel(panels.ObjectPanel): 

511 template_name = 'extras/panels/imageattachment_image.html' 

512 title = _('Image') 

513 

514 

515# 

516# JournalEntry panels 

517# 

518 

519class JournalEntryPanel(panels.ObjectAttributesPanel): 

520 title = _('Journal Entry') 

521 

522 assigned_object = attrs.RelatedObjectAttr('assigned_object', linkify=True, label=_('Object')) 

523 created = attrs.DateTimeAttr('created', spec='minutes') 

524 created_by = attrs.TextAttr('created_by') 

525 kind = attrs.ChoiceAttr('kind')