views.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. from django_tables2 import RequestConfig
  2. from django.contrib import messages
  3. from django.contrib.admin.views.decorators import staff_member_required
  4. from django.contrib.contenttypes.models import ContentType
  5. from django.core.urlresolvers import reverse
  6. from django.db import transaction, IntegrityError
  7. from django.db.models import ProtectedError
  8. from django.http import HttpResponse, HttpResponseRedirect
  9. from django.shortcuts import get_object_or_404, redirect, render
  10. from django.template import TemplateSyntaxError
  11. from django.utils.decorators import method_decorator
  12. from django.utils.http import is_safe_url
  13. from django.views.generic import View
  14. from extras.models import ExportTemplate, UserAction
  15. from .error_handlers import handle_protectederror
  16. from .forms import ConfirmationForm
  17. from .paginator import EnhancedPaginator
  18. class ObjectListView(View):
  19. queryset = None
  20. filter = None
  21. filter_form = None
  22. table = None
  23. edit_permissions = []
  24. template_name = None
  25. redirect_on_single_result = True
  26. def get(self, request, *args, **kwargs):
  27. model = self.queryset.model
  28. object_ct = ContentType.objects.get_for_model(model)
  29. if self.filter:
  30. self.queryset = self.filter(request.GET, self.queryset).qs
  31. # Check for export template rendering
  32. if request.GET.get('export'):
  33. et = get_object_or_404(ExportTemplate, content_type=object_ct, name=request.GET.get('export'))
  34. try:
  35. response = et.to_response(context_dict={'queryset': self.queryset.all()},
  36. filename='netbox_{}'.format(self.queryset.model._meta.verbose_name_plural))
  37. return response
  38. except TemplateSyntaxError:
  39. messages.error(request, "There was an error rendering the selected export template ({})."
  40. .format(et.name))
  41. # Fall back to built-in CSV export
  42. elif 'export' in request.GET and hasattr(model, 'to_csv'):
  43. output = '\n'.join([obj.to_csv() for obj in self.queryset.all()])
  44. response = HttpResponse(
  45. output,
  46. content_type='text/csv'
  47. )
  48. response['Content-Disposition'] = 'attachment; filename="netbox_{}.csv"'\
  49. .format(self.queryset.model._meta.verbose_name_plural)
  50. return response
  51. # Attempt to redirect automatically if the search query returns a single result
  52. if self.redirect_on_single_result and self.queryset.count() == 1 and request.GET:
  53. try:
  54. return HttpResponseRedirect(self.queryset[0].get_absolute_url())
  55. except AttributeError:
  56. pass
  57. # Provide a hook to tweak the queryset based on the request immediately prior to rendering the object list
  58. self.queryset = self.alter_queryset(request)
  59. # Construct the table based on the user's permissions
  60. table = self.table(self.queryset)
  61. table.model = model
  62. if 'pk' in table.base_columns and any([request.user.has_perm(perm) for perm in self.edit_permissions]):
  63. table.base_columns['pk'].visible = True
  64. RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(table)
  65. context = {
  66. 'table': table,
  67. 'filter_form': self.filter_form(request.GET, label_suffix='') if self.filter_form else None,
  68. 'export_templates': ExportTemplate.objects.filter(content_type=object_ct),
  69. }
  70. context.update(self.extra_context())
  71. return render(request, self.template_name, context)
  72. def alter_queryset(self, request):
  73. # .all() is necessary to avoid caching queries
  74. return self.queryset.all()
  75. def extra_context(self):
  76. return {}
  77. class ObjectEditView(View):
  78. model = None
  79. form_class = None
  80. fields_initial = []
  81. template_name = 'utilities/obj_edit.html'
  82. success_url = None
  83. cancel_url = None
  84. def get_object(self, kwargs):
  85. # Look up object by slug if one has been provided. Otherwise, use PK.
  86. if 'slug' in kwargs:
  87. return get_object_or_404(self.model, slug=kwargs['slug'])
  88. else:
  89. return get_object_or_404(self.model, pk=kwargs['pk'])
  90. def get(self, request, *args, **kwargs):
  91. if kwargs:
  92. obj = self.get_object(kwargs)
  93. form = self.form_class(instance=obj)
  94. else:
  95. obj = None
  96. form = self.form_class(initial={k: request.GET.get(k) for k in self.fields_initial})
  97. return render(request, self.template_name, {
  98. 'obj': obj,
  99. 'obj_type': self.model._meta.verbose_name,
  100. 'form': form,
  101. 'cancel_url': obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else reverse(self.cancel_url),
  102. })
  103. def post(self, request, *args, **kwargs):
  104. # Validate object if editing an existing object
  105. obj = self.get_object(kwargs) if kwargs else None
  106. form = self.form_class(request.POST, instance=obj)
  107. if form.is_valid():
  108. obj = form.save(commit=False)
  109. obj_created = not obj.pk
  110. obj.save()
  111. msg = 'Created ' if obj_created else 'Modified '
  112. msg += self.model._meta.verbose_name
  113. if hasattr(obj, 'get_absolute_url'):
  114. msg = '{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), obj)
  115. else:
  116. msg = '{} {}'.format(msg, obj)
  117. messages.success(request, msg)
  118. if obj_created:
  119. UserAction.objects.log_create(request.user, obj, msg)
  120. else:
  121. UserAction.objects.log_edit(request.user, obj, msg)
  122. if '_addanother' in request.POST:
  123. return redirect(request.path)
  124. elif self.success_url:
  125. return redirect(self.success_url)
  126. else:
  127. return redirect(obj.get_absolute_url())
  128. return render(request, self.template_name, {
  129. 'obj': obj,
  130. 'obj_type': self.model._meta.verbose_name,
  131. 'form': form,
  132. 'cancel_url': obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else reverse(self.cancel_url),
  133. })
  134. class ObjectDeleteView(View):
  135. model = None
  136. template_name = 'utilities/obj_delete.html'
  137. redirect_url = None
  138. def get_object(self, kwargs):
  139. # Look up object by slug if one has been provided. Otherwise, use PK.
  140. if 'slug' in kwargs:
  141. return get_object_or_404(self.model, slug=kwargs['slug'])
  142. else:
  143. return get_object_or_404(self.model, pk=kwargs['pk'])
  144. def get(self, request, *args, **kwargs):
  145. obj = self.get_object(kwargs)
  146. form = ConfirmationForm()
  147. return render(request, self.template_name, {
  148. 'obj': obj,
  149. 'form': form,
  150. 'obj_type': self.model._meta.verbose_name,
  151. 'cancel_url': obj.get_absolute_url(),
  152. })
  153. def post(self, request, *args, **kwargs):
  154. obj = self.get_object(kwargs)
  155. form = ConfirmationForm(request.POST)
  156. if form.is_valid():
  157. try:
  158. obj.delete()
  159. msg = 'Deleted {} {}'.format(self.model._meta.verbose_name, obj)
  160. messages.success(request, msg)
  161. UserAction.objects.log_delete(request.user, obj, msg)
  162. return redirect(self.redirect_url)
  163. except ProtectedError, e:
  164. handle_protectederror(obj, request, e)
  165. return redirect(obj.get_absolute_url())
  166. return render(request, self.template_name, {
  167. 'obj': obj,
  168. 'form': form,
  169. 'obj_type': self.model._meta.verbose_name,
  170. 'cancel_url': obj.get_absolute_url(),
  171. })
  172. class BulkImportView(View):
  173. form = None
  174. table = None
  175. template_name = None
  176. obj_list_url = None
  177. def get(self, request, *args, **kwargs):
  178. return render(request, self.template_name, {
  179. 'form': self.form(),
  180. 'obj_list_url': self.obj_list_url,
  181. })
  182. def post(self, request, *args, **kwargs):
  183. form = self.form(request.POST)
  184. if form.is_valid():
  185. new_objs = []
  186. try:
  187. with transaction.atomic():
  188. for obj in form.cleaned_data['csv']:
  189. self.save_obj(obj)
  190. new_objs.append(obj)
  191. obj_table = self.table(new_objs)
  192. if new_objs:
  193. msg = 'Imported {} {}'.format(len(new_objs), new_objs[0]._meta.verbose_name_plural)
  194. messages.success(request, msg)
  195. UserAction.objects.log_import(request.user, ContentType.objects.get_for_model(new_objs[0]), msg)
  196. return render(request, "import_success.html", {
  197. 'table': obj_table,
  198. })
  199. except IntegrityError as e:
  200. form.add_error('csv', "Record {}: {}".format(len(new_objs) + 1, e.__cause__))
  201. return render(request, self.template_name, {
  202. 'form': form,
  203. 'obj_list_url': self.obj_list_url,
  204. })
  205. def save_obj(self, obj):
  206. obj.save()
  207. class BulkEditView(View):
  208. cls = None
  209. form = None
  210. template_name = None
  211. default_redirect_url = None
  212. def get(self, request, *args, **kwargs):
  213. return redirect(self.default_redirect_url)
  214. def post(self, request, *args, **kwargs):
  215. posted_redirect_url = request.POST.get('redirect_url')
  216. if posted_redirect_url and is_safe_url(url=posted_redirect_url, host=request.get_host()):
  217. redirect_url = posted_redirect_url
  218. else:
  219. redirect_url = reverse(self.default_redirect_url)
  220. if request.POST.get('_all'):
  221. pk_list = [x for x in request.POST.get('pk_all').split(',') if x]
  222. else:
  223. pk_list = request.POST.getlist('pk')
  224. if '_apply' in request.POST:
  225. form = self.form(request.POST)
  226. if form.is_valid():
  227. updated_count = self.update_objects(pk_list, form)
  228. if updated_count:
  229. msg = 'Updated {} {}'.format(updated_count, self.cls._meta.verbose_name_plural)
  230. messages.success(self.request, msg)
  231. UserAction.objects.log_bulk_edit(request.user, ContentType.objects.get_for_model(self.cls), msg)
  232. return redirect(redirect_url)
  233. else:
  234. form = self.form(initial={'pk': pk_list})
  235. selected_objects = self.cls.objects.filter(pk__in=pk_list)
  236. if not selected_objects:
  237. messages.warning(request, "No {} were selected.".format(self.cls._meta.verbose_name_plural))
  238. return redirect(redirect_url)
  239. return render(request, self.template_name, {
  240. 'form': form,
  241. 'selected_objects': selected_objects,
  242. 'cancel_url': redirect_url,
  243. })
  244. def update_objects(self, obj_list, form):
  245. """
  246. This method provides the update logic (must be overridden by subclasses).
  247. """
  248. raise NotImplementedError()
  249. class BulkDeleteView(View):
  250. cls = None
  251. form = None
  252. template_name = 'utilities/confirm_bulk_delete.html'
  253. default_redirect_url = None
  254. @method_decorator(staff_member_required)
  255. def dispatch(self, *args, **kwargs):
  256. return super(BulkDeleteView, self).dispatch(*args, **kwargs)
  257. def get(self, request, *args, **kwargs):
  258. return redirect(self.default_redirect_url)
  259. def post(self, request, *args, **kwargs):
  260. posted_redirect_url = request.POST.get('redirect_url')
  261. if posted_redirect_url and is_safe_url(url=posted_redirect_url, host=request.get_host()):
  262. redirect_url = posted_redirect_url
  263. else:
  264. redirect_url = reverse(self.default_redirect_url)
  265. if request.POST.get('_all'):
  266. pk_list = [x for x in request.POST.get('pk_all').split(',') if x]
  267. else:
  268. pk_list = request.POST.getlist('pk')
  269. if '_confirm' in request.POST:
  270. form = self.form(request.POST)
  271. if form.is_valid():
  272. # Delete objects
  273. queryset = self.cls.objects.filter(pk__in=pk_list)
  274. try:
  275. deleted_count = queryset.delete()[1][self.cls._meta.label]
  276. except ProtectedError, e:
  277. handle_protectederror(list(queryset), request, e)
  278. return redirect(redirect_url)
  279. msg = 'Deleted {} {}'.format(deleted_count, self.cls._meta.verbose_name_plural)
  280. messages.success(request, msg)
  281. UserAction.objects.log_bulk_delete(request.user, ContentType.objects.get_for_model(self.cls), msg)
  282. return redirect(redirect_url)
  283. else:
  284. form = self.form(initial={'pk': pk_list})
  285. selected_objects = self.cls.objects.filter(pk__in=pk_list)
  286. if not selected_objects:
  287. messages.warning(request, "No {} were selected for deletion.".format(self.cls._meta.verbose_name_plural))
  288. return redirect(redirect_url)
  289. return render(request, self.template_name, {
  290. 'form': form,
  291. 'obj_type_plural': self.cls._meta.verbose_name_plural,
  292. 'selected_objects': selected_objects,
  293. 'cancel_url': redirect_url,
  294. })