views.py 32 KB

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