views.py 35 KB

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