views.py 32 KB

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