views.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870
  1. from __future__ import unicode_literals
  2. from collections import OrderedDict
  3. from copy import deepcopy
  4. from django_tables2 import RequestConfig
  5. from django.conf import settings
  6. from django.contrib import messages
  7. from django.contrib.contenttypes.models import ContentType
  8. from django.core.exceptions import ValidationError
  9. from django.db import transaction, IntegrityError
  10. from django.db.models import ProtectedError
  11. from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, TypedChoiceField
  12. from django.http import HttpResponse
  13. from django.shortcuts import get_object_or_404, redirect, render
  14. from django.template import TemplateSyntaxError
  15. from django.urls import reverse
  16. from django.utils.html import escape
  17. from django.utils.http import is_safe_url
  18. from django.utils.safestring import mark_safe
  19. from django.views.generic import View
  20. from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction
  21. from utilities.forms import BootstrapMixin, CSVDataField
  22. from .error_handlers import handle_protectederror
  23. from .forms import ConfirmationForm
  24. from .paginator import EnhancedPaginator
  25. class CustomFieldQueryset:
  26. """
  27. Annotate custom fields on objects within a QuerySet.
  28. """
  29. def __init__(self, queryset, custom_fields):
  30. self.queryset = queryset
  31. self.custom_fields = custom_fields
  32. def __iter__(self):
  33. for obj in self.queryset:
  34. values_dict = {cfv.field_id: cfv.value for cfv in obj.custom_field_values.all()}
  35. obj.custom_fields = OrderedDict([(field, values_dict.get(field.pk)) for field in self.custom_fields])
  36. yield obj
  37. class GetReturnURLMixin(object):
  38. """
  39. Provides logic for determining where a user should be redirected after processing a form.
  40. """
  41. default_return_url = None
  42. def get_return_url(self, request, obj):
  43. query_param = request.GET.get('return_url')
  44. if query_param and is_safe_url(url=query_param, host=request.get_host()):
  45. return query_param
  46. elif obj.pk and hasattr(obj, 'get_absolute_url'):
  47. return obj.get_absolute_url()
  48. elif self.default_return_url is not None:
  49. return reverse(self.default_return_url)
  50. return reverse('home')
  51. class ObjectListView(View):
  52. """
  53. List a series of objects.
  54. queryset: The queryset of objects to display
  55. filter: A django-filter FilterSet that is applied to the queryset
  56. filter_form: The form used to render filter options
  57. table: The django-tables2 Table used to render the objects list
  58. template_name: The name of the template
  59. """
  60. queryset = None
  61. filter = None
  62. filter_form = None
  63. table = None
  64. template_name = None
  65. def get(self, request):
  66. model = self.queryset.model
  67. object_ct = ContentType.objects.get_for_model(model)
  68. if self.filter:
  69. self.queryset = self.filter(request.GET, self.queryset).qs
  70. # If this type of object has one or more custom fields, prefetch any relevant custom field values
  71. custom_fields = CustomField.objects.filter(obj_type=ContentType.objects.get_for_model(model))\
  72. .prefetch_related('choices')
  73. if custom_fields:
  74. self.queryset = self.queryset.prefetch_related('custom_field_values')
  75. # Check for export template rendering
  76. if request.GET.get('export'):
  77. et = get_object_or_404(ExportTemplate, content_type=object_ct, name=request.GET.get('export'))
  78. queryset = CustomFieldQueryset(self.queryset, custom_fields) if custom_fields else self.queryset
  79. try:
  80. response = et.to_response(context_dict={'queryset': queryset},
  81. filename='netbox_{}'.format(model._meta.verbose_name_plural))
  82. return response
  83. except TemplateSyntaxError:
  84. messages.error(request, "There was an error rendering the selected export template ({})."
  85. .format(et.name))
  86. # Fall back to built-in CSV export
  87. elif 'export' in request.GET and hasattr(model, 'to_csv'):
  88. headers = getattr(model, 'csv_headers', None)
  89. output = ','.join(headers) + '\n' if headers else ''
  90. output += '\n'.join([obj.to_csv() for obj in self.queryset])
  91. response = HttpResponse(
  92. output,
  93. content_type='text/csv'
  94. )
  95. response['Content-Disposition'] = 'attachment; filename="netbox_{}.csv"'\
  96. .format(self.queryset.model._meta.verbose_name_plural)
  97. return response
  98. # Provide a hook to tweak the queryset based on the request immediately prior to rendering the object list
  99. self.queryset = self.alter_queryset(request)
  100. # Compile user model permissions for access from within the template
  101. perm_base_name = '{}.{{}}_{}'.format(model._meta.app_label, model._meta.model_name)
  102. permissions = {p: request.user.has_perm(perm_base_name.format(p)) for p in ['add', 'change', 'delete']}
  103. # Construct the table based on the user's permissions
  104. table = self.table(self.queryset)
  105. if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
  106. table.columns.show('pk')
  107. # Apply the request context
  108. paginate = {
  109. 'klass': EnhancedPaginator,
  110. 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
  111. }
  112. RequestConfig(request, paginate).configure(table)
  113. context = {
  114. 'table': table,
  115. 'permissions': permissions,
  116. 'filter_form': self.filter_form(request.GET, label_suffix='') if self.filter_form else None,
  117. 'export_templates': ExportTemplate.objects.filter(content_type=object_ct),
  118. }
  119. context.update(self.extra_context())
  120. return render(request, self.template_name, context)
  121. def alter_queryset(self, request):
  122. # .all() is necessary to avoid caching queries
  123. return self.queryset.all()
  124. def extra_context(self):
  125. return {}
  126. class ObjectEditView(GetReturnURLMixin, View):
  127. """
  128. Create or edit a single object.
  129. model: The model of the object being edited
  130. form_class: The form used to create or edit the object
  131. template_name: The name of the template
  132. default_return_url: The name of the URL used to display a list of this object type
  133. """
  134. model = None
  135. form_class = None
  136. template_name = 'utilities/obj_edit.html'
  137. def get_object(self, kwargs):
  138. # Look up object by slug or PK. Return None if neither was provided.
  139. if 'slug' in kwargs:
  140. return get_object_or_404(self.model, slug=kwargs['slug'])
  141. elif 'pk' in kwargs:
  142. return get_object_or_404(self.model, pk=kwargs['pk'])
  143. return self.model()
  144. def alter_obj(self, obj, request, url_args, url_kwargs):
  145. # Allow views to add extra info to an object before it is processed. For example, a parent object can be defined
  146. # given some parameter from the request URL.
  147. return obj
  148. def get(self, request, *args, **kwargs):
  149. obj = self.get_object(kwargs)
  150. obj = self.alter_obj(obj, request, args, kwargs)
  151. # Parse initial data manually to avoid setting field values as lists
  152. initial_data = {k: request.GET[k] for k in request.GET}
  153. form = self.form_class(instance=obj, initial=initial_data)
  154. return render(request, self.template_name, {
  155. 'obj': obj,
  156. 'obj_type': self.model._meta.verbose_name,
  157. 'form': form,
  158. 'return_url': self.get_return_url(request, obj),
  159. })
  160. def post(self, request, *args, **kwargs):
  161. obj = self.get_object(kwargs)
  162. obj = self.alter_obj(obj, request, args, kwargs)
  163. form = self.form_class(request.POST, request.FILES, instance=obj)
  164. if form.is_valid():
  165. obj_created = not form.instance.pk
  166. obj = form.save()
  167. msg = 'Created ' if obj_created else 'Modified '
  168. msg += self.model._meta.verbose_name
  169. if hasattr(obj, 'get_absolute_url'):
  170. msg = '{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), escape(obj))
  171. else:
  172. msg = '{} {}'.format(msg, escape(obj))
  173. messages.success(request, mark_safe(msg))
  174. if obj_created:
  175. UserAction.objects.log_create(request.user, obj, msg)
  176. else:
  177. UserAction.objects.log_edit(request.user, obj, msg)
  178. if '_addanother' in request.POST:
  179. return redirect(request.get_full_path())
  180. return_url = form.cleaned_data.get('return_url')
  181. if return_url is not None and is_safe_url(url=return_url, host=request.get_host()):
  182. return redirect(return_url)
  183. else:
  184. return redirect(self.get_return_url(request, obj))
  185. return render(request, self.template_name, {
  186. 'obj': obj,
  187. 'obj_type': self.model._meta.verbose_name,
  188. 'form': form,
  189. 'return_url': self.get_return_url(request, obj),
  190. })
  191. class ObjectDeleteView(GetReturnURLMixin, View):
  192. """
  193. Delete a single object.
  194. model: The model of the object being deleted
  195. template_name: The name of the template
  196. default_return_url: Name of the URL to which the user is redirected after deleting the object
  197. """
  198. model = None
  199. template_name = 'utilities/obj_delete.html'
  200. def get_object(self, kwargs):
  201. # Look up object by slug if one has been provided. Otherwise, use PK.
  202. if 'slug' in kwargs:
  203. return get_object_or_404(self.model, slug=kwargs['slug'])
  204. else:
  205. return get_object_or_404(self.model, pk=kwargs['pk'])
  206. def get(self, request, **kwargs):
  207. obj = self.get_object(kwargs)
  208. form = ConfirmationForm(initial=request.GET)
  209. return render(request, self.template_name, {
  210. 'obj': obj,
  211. 'form': form,
  212. 'obj_type': self.model._meta.verbose_name,
  213. 'return_url': self.get_return_url(request, obj),
  214. })
  215. def post(self, request, **kwargs):
  216. obj = self.get_object(kwargs)
  217. form = ConfirmationForm(request.POST)
  218. if form.is_valid():
  219. try:
  220. obj.delete()
  221. except ProtectedError as e:
  222. handle_protectederror(obj, request, e)
  223. return redirect(obj.get_absolute_url())
  224. msg = 'Deleted {} {}'.format(self.model._meta.verbose_name, obj)
  225. messages.success(request, msg)
  226. UserAction.objects.log_delete(request.user, obj, msg)
  227. return_url = form.cleaned_data.get('return_url')
  228. if return_url is not None and is_safe_url(url=return_url, host=request.get_host()):
  229. return redirect(return_url)
  230. else:
  231. return redirect(self.get_return_url(request, obj))
  232. return render(request, self.template_name, {
  233. 'obj': obj,
  234. 'form': form,
  235. 'obj_type': self.model._meta.verbose_name,
  236. 'return_url': self.get_return_url(request, obj),
  237. })
  238. class BulkCreateView(View):
  239. """
  240. Create new objects in bulk.
  241. pattern_form: Form class which provides the `pattern` field
  242. model_form: The ModelForm used to create individual objects
  243. template_name: The name of the template
  244. default_return_url: Name of the URL to which the user is redirected after creating the objects
  245. """
  246. pattern_form = None
  247. model_form = None
  248. pattern_target = ''
  249. template_name = None
  250. default_return_url = 'home'
  251. def get(self, request):
  252. pattern_form = self.pattern_form()
  253. model_form = self.model_form()
  254. return render(request, self.template_name, {
  255. 'obj_type': self.model_form._meta.model._meta.verbose_name,
  256. 'pattern_form': pattern_form,
  257. 'model_form': model_form,
  258. 'return_url': reverse(self.default_return_url),
  259. })
  260. def post(self, request):
  261. model = self.model_form._meta.model
  262. pattern_form = self.pattern_form(request.POST)
  263. model_form = self.model_form(request.POST)
  264. if pattern_form.is_valid():
  265. pattern = pattern_form.cleaned_data['pattern']
  266. new_objs = []
  267. try:
  268. with transaction.atomic():
  269. # Create objects from the expanded. Abort the transaction on the first validation error.
  270. for value in pattern:
  271. # Reinstantiate the model form each time to avoid overwriting the same instance. Use a mutable
  272. # copy of the POST QueryDict so that we can update the target field value.
  273. model_form = self.model_form(request.POST.copy())
  274. model_form.data[self.pattern_target] = value
  275. # Validate each new object independently.
  276. if model_form.is_valid():
  277. obj = model_form.save()
  278. new_objs.append(obj)
  279. else:
  280. # Copy any errors on the pattern target field to the pattern form.
  281. errors = model_form.errors.as_data()
  282. if errors.get(self.pattern_target):
  283. pattern_form.add_error('pattern', errors[self.pattern_target])
  284. # Raise an IntegrityError to break the for loop and abort the transaction.
  285. raise IntegrityError()
  286. # If we make it to this point, validation has succeeded on all new objects.
  287. msg = "Added {} {}".format(len(new_objs), model._meta.verbose_name_plural)
  288. messages.success(request, msg)
  289. UserAction.objects.log_bulk_create(request.user, ContentType.objects.get_for_model(model), msg)
  290. if '_addanother' in request.POST:
  291. return redirect(request.path)
  292. return redirect(self.default_return_url)
  293. except IntegrityError:
  294. pass
  295. return render(request, self.template_name, {
  296. 'pattern_form': pattern_form,
  297. 'model_form': model_form,
  298. 'obj_type': model._meta.verbose_name,
  299. 'return_url': reverse(self.default_return_url),
  300. })
  301. class BulkImportView(View):
  302. """
  303. Import objects in bulk (CSV format).
  304. model_form: The form used to create each imported object
  305. table: The django-tables2 Table used to render the list of imported objects
  306. template_name: The name of the template
  307. default_return_url: The name of the URL to use for the cancel button
  308. """
  309. model_form = None
  310. table = None
  311. default_return_url = None
  312. template_name = 'utilities/obj_import.html'
  313. def _import_form(self, *args, **kwargs):
  314. fields = self.model_form().fields.keys()
  315. required_fields = [name for name, field in self.model_form().fields.items() if field.required]
  316. class ImportForm(BootstrapMixin, Form):
  317. csv = CSVDataField(fields=fields, required_fields=required_fields)
  318. return ImportForm(*args, **kwargs)
  319. def _save_obj(self, obj_form):
  320. """
  321. Provide a hook to modify the object immediately before saving it (e.g. to encrypt secret data).
  322. """
  323. return obj_form.save()
  324. def get(self, request):
  325. return render(request, self.template_name, {
  326. 'form': self._import_form(),
  327. 'fields': self.model_form().fields,
  328. 'obj_type': self.model_form._meta.model._meta.verbose_name,
  329. 'return_url': self.default_return_url,
  330. })
  331. def post(self, request):
  332. new_objs = []
  333. form = self._import_form(request.POST)
  334. if form.is_valid():
  335. try:
  336. # Iterate through CSV data and bind each row to a new model form instance.
  337. with transaction.atomic():
  338. for row, data in enumerate(form.cleaned_data['csv'], start=1):
  339. obj_form = self.model_form(data)
  340. if obj_form.is_valid():
  341. obj = self._save_obj(obj_form)
  342. new_objs.append(obj)
  343. else:
  344. for field, err in obj_form.errors.items():
  345. form.add_error('csv', "Row {} {}: {}".format(row, field, err[0]))
  346. raise ValidationError("")
  347. # Compile a table containing the imported objects
  348. obj_table = self.table(new_objs)
  349. if new_objs:
  350. msg = 'Imported {} {}'.format(len(new_objs), new_objs[0]._meta.verbose_name_plural)
  351. messages.success(request, msg)
  352. UserAction.objects.log_import(request.user, ContentType.objects.get_for_model(new_objs[0]), msg)
  353. return render(request, "import_success.html", {
  354. 'table': obj_table,
  355. 'return_url': self.default_return_url,
  356. })
  357. except ValidationError:
  358. pass
  359. return render(request, self.template_name, {
  360. 'form': form,
  361. 'fields': self.model_form().fields,
  362. 'obj_type': self.model_form._meta.model._meta.verbose_name,
  363. 'return_url': self.default_return_url,
  364. })
  365. class BulkEditView(View):
  366. """
  367. Edit objects in bulk.
  368. cls: The model of the objects being edited
  369. parent_cls: The model of the parent object (if any)
  370. queryset: Custom queryset to use when retrieving objects (e.g. to select related objects)
  371. filter: FilterSet to apply when deleting by QuerySet
  372. table: The table used to display devices being edited
  373. form: The form class used to edit objects in bulk
  374. template_name: The name of the template
  375. default_return_url: Name of the URL to which the user is redirected after editing the objects (can be overridden by
  376. POSTing return_url)
  377. """
  378. cls = None
  379. parent_cls = None
  380. queryset = None
  381. filter = None
  382. table = None
  383. form = None
  384. template_name = 'utilities/obj_bulk_edit.html'
  385. default_return_url = 'home'
  386. def get(self):
  387. return redirect(self.default_return_url)
  388. def post(self, request, **kwargs):
  389. # Attempt to derive parent object if a parent class has been given
  390. if self.parent_cls:
  391. parent_obj = get_object_or_404(self.parent_cls, **kwargs)
  392. else:
  393. parent_obj = None
  394. # Determine URL to redirect users upon modification of objects
  395. posted_return_url = request.POST.get('return_url')
  396. if posted_return_url and is_safe_url(url=posted_return_url, host=request.get_host()):
  397. return_url = posted_return_url
  398. elif parent_obj:
  399. return_url = parent_obj.get_absolute_url()
  400. else:
  401. return_url = reverse(self.default_return_url)
  402. # Are we editing *all* objects in the queryset or just a selected subset?
  403. if request.POST.get('_all') and self.filter is not None:
  404. pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk')).qs]
  405. else:
  406. pk_list = [int(pk) for pk in request.POST.getlist('pk')]
  407. if '_apply' in request.POST:
  408. form = self.form(self.cls, request.POST)
  409. if form.is_valid():
  410. custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else []
  411. standard_fields = [field for field in form.fields if field not in custom_fields and field != 'pk']
  412. # Update standard fields. If a field is listed in _nullify, delete its value.
  413. nullified_fields = request.POST.getlist('_nullify')
  414. fields_to_update = {}
  415. for field in standard_fields:
  416. if field in form.nullable_fields and field in nullified_fields:
  417. if isinstance(form.fields[field], CharField):
  418. fields_to_update[field] = ''
  419. else:
  420. fields_to_update[field] = None
  421. elif form.cleaned_data[field] not in (None, ''):
  422. fields_to_update[field] = form.cleaned_data[field]
  423. updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
  424. # Update custom fields for objects
  425. if custom_fields:
  426. objs_updated = self.update_custom_fields(pk_list, form, custom_fields, nullified_fields)
  427. if objs_updated and not updated_count:
  428. updated_count = objs_updated
  429. if updated_count:
  430. msg = 'Updated {} {}'.format(updated_count, self.cls._meta.verbose_name_plural)
  431. messages.success(self.request, msg)
  432. UserAction.objects.log_bulk_edit(request.user, ContentType.objects.get_for_model(self.cls), msg)
  433. return redirect(return_url)
  434. else:
  435. initial_data = request.POST.copy()
  436. initial_data['pk'] = pk_list
  437. form = self.form(self.cls, initial=initial_data)
  438. # Retrieve objects being edited
  439. queryset = self.queryset or self.cls.objects.all()
  440. table = self.table(queryset.filter(pk__in=pk_list), orderable=False)
  441. if not table.rows:
  442. messages.warning(request, "No {} were selected.".format(self.cls._meta.verbose_name_plural))
  443. return redirect(return_url)
  444. return render(request, self.template_name, {
  445. 'form': form,
  446. 'table': table,
  447. 'obj_type_plural': self.cls._meta.verbose_name_plural,
  448. 'return_url': return_url,
  449. })
  450. def update_custom_fields(self, pk_list, form, fields, nullified_fields):
  451. obj_type = ContentType.objects.get_for_model(self.cls)
  452. objs_updated = False
  453. for name in fields:
  454. field = form.fields[name].model
  455. # Setting the field to null
  456. if name in form.nullable_fields and name in nullified_fields:
  457. # Delete all CustomFieldValues for instances of this field belonging to the selected objects.
  458. CustomFieldValue.objects.filter(field=field, obj_type=obj_type, obj_id__in=pk_list).delete()
  459. objs_updated = True
  460. # Updating the value of the field
  461. elif form.cleaned_data[name] not in [None, '']:
  462. # Check for zero value (bulk editing)
  463. if isinstance(form.fields[name], TypedChoiceField) and form.cleaned_data[name] == 0:
  464. serialized_value = field.serialize_value(None)
  465. else:
  466. serialized_value = field.serialize_value(form.cleaned_data[name])
  467. # Gather any pre-existing CustomFieldValues for the objects being edited.
  468. existing_cfvs = CustomFieldValue.objects.filter(field=field, obj_type=obj_type, obj_id__in=pk_list)
  469. # Determine which objects have an existing CFV to update and which need a new CFV created.
  470. update_list = [cfv['obj_id'] for cfv in existing_cfvs.values()]
  471. create_list = list(set(pk_list) - set(update_list))
  472. # Creating/updating CFVs
  473. if serialized_value:
  474. existing_cfvs.update(serialized_value=serialized_value)
  475. CustomFieldValue.objects.bulk_create([
  476. CustomFieldValue(field=field, obj_type=obj_type, obj_id=pk, serialized_value=serialized_value)
  477. for pk in create_list
  478. ])
  479. # Deleting CFVs
  480. else:
  481. existing_cfvs.delete()
  482. objs_updated = True
  483. return len(pk_list) if objs_updated else 0
  484. class BulkDeleteView(View):
  485. """
  486. Delete objects in bulk.
  487. cls: The model of the objects being deleted
  488. parent_cls: The model of the parent object (if any)
  489. queryset: Custom queryset to use when retrieving objects (e.g. to select related objects)
  490. filter: FilterSet to apply when deleting by QuerySet
  491. table: The table used to display devices being deleted
  492. form: The form class used to delete objects in bulk
  493. template_name: The name of the template
  494. default_return_url: Name of the URL to which the user is redirected after deleting the objects (can be overriden by
  495. POSTing return_url)
  496. """
  497. cls = None
  498. parent_cls = None
  499. queryset = None
  500. filter = None
  501. table = None
  502. form = None
  503. template_name = 'utilities/obj_bulk_delete.html'
  504. default_return_url = 'home'
  505. def post(self, request, **kwargs):
  506. # Attempt to derive parent object if a parent class has been given
  507. if self.parent_cls:
  508. parent_obj = get_object_or_404(self.parent_cls, **kwargs)
  509. else:
  510. parent_obj = None
  511. # Determine URL to redirect users upon deletion of objects
  512. posted_return_url = request.POST.get('return_url')
  513. if posted_return_url and is_safe_url(url=posted_return_url, host=request.get_host()):
  514. return_url = posted_return_url
  515. elif parent_obj:
  516. return_url = parent_obj.get_absolute_url()
  517. else:
  518. return_url = reverse(self.default_return_url)
  519. # Are we deleting *all* objects in the queryset or just a selected subset?
  520. if request.POST.get('_all') and self.filter is not None:
  521. pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk')).qs]
  522. else:
  523. pk_list = [int(pk) for pk in request.POST.getlist('pk')]
  524. form_cls = self.get_form()
  525. if '_confirm' in request.POST:
  526. form = form_cls(request.POST)
  527. if form.is_valid():
  528. # Delete objects
  529. queryset = self.cls.objects.filter(pk__in=pk_list)
  530. try:
  531. deleted_count = queryset.delete()[1][self.cls._meta.label]
  532. except ProtectedError as e:
  533. handle_protectederror(list(queryset), request, e)
  534. return redirect(return_url)
  535. msg = 'Deleted {} {}'.format(deleted_count, self.cls._meta.verbose_name_plural)
  536. messages.success(request, msg)
  537. UserAction.objects.log_bulk_delete(request.user, ContentType.objects.get_for_model(self.cls), msg)
  538. return redirect(return_url)
  539. else:
  540. form = form_cls(initial={'pk': pk_list, 'return_url': return_url})
  541. # Retrieve objects being deleted
  542. queryset = self.queryset or self.cls.objects.all()
  543. table = self.table(queryset.filter(pk__in=pk_list), orderable=False)
  544. if not table.rows:
  545. messages.warning(request, "No {} were selected for deletion.".format(self.cls._meta.verbose_name_plural))
  546. return redirect(return_url)
  547. return render(request, self.template_name, {
  548. 'form': form,
  549. 'parent_obj': parent_obj,
  550. 'obj_type_plural': self.cls._meta.verbose_name_plural,
  551. 'table': table,
  552. 'return_url': return_url,
  553. })
  554. def get_form(self):
  555. """
  556. Provide a standard bulk delete form if none has been specified for the view
  557. """
  558. class BulkDeleteForm(ConfirmationForm):
  559. pk = ModelMultipleChoiceField(queryset=self.cls.objects.all(), widget=MultipleHiddenInput)
  560. if self.form:
  561. return self.form
  562. return BulkDeleteForm
  563. #
  564. # Device/VirtualMachine components
  565. #
  566. class ComponentCreateView(View):
  567. """
  568. Add one or more components (e.g. interfaces, console ports, etc.) to a Device or VirtualMachine.
  569. """
  570. parent_model = None
  571. parent_field = None
  572. model = None
  573. form = None
  574. model_form = None
  575. template_name = None
  576. def get(self, request, pk):
  577. parent = get_object_or_404(self.parent_model, pk=pk)
  578. form = self.form(parent, initial=request.GET)
  579. return render(request, self.template_name, {
  580. 'parent': parent,
  581. 'component_type': self.model._meta.verbose_name,
  582. 'form': form,
  583. 'return_url': parent.get_absolute_url(),
  584. })
  585. def post(self, request, pk):
  586. parent = get_object_or_404(self.parent_model, pk=pk)
  587. form = self.form(parent, request.POST)
  588. if form.is_valid():
  589. new_components = []
  590. data = deepcopy(form.cleaned_data)
  591. for name in form.cleaned_data['name_pattern']:
  592. component_data = {
  593. self.parent_field: parent.pk,
  594. 'name': name,
  595. }
  596. # Replace objects with their primary key to keep component_form.clean() happy
  597. for k, v in data.items():
  598. if hasattr(v, 'pk'):
  599. component_data[k] = v.pk
  600. else:
  601. component_data[k] = v
  602. component_form = self.model_form(component_data)
  603. if component_form.is_valid():
  604. new_components.append(component_form.save(commit=False))
  605. else:
  606. for field, errors in component_form.errors.as_data().items():
  607. # Assign errors on the child form's name field to name_pattern on the parent form
  608. if field == 'name':
  609. field = 'name_pattern'
  610. for e in errors:
  611. form.add_error(field, '{}: {}'.format(name, ', '.join(e)))
  612. if not form.errors:
  613. self.model.objects.bulk_create(new_components)
  614. messages.success(request, "Added {} {} to {}.".format(
  615. len(new_components), self.model._meta.verbose_name_plural, parent
  616. ))
  617. if '_addanother' in request.POST:
  618. return redirect(request.path)
  619. else:
  620. return redirect(parent.get_absolute_url())
  621. return render(request, self.template_name, {
  622. 'parent': parent,
  623. 'component_type': self.model._meta.verbose_name,
  624. 'form': form,
  625. 'return_url': parent.get_absolute_url(),
  626. })
  627. class ComponentEditView(ObjectEditView):
  628. parent_field = None
  629. def get_return_url(self, request, obj):
  630. return getattr(obj, self.parent_field).get_absolute_url()
  631. class ComponentDeleteView(ObjectDeleteView):
  632. parent_field = None
  633. def get_return_url(self, request, obj):
  634. return getattr(obj, self.parent_field).get_absolute_url()
  635. class BulkComponentCreateView(View):
  636. """
  637. Add one or more components (e.g. interfaces, console ports, etc.) to a set of Devices or VirtualMachines.
  638. """
  639. parent_model = None
  640. parent_field = None
  641. form = None
  642. model = None
  643. model_form = None
  644. filter = None
  645. table = None
  646. template_name = 'utilities/obj_bulk_add_component.html'
  647. default_return_url = 'home'
  648. def post(self, request):
  649. # Are we editing *all* objects in the queryset or just a selected subset?
  650. if request.POST.get('_all') and self.filter is not None:
  651. pk_list = [obj.pk for obj in self.filter(request.GET, self.model.objects.only('pk')).qs]
  652. else:
  653. pk_list = [int(pk) for pk in request.POST.getlist('pk')]
  654. # Determine URL to redirect users upon modification of objects
  655. posted_return_url = request.POST.get('return_url')
  656. if posted_return_url and is_safe_url(url=posted_return_url, host=request.get_host()):
  657. return_url = posted_return_url
  658. else:
  659. return_url = reverse(self.default_return_url)
  660. selected_objects = self.parent_model.objects.filter(pk__in=pk_list)
  661. if not selected_objects:
  662. messages.warning(request, "No {} were selected.".format(self.parent_model._meta.verbose_name_plural))
  663. return redirect(return_url)
  664. table = self.table(selected_objects)
  665. if '_create' in request.POST:
  666. form = self.form(request.POST)
  667. if form.is_valid():
  668. new_components = []
  669. data = deepcopy(form.cleaned_data)
  670. for obj in data['pk']:
  671. names = data['name_pattern']
  672. for name in names:
  673. component_data = {
  674. self.parent_field: obj.pk,
  675. 'name': name,
  676. }
  677. component_data.update(data)
  678. component_form = self.model_form(component_data)
  679. if component_form.is_valid():
  680. new_components.append(component_form.save(commit=False))
  681. else:
  682. for field, errors in component_form.errors.as_data().items():
  683. for e in errors:
  684. form.add_error(field, '{} {}: {}'.format(obj, name, ', '.join(e)))
  685. if not form.errors:
  686. self.model.objects.bulk_create(new_components)
  687. messages.success(request, "Added {} {} to {} {}.".format(
  688. len(new_components),
  689. self.model._meta.verbose_name_plural,
  690. len(form.cleaned_data['pk']),
  691. self.parent_model._meta.verbose_name_plural
  692. ))
  693. return redirect(return_url)
  694. else:
  695. form = self.form(initial={'pk': pk_list})
  696. return render(request, self.template_name, {
  697. 'form': form,
  698. 'component_name': self.model._meta.verbose_name_plural,
  699. 'table': table,
  700. 'return_url': reverse('dcim:device_list'),
  701. })