2
0

views.py 38 KB

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