object_views.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  1. import logging
  2. from copy import deepcopy
  3. from django.contrib import messages
  4. from django.contrib.contenttypes.models import ContentType
  5. from django.core.exceptions import ObjectDoesNotExist
  6. from django.db import transaction
  7. from django.db.models import ProtectedError
  8. from django.forms.widgets import HiddenInput
  9. from django.http import HttpResponse
  10. from django.shortcuts import get_object_or_404, redirect, render
  11. from django.utils.html import escape
  12. from django.utils.http import is_safe_url
  13. from django.utils.safestring import mark_safe
  14. from django.views.generic import View
  15. from django_tables2.export import TableExport
  16. from dcim.forms.object_create import ComponentCreateForm
  17. from extras.models import ExportTemplate
  18. from extras.signals import clear_webhooks
  19. from utilities.error_handlers import handle_protectederror
  20. from utilities.exceptions import AbortTransaction, PermissionsViolation
  21. from utilities.forms import ConfirmationForm, ImportForm, restrict_form_fields
  22. from utilities.htmx import is_htmx
  23. from utilities.permissions import get_permission_for_model
  24. from utilities.tables import paginate_table
  25. from utilities.utils import normalize_querydict, prepare_cloned_fields
  26. from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin
  27. __all__ = (
  28. 'ComponentCreateView',
  29. 'ObjectChildrenView',
  30. 'ObjectDeleteView',
  31. 'ObjectEditView',
  32. 'ObjectImportView',
  33. 'ObjectListView',
  34. 'ObjectView',
  35. )
  36. class ObjectView(ObjectPermissionRequiredMixin, View):
  37. """
  38. Retrieve a single object for display.
  39. queryset: The base queryset for retrieving the object
  40. template_name: Name of the template to use
  41. """
  42. queryset = None
  43. template_name = None
  44. def get_required_permission(self):
  45. return get_permission_for_model(self.queryset.model, 'view')
  46. def get_template_name(self):
  47. """
  48. Return self.template_name if set. Otherwise, resolve the template path by model app_label and name.
  49. """
  50. if self.template_name is not None:
  51. return self.template_name
  52. model_opts = self.queryset.model._meta
  53. return f'{model_opts.app_label}/{model_opts.model_name}.html'
  54. def get_extra_context(self, request, instance):
  55. """
  56. Return any additional context data for the template.
  57. :param request: The current request
  58. :param instance: The object being viewed
  59. """
  60. return {}
  61. def get(self, request, *args, **kwargs):
  62. """
  63. GET request handler. *args and **kwargs are passed to identify the object being queried.
  64. :param request: The current request
  65. """
  66. instance = get_object_or_404(self.queryset, **kwargs)
  67. return render(request, self.get_template_name(), {
  68. 'object': instance,
  69. **self.get_extra_context(request, instance),
  70. })
  71. class ObjectChildrenView(ObjectView):
  72. """
  73. Display a table of child objects associated with the parent object.
  74. queryset: The base queryset for retrieving the *parent* object
  75. table: Table class used to render child objects list
  76. template_name: Name of the template to use
  77. """
  78. queryset = None
  79. child_model = None
  80. table = None
  81. filterset = None
  82. template_name = None
  83. def get_children(self, request, parent):
  84. """
  85. Return a QuerySet of child objects.
  86. request: The current request
  87. parent: The parent object
  88. """
  89. raise NotImplementedError(f'{self.__class__.__name__} must implement get_children()')
  90. def prep_table_data(self, request, queryset, parent):
  91. """
  92. Provides a hook for subclassed views to modify data before initializing the table.
  93. :param request: The current request
  94. :param queryset: The filtered queryset of child objects
  95. :param parent: The parent object
  96. """
  97. return queryset
  98. def get(self, request, *args, **kwargs):
  99. """
  100. GET handler for rendering child objects.
  101. """
  102. instance = get_object_or_404(self.queryset, **kwargs)
  103. child_objects = self.get_children(request, instance)
  104. if self.filterset:
  105. child_objects = self.filterset(request.GET, child_objects).qs
  106. permissions = {}
  107. for action in ('change', 'delete'):
  108. perm_name = get_permission_for_model(self.child_model, action)
  109. permissions[action] = request.user.has_perm(perm_name)
  110. table = self.table(self.prep_table_data(request, child_objects, instance), user=request.user)
  111. # Determine whether to display bulk action checkboxes
  112. if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
  113. table.columns.show('pk')
  114. paginate_table(table, request)
  115. # If this is an HTMX request, return only the rendered table HTML
  116. if is_htmx(request):
  117. return render(request, 'htmx/table.html', {
  118. 'object': instance,
  119. 'table': table,
  120. })
  121. return render(request, self.get_template_name(), {
  122. 'object': instance,
  123. 'table': table,
  124. 'permissions': permissions,
  125. **self.get_extra_context(request, instance),
  126. })
  127. class ObjectListView(ObjectPermissionRequiredMixin, View):
  128. """
  129. List a series of objects.
  130. queryset: The queryset of objects to display. Note: Prefetching related objects is not necessary, as the
  131. table will prefetch objects as needed depending on the columns being displayed.
  132. filterset: A django-filter FilterSet that is applied to the queryset
  133. filterset_form: The form used to render filter options
  134. table: The django-tables2 Table used to render the objects list
  135. template_name: The name of the template
  136. action_buttons: A list of buttons to include at the top of the page
  137. """
  138. queryset = None
  139. filterset = None
  140. filterset_form = None
  141. table = None
  142. template_name = 'generic/object_list.html'
  143. action_buttons = ('add', 'import', 'export')
  144. def get_required_permission(self):
  145. return get_permission_for_model(self.queryset.model, 'view')
  146. def get_table(self, request, permissions):
  147. """
  148. Return the django-tables2 Table instance to be used for rendering the objects list.
  149. :param request: The current request
  150. :param permissions: A dictionary mapping of the view, add, change, and delete permissions to booleans indicating
  151. whether the user has each
  152. """
  153. table = self.table(self.queryset, user=request.user)
  154. if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
  155. table.columns.show('pk')
  156. return table
  157. def export_yaml(self):
  158. """
  159. Export the queryset of objects as concatenated YAML documents.
  160. """
  161. yaml_data = [obj.to_yaml() for obj in self.queryset]
  162. return '---\n'.join(yaml_data)
  163. def export_table(self, table, columns=None):
  164. """
  165. Export all table data in CSV format.
  166. :param table: The Table instance to export
  167. :param columns: A list of specific columns to include. If not specified, all columns will be exported.
  168. """
  169. exclude_columns = {'pk', 'actions'}
  170. if columns:
  171. all_columns = [col_name for col_name, _ in table.selected_columns + table.available_columns]
  172. exclude_columns.update({
  173. col for col in all_columns if col not in columns
  174. })
  175. exporter = TableExport(
  176. export_format=TableExport.CSV,
  177. table=table,
  178. exclude_columns=exclude_columns
  179. )
  180. return exporter.response(
  181. filename=f'netbox_{self.queryset.model._meta.verbose_name_plural}.csv'
  182. )
  183. def export_template(self, template, request):
  184. """
  185. Render an ExportTemplate using the current queryset.
  186. :param template: ExportTemplate instance
  187. :param request: The current request
  188. """
  189. try:
  190. return template.render_to_response(self.queryset)
  191. except Exception as e:
  192. messages.error(request, f"There was an error rendering the selected export template ({template.name}): {e}")
  193. return redirect(request.path)
  194. def get_extra_context(self, request):
  195. """
  196. Return any additional context data for the template.
  197. :param request: The current request
  198. """
  199. return {}
  200. def get(self, request):
  201. """
  202. GET request handler.
  203. :param request: The current request
  204. """
  205. model = self.queryset.model
  206. content_type = ContentType.objects.get_for_model(model)
  207. if self.filterset:
  208. self.queryset = self.filterset(request.GET, self.queryset).qs
  209. # Compile a dictionary indicating which permissions are available to the current user for this model
  210. permissions = {}
  211. for action in ('add', 'change', 'delete', 'view'):
  212. perm_name = get_permission_for_model(model, action)
  213. permissions[action] = request.user.has_perm(perm_name)
  214. if 'export' in request.GET:
  215. # Export the current table view
  216. if request.GET['export'] == 'table':
  217. table = self.get_table(request, permissions)
  218. columns = [name for name, _ in table.selected_columns]
  219. return self.export_table(table, columns)
  220. # Render an ExportTemplate
  221. elif request.GET['export']:
  222. template = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export'])
  223. return self.export_template(template, request)
  224. # Check for YAML export support on the model
  225. elif hasattr(model, 'to_yaml'):
  226. response = HttpResponse(self.export_yaml(), content_type='text/yaml')
  227. filename = 'netbox_{}.yaml'.format(self.queryset.model._meta.verbose_name_plural)
  228. response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
  229. return response
  230. # Fall back to default table/YAML export
  231. else:
  232. table = self.get_table(request, permissions)
  233. return self.export_table(table)
  234. # Render the objects table
  235. table = self.get_table(request, permissions)
  236. paginate_table(table, request)
  237. # If this is an HTMX request, return only the rendered table HTML
  238. if is_htmx(request):
  239. return render(request, 'htmx/table.html', {
  240. 'table': table,
  241. })
  242. context = {
  243. 'content_type': content_type,
  244. 'table': table,
  245. 'permissions': permissions,
  246. 'action_buttons': self.action_buttons,
  247. 'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
  248. }
  249. context.update(self.get_extra_context(request))
  250. return render(request, self.template_name, context)
  251. class ObjectImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
  252. """
  253. Import a single object (YAML or JSON format).
  254. queryset: Base queryset for the objects being created
  255. model_form: The ModelForm used to create individual objects
  256. related_object_forms: A dictionary mapping of forms to be used for the creation of related (child) objects
  257. template_name: The name of the template
  258. """
  259. queryset = None
  260. model_form = None
  261. related_object_forms = dict()
  262. template_name = 'generic/object_import.html'
  263. def get_required_permission(self):
  264. return get_permission_for_model(self.queryset.model, 'add')
  265. def prep_related_object_data(self, parent, data):
  266. """
  267. Hook to modify the data for related objects before it's passed to the related object form (for example, to
  268. assign a parent object).
  269. """
  270. return data
  271. def _create_object(self, model_form):
  272. # Save the primary object
  273. obj = model_form.save()
  274. # Enforce object-level permissions
  275. if not self.queryset.filter(pk=obj.pk).first():
  276. raise PermissionsViolation()
  277. # Iterate through the related object forms (if any), validating and saving each instance.
  278. for field_name, related_object_form in self.related_object_forms.items():
  279. related_obj_pks = []
  280. for i, rel_obj_data in enumerate(model_form.data.get(field_name, list())):
  281. rel_obj_data = self.prep_related_object_data(obj, rel_obj_data)
  282. f = related_object_form(rel_obj_data)
  283. for subfield_name, field in f.fields.items():
  284. if subfield_name not in rel_obj_data and hasattr(field, 'initial'):
  285. f.data[subfield_name] = field.initial
  286. if f.is_valid():
  287. related_obj = f.save()
  288. related_obj_pks.append(related_obj.pk)
  289. else:
  290. # Replicate errors on the related object form to the primary form for display
  291. for subfield_name, errors in f.errors.items():
  292. for err in errors:
  293. err_msg = "{}[{}] {}: {}".format(field_name, i, subfield_name, err)
  294. model_form.add_error(None, err_msg)
  295. raise AbortTransaction()
  296. # Enforce object-level permissions on related objects
  297. model = related_object_form.Meta.model
  298. if model.objects.filter(pk__in=related_obj_pks).count() != len(related_obj_pks):
  299. raise ObjectDoesNotExist
  300. return obj
  301. def get(self, request):
  302. form = ImportForm()
  303. return render(request, self.template_name, {
  304. 'form': form,
  305. 'obj_type': self.queryset.model._meta.verbose_name,
  306. 'return_url': self.get_return_url(request),
  307. })
  308. def post(self, request):
  309. logger = logging.getLogger('netbox.views.ObjectImportView')
  310. form = ImportForm(request.POST)
  311. if form.is_valid():
  312. logger.debug("Import form validation was successful")
  313. # Initialize model form
  314. data = form.cleaned_data['data']
  315. model_form = self.model_form(data)
  316. restrict_form_fields(model_form, request.user)
  317. # Assign default values for any fields which were not specified. We have to do this manually because passing
  318. # 'initial=' to the form on initialization merely sets default values for the widgets. Since widgets are not
  319. # used for YAML/JSON import, we first bind the imported data normally, then update the form's data with the
  320. # applicable field defaults as needed prior to form validation.
  321. for field_name, field in model_form.fields.items():
  322. if field_name not in data and hasattr(field, 'initial'):
  323. model_form.data[field_name] = field.initial
  324. if model_form.is_valid():
  325. try:
  326. with transaction.atomic():
  327. obj = self._create_object(model_form)
  328. except AbortTransaction:
  329. clear_webhooks.send(sender=self)
  330. except PermissionsViolation:
  331. msg = "Object creation failed due to object-level permissions violation"
  332. logger.debug(msg)
  333. form.add_error(None, msg)
  334. clear_webhooks.send(sender=self)
  335. if not model_form.errors:
  336. logger.info(f"Import object {obj} (PK: {obj.pk})")
  337. msg = f'Imported object: <a href="{obj.get_absolute_url()}">{obj}</a>'
  338. messages.success(request, mark_safe(msg))
  339. if '_addanother' in request.POST:
  340. return redirect(request.get_full_path())
  341. return_url = form.cleaned_data.get('return_url')
  342. if return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
  343. return redirect(return_url)
  344. return redirect(self.get_return_url(request, obj))
  345. else:
  346. logger.debug("Model form validation failed")
  347. # Replicate model form errors for display
  348. for field, errors in model_form.errors.items():
  349. for err in errors:
  350. if field == '__all__':
  351. form.add_error(None, err)
  352. else:
  353. form.add_error(None, "{}: {}".format(field, err))
  354. else:
  355. logger.debug("Import form validation failed")
  356. return render(request, self.template_name, {
  357. 'form': form,
  358. 'obj_type': self.queryset.model._meta.verbose_name,
  359. 'return_url': self.get_return_url(request),
  360. })
  361. class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
  362. """
  363. Create or edit a single object.
  364. queryset: The base QuerySet for the object being modified
  365. model_form: The form used to create or edit the object
  366. template_name: The name of the template
  367. """
  368. queryset = None
  369. model_form = None
  370. template_name = 'generic/object_edit.html'
  371. def get_required_permission(self):
  372. # self._permission_action is set by dispatch() to either "add" or "change" depending on whether
  373. # we are modifying an existing object or creating a new one.
  374. return get_permission_for_model(self.queryset.model, self._permission_action)
  375. def get_object(self, **kwargs):
  376. """
  377. Return an instance for editing. If a PK has been specified, this will be an existing object.
  378. :param kwargs: URL path kwargs
  379. """
  380. if 'pk' in kwargs:
  381. obj = get_object_or_404(self.queryset, **kwargs)
  382. # Take a snapshot of change-logged models
  383. if hasattr(obj, 'snapshot'):
  384. obj.snapshot()
  385. return obj
  386. return self.queryset.model()
  387. def alter_object(self, obj, request, url_args, url_kwargs):
  388. """
  389. Provides a hook for views to modify an object before it is processed. For example, a parent object can be
  390. defined given some parameter from the request URL.
  391. :param obj: The object being edited
  392. :param request: The current request
  393. :param url_args: URL path args
  394. :param url_kwargs: URL path kwargs
  395. """
  396. return obj
  397. def dispatch(self, request, *args, **kwargs):
  398. # Determine required permission based on whether we are editing an existing object
  399. self._permission_action = 'change' if kwargs else 'add'
  400. return super().dispatch(request, *args, **kwargs)
  401. def get(self, request, *args, **kwargs):
  402. """
  403. GET request handler.
  404. :param request: The current request
  405. """
  406. obj = self.get_object(**kwargs)
  407. obj = self.alter_object(obj, request, args, kwargs)
  408. initial_data = normalize_querydict(request.GET)
  409. form = self.model_form(instance=obj, initial=initial_data)
  410. restrict_form_fields(form, request.user)
  411. return render(request, self.template_name, {
  412. 'obj': obj,
  413. 'obj_type': self.queryset.model._meta.verbose_name,
  414. 'form': form,
  415. 'return_url': self.get_return_url(request, obj),
  416. })
  417. def post(self, request, *args, **kwargs):
  418. """
  419. POST request handler.
  420. :param request: The current request
  421. """
  422. logger = logging.getLogger('netbox.views.ObjectEditView')
  423. obj = self.get_object(**kwargs)
  424. obj = self.alter_object(obj, request, args, kwargs)
  425. form = self.model_form(
  426. data=request.POST,
  427. files=request.FILES,
  428. instance=obj
  429. )
  430. restrict_form_fields(form, request.user)
  431. if form.is_valid():
  432. logger.debug("Form validation was successful")
  433. try:
  434. with transaction.atomic():
  435. object_created = form.instance.pk is None
  436. obj = form.save()
  437. # Check that the new object conforms with any assigned object-level permissions
  438. if not self.queryset.filter(pk=obj.pk).first():
  439. raise PermissionsViolation()
  440. msg = '{} {}'.format(
  441. 'Created' if object_created else 'Modified',
  442. self.queryset.model._meta.verbose_name
  443. )
  444. logger.info(f"{msg} {obj} (PK: {obj.pk})")
  445. if hasattr(obj, 'get_absolute_url'):
  446. msg = '{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), escape(obj))
  447. else:
  448. msg = '{} {}'.format(msg, escape(obj))
  449. messages.success(request, mark_safe(msg))
  450. if '_addanother' in request.POST:
  451. redirect_url = request.path
  452. # If the object has clone_fields, pre-populate a new instance of the form
  453. params = prepare_cloned_fields(obj)
  454. if 'return_url' in request.GET:
  455. params['return_url'] = request.GET.get('return_url')
  456. if params:
  457. redirect_url += f"?{params.urlencode()}"
  458. return redirect(redirect_url)
  459. return_url = self.get_return_url(request, obj)
  460. return redirect(return_url)
  461. except PermissionsViolation:
  462. msg = "Object save failed due to object-level permissions violation"
  463. logger.debug(msg)
  464. form.add_error(None, msg)
  465. clear_webhooks.send(sender=self)
  466. else:
  467. logger.debug("Form validation failed")
  468. return render(request, self.template_name, {
  469. 'obj': obj,
  470. 'obj_type': self.queryset.model._meta.verbose_name,
  471. 'form': form,
  472. 'return_url': self.get_return_url(request, obj),
  473. })
  474. class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
  475. """
  476. Delete a single object.
  477. queryset: The base queryset for the object being deleted
  478. template_name: The name of the template
  479. """
  480. queryset = None
  481. template_name = 'generic/object_delete.html'
  482. def get_required_permission(self):
  483. return get_permission_for_model(self.queryset.model, 'delete')
  484. def get_object(self, **kwargs):
  485. """
  486. Return an instance for deletion. If a PK has been specified, this will be an existing object.
  487. :param kwargs: URL path kwargs
  488. """
  489. obj = get_object_or_404(self.queryset, **kwargs)
  490. # Take a snapshot of change-logged models
  491. if hasattr(obj, 'snapshot'):
  492. obj.snapshot()
  493. return obj
  494. def get(self, request, *args, **kwargs):
  495. """
  496. GET request handler.
  497. :param request: The current request
  498. """
  499. obj = self.get_object(**kwargs)
  500. form = ConfirmationForm(initial=request.GET)
  501. return render(request, self.template_name, {
  502. 'obj': obj,
  503. 'form': form,
  504. 'obj_type': self.queryset.model._meta.verbose_name,
  505. 'return_url': self.get_return_url(request, obj),
  506. })
  507. def post(self, request, *args, **kwargs):
  508. """
  509. POST request handler.
  510. :param request: The current request
  511. """
  512. logger = logging.getLogger('netbox.views.ObjectDeleteView')
  513. obj = self.get_object(**kwargs)
  514. form = ConfirmationForm(request.POST)
  515. if form.is_valid():
  516. logger.debug("Form validation was successful")
  517. try:
  518. obj.delete()
  519. except ProtectedError as e:
  520. logger.info("Caught ProtectedError while attempting to delete object")
  521. handle_protectederror([obj], request, e)
  522. return redirect(obj.get_absolute_url())
  523. msg = 'Deleted {} {}'.format(self.queryset.model._meta.verbose_name, obj)
  524. logger.info(msg)
  525. messages.success(request, msg)
  526. return_url = form.cleaned_data.get('return_url')
  527. if return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
  528. return redirect(return_url)
  529. else:
  530. return redirect(self.get_return_url(request, obj))
  531. else:
  532. logger.debug("Form validation failed")
  533. return render(request, self.template_name, {
  534. 'obj': obj,
  535. 'form': form,
  536. 'obj_type': self.queryset.model._meta.verbose_name,
  537. 'return_url': self.get_return_url(request, obj),
  538. })
  539. #
  540. # Device/VirtualMachine components
  541. #
  542. class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
  543. """
  544. Add one or more components (e.g. interfaces, console ports, etc.) to a Device or VirtualMachine.
  545. """
  546. queryset = None
  547. form = None
  548. model_form = None
  549. template_name = 'dcim/component_create.html'
  550. patterned_fields = ('name', 'label')
  551. def get_required_permission(self):
  552. return get_permission_for_model(self.queryset.model, 'add')
  553. def alter_object(self, instance, request):
  554. return instance
  555. def initialize_forms(self, request):
  556. data = request.POST if request.method == 'POST' else None
  557. initial_data = normalize_querydict(request.GET)
  558. form = self.form(data=data, initial=request.GET)
  559. model_form = self.model_form(data=data, initial=initial_data)
  560. # These fields will be set from the pattern values
  561. for field_name in self.patterned_fields:
  562. model_form.fields[field_name].widget = HiddenInput()
  563. return form, model_form
  564. def get(self, request):
  565. form, model_form = self.initialize_forms(request)
  566. instance = self.alter_object(self.queryset.model, request)
  567. return render(request, self.template_name, {
  568. 'obj': instance,
  569. 'obj_type': self.queryset.model._meta.verbose_name,
  570. 'replication_form': form,
  571. 'form': model_form,
  572. 'return_url': self.get_return_url(request),
  573. })
  574. def post(self, request):
  575. form, model_form = self.initialize_forms(request)
  576. instance = self.alter_object(self.queryset.model, request)
  577. self.validate_form(request, form)
  578. if form.is_valid() and not form.errors:
  579. if '_addanother' in request.POST:
  580. return redirect(request.get_full_path())
  581. else:
  582. return redirect(self.get_return_url(request))
  583. return render(request, self.template_name, {
  584. 'obj': instance,
  585. 'obj_type': self.queryset.model._meta.verbose_name,
  586. 'replication_form': form,
  587. 'form': model_form,
  588. 'return_url': self.get_return_url(request),
  589. })
  590. # TODO: Refactor this method for clarity & better error reporting
  591. def validate_form(self, request, form):
  592. """
  593. Validate form values and set errors on the form object as they are detected. If
  594. no errors are found, signal success messages.
  595. """
  596. logger = logging.getLogger('netbox.views.ComponentCreateView')
  597. if form.is_valid():
  598. new_components = []
  599. data = deepcopy(request.POST)
  600. names = form.cleaned_data['name_pattern']
  601. labels = form.cleaned_data.get('label_pattern')
  602. for i, name in enumerate(names):
  603. label = labels[i] if labels else None
  604. # Initialize the individual component form
  605. data['name'] = name
  606. data['label'] = label
  607. if hasattr(form, 'get_iterative_data'):
  608. data.update(form.get_iterative_data(i))
  609. component_form = self.model_form(data)
  610. if component_form.is_valid():
  611. new_components.append(component_form)
  612. if not form.errors and not component_form.errors:
  613. try:
  614. with transaction.atomic():
  615. # Create the new components
  616. new_objs = []
  617. for component_form in new_components:
  618. obj = component_form.save()
  619. new_objs.append(obj)
  620. # Enforce object-level permissions
  621. if self.queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs):
  622. raise PermissionsViolation
  623. messages.success(request, "Added {} {}".format(
  624. len(new_components), self.queryset.model._meta.verbose_name_plural
  625. ))
  626. # Return the newly created objects so overridden post methods can use the data as needed.
  627. return new_objs
  628. except PermissionsViolation:
  629. msg = "Component creation failed due to object-level permissions violation"
  630. logger.debug(msg)
  631. form.add_error(None, msg)
  632. clear_webhooks.send(sender=self)
  633. return None