views.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. from django import template
  2. from django.conf import settings
  3. from django.contrib import messages
  4. from django.contrib.auth.mixins import PermissionRequiredMixin
  5. from django.contrib.contenttypes.models import ContentType
  6. from django.db.models import Count, Q
  7. from django.http import Http404, HttpResponseForbidden
  8. from django.shortcuts import get_object_or_404, redirect, render
  9. from django.utils.safestring import mark_safe
  10. from django.views.generic import View
  11. from django_tables2 import RequestConfig
  12. from utilities.forms import ConfirmationForm
  13. from utilities.paginator import EnhancedPaginator
  14. from utilities.utils import shallow_compare_dict
  15. from utilities.views import BulkDeleteView, BulkEditView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView
  16. from . import filters, forms
  17. from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult, Tag, TaggedItem
  18. from .reports import get_report, get_reports
  19. from .scripts import get_scripts, run_script
  20. from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemTable
  21. #
  22. # Tags
  23. #
  24. class TagListView(ObjectListView):
  25. queryset = Tag.objects.annotate(
  26. items=Count('extras_taggeditem_items', distinct=True)
  27. ).order_by(
  28. 'name'
  29. )
  30. filterset = filters.TagFilterSet
  31. filterset_form = forms.TagFilterForm
  32. table = TagTable
  33. action_buttons = ()
  34. class TagView(ObjectView):
  35. queryset = Tag.objects.all()
  36. def get(self, request, slug):
  37. tag = get_object_or_404(self.queryset, slug=slug)
  38. tagged_items = TaggedItem.objects.filter(
  39. tag=tag
  40. ).prefetch_related(
  41. 'content_type', 'content_object'
  42. )
  43. # Generate a table of all items tagged with this Tag
  44. items_table = TaggedItemTable(tagged_items)
  45. paginate = {
  46. 'paginator_class': EnhancedPaginator,
  47. 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
  48. }
  49. RequestConfig(request, paginate).configure(items_table)
  50. return render(request, 'extras/tag.html', {
  51. 'tag': tag,
  52. 'items_count': tagged_items.count(),
  53. 'items_table': items_table,
  54. })
  55. class TagEditView(ObjectEditView):
  56. queryset = Tag.objects.all()
  57. model_form = forms.TagForm
  58. default_return_url = 'extras:tag_list'
  59. template_name = 'extras/tag_edit.html'
  60. class TagDeleteView(ObjectDeleteView):
  61. queryset = Tag.objects.all()
  62. default_return_url = 'extras:tag_list'
  63. class TagBulkEditView(BulkEditView):
  64. queryset = Tag.objects.annotate(
  65. items=Count('extras_taggeditem_items', distinct=True)
  66. ).order_by(
  67. 'name'
  68. )
  69. table = TagTable
  70. form = forms.TagBulkEditForm
  71. default_return_url = 'extras:tag_list'
  72. class TagBulkDeleteView(BulkDeleteView):
  73. queryset = Tag.objects.annotate(
  74. items=Count('extras_taggeditem_items')
  75. ).order_by(
  76. 'name'
  77. )
  78. table = TagTable
  79. default_return_url = 'extras:tag_list'
  80. #
  81. # Config contexts
  82. #
  83. class ConfigContextListView(ObjectListView):
  84. queryset = ConfigContext.objects.all()
  85. filterset = filters.ConfigContextFilterSet
  86. filterset_form = forms.ConfigContextFilterForm
  87. table = ConfigContextTable
  88. action_buttons = ('add',)
  89. class ConfigContextView(ObjectView):
  90. queryset = ConfigContext.objects.all()
  91. def get(self, request, pk):
  92. configcontext = get_object_or_404(self.queryset, pk=pk)
  93. # Determine user's preferred output format
  94. if request.GET.get('format') in ['json', 'yaml']:
  95. format = request.GET.get('format')
  96. if request.user.is_authenticated:
  97. request.user.config.set('extras.configcontext.format', format, commit=True)
  98. elif request.user.is_authenticated:
  99. format = request.user.config.get('extras.configcontext.format', 'json')
  100. else:
  101. format = 'json'
  102. return render(request, 'extras/configcontext.html', {
  103. 'configcontext': configcontext,
  104. 'format': format,
  105. })
  106. class ConfigContextEditView(ObjectEditView):
  107. queryset = ConfigContext.objects.all()
  108. model_form = forms.ConfigContextForm
  109. default_return_url = 'extras:configcontext_list'
  110. template_name = 'extras/configcontext_edit.html'
  111. class ConfigContextBulkEditView(BulkEditView):
  112. queryset = ConfigContext.objects.all()
  113. filterset = filters.ConfigContextFilterSet
  114. table = ConfigContextTable
  115. form = forms.ConfigContextBulkEditForm
  116. default_return_url = 'extras:configcontext_list'
  117. class ConfigContextDeleteView(ObjectDeleteView):
  118. queryset = ConfigContext.objects.all()
  119. default_return_url = 'extras:configcontext_list'
  120. class ConfigContextBulkDeleteView(BulkDeleteView):
  121. queryset = ConfigContext.objects.all()
  122. table = ConfigContextTable
  123. default_return_url = 'extras:configcontext_list'
  124. class ObjectConfigContextView(ObjectView):
  125. base_template = None
  126. def get(self, request, pk):
  127. obj = get_object_or_404(self.queryset, pk=pk)
  128. source_contexts = ConfigContext.objects.restrict(request.user, 'view').get_for_object(obj)
  129. model_name = self.queryset.model._meta.model_name
  130. # Determine user's preferred output format
  131. if request.GET.get('format') in ['json', 'yaml']:
  132. format = request.GET.get('format')
  133. if request.user.is_authenticated:
  134. request.user.config.set('extras.configcontext.format', format, commit=True)
  135. elif request.user.is_authenticated:
  136. format = request.user.config.get('extras.configcontext.format', 'json')
  137. else:
  138. format = 'json'
  139. return render(request, 'extras/object_configcontext.html', {
  140. model_name: obj,
  141. 'obj': obj,
  142. 'rendered_context': obj.get_config_context(),
  143. 'source_contexts': source_contexts,
  144. 'format': format,
  145. 'base_template': self.base_template,
  146. 'active_tab': 'config-context',
  147. })
  148. #
  149. # Change logging
  150. #
  151. class ObjectChangeListView(ObjectListView):
  152. queryset = ObjectChange.objects.prefetch_related('user', 'changed_object_type')
  153. filterset = filters.ObjectChangeFilterSet
  154. filterset_form = forms.ObjectChangeFilterForm
  155. table = ObjectChangeTable
  156. template_name = 'extras/objectchange_list.html'
  157. action_buttons = ('export',)
  158. class ObjectChangeView(ObjectView):
  159. queryset = ObjectChange.objects.all()
  160. def get(self, request, pk):
  161. objectchange = get_object_or_404(self.queryset, pk=pk)
  162. related_changes = ObjectChange.objects.restrict(request.user, 'view').filter(
  163. request_id=objectchange.request_id
  164. ).exclude(
  165. pk=objectchange.pk
  166. )
  167. related_changes_table = ObjectChangeTable(
  168. data=related_changes[:50],
  169. orderable=False
  170. )
  171. objectchanges = ObjectChange.objects.restrict(request.user, 'view').filter(
  172. changed_object_type=objectchange.changed_object_type,
  173. changed_object_id=objectchange.changed_object_id,
  174. )
  175. next_change = objectchanges.filter(time__gt=objectchange.time).order_by('time').first()
  176. prev_change = objectchanges.filter(time__lt=objectchange.time).order_by('-time').first()
  177. if prev_change:
  178. diff_added = shallow_compare_dict(
  179. prev_change.object_data,
  180. objectchange.object_data,
  181. exclude=['last_updated'],
  182. )
  183. diff_removed = {x: prev_change.object_data.get(x) for x in diff_added}
  184. else:
  185. # No previous change; this is the initial change that added the object
  186. diff_added = diff_removed = objectchange.object_data
  187. return render(request, 'extras/objectchange.html', {
  188. 'objectchange': objectchange,
  189. 'diff_added': diff_added,
  190. 'diff_removed': diff_removed,
  191. 'next_change': next_change,
  192. 'prev_change': prev_change,
  193. 'related_changes_table': related_changes_table,
  194. 'related_changes_count': related_changes.count()
  195. })
  196. class ObjectChangeLogView(View):
  197. """
  198. Present a history of changes made to a particular object.
  199. """
  200. def get(self, request, model, **kwargs):
  201. # Get object my model and kwargs (e.g. slug='foo')
  202. obj = get_object_or_404(model, **kwargs)
  203. # Gather all changes for this object (and its related objects)
  204. content_type = ContentType.objects.get_for_model(model)
  205. objectchanges = ObjectChange.objects.restrict(request.user, 'view').prefetch_related(
  206. 'user', 'changed_object_type'
  207. ).filter(
  208. Q(changed_object_type=content_type, changed_object_id=obj.pk) |
  209. Q(related_object_type=content_type, related_object_id=obj.pk)
  210. )
  211. objectchanges_table = ObjectChangeTable(
  212. data=objectchanges,
  213. orderable=False
  214. )
  215. # Apply the request context
  216. paginate = {
  217. 'paginator_class': EnhancedPaginator,
  218. 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
  219. }
  220. RequestConfig(request, paginate).configure(objectchanges_table)
  221. # Check whether a header template exists for this model
  222. base_template = '{}/{}.html'.format(model._meta.app_label, model._meta.model_name)
  223. try:
  224. template.loader.get_template(base_template)
  225. object_var = model._meta.model_name
  226. except template.TemplateDoesNotExist:
  227. base_template = 'base.html'
  228. object_var = 'obj'
  229. return render(request, 'extras/object_changelog.html', {
  230. object_var: obj,
  231. 'table': objectchanges_table,
  232. 'base_template': base_template,
  233. 'active_tab': 'changelog',
  234. })
  235. #
  236. # Image attachments
  237. #
  238. class ImageAttachmentEditView(ObjectEditView):
  239. queryset = ImageAttachment.objects.all()
  240. model_form = forms.ImageAttachmentForm
  241. def alter_obj(self, imageattachment, request, args, kwargs):
  242. if not imageattachment.pk:
  243. # Assign the parent object based on URL kwargs
  244. model = kwargs.get('model')
  245. imageattachment.parent = get_object_or_404(model, pk=kwargs['object_id'])
  246. return imageattachment
  247. def get_return_url(self, request, imageattachment):
  248. return imageattachment.parent.get_absolute_url()
  249. class ImageAttachmentDeleteView(ObjectDeleteView):
  250. queryset = ImageAttachment.objects.all()
  251. def get_return_url(self, request, imageattachment):
  252. return imageattachment.parent.get_absolute_url()
  253. #
  254. # Reports
  255. #
  256. class ReportListView(PermissionRequiredMixin, View):
  257. """
  258. Retrieve all of the available reports from disk and the recorded ReportResult (if any) for each.
  259. """
  260. permission_required = 'extras.view_reportresult'
  261. def get(self, request):
  262. reports = get_reports()
  263. results = {r.report: r for r in ReportResult.objects.all()}
  264. ret = []
  265. for module, report_list in reports:
  266. module_reports = []
  267. for report in report_list:
  268. report.result = results.get(report.full_name, None)
  269. module_reports.append(report)
  270. ret.append((module, module_reports))
  271. return render(request, 'extras/report_list.html', {
  272. 'reports': ret,
  273. })
  274. class ReportView(PermissionRequiredMixin, View):
  275. """
  276. Display a single Report and its associated ReportResult (if any).
  277. """
  278. permission_required = 'extras.view_reportresult'
  279. def get(self, request, name):
  280. # Retrieve the Report by "<module>.<report>"
  281. module_name, report_name = name.split('.')
  282. report = get_report(module_name, report_name)
  283. if report is None:
  284. raise Http404
  285. # Attach the ReportResult (if any)
  286. report.result = ReportResult.objects.filter(report=report.full_name).first()
  287. return render(request, 'extras/report.html', {
  288. 'report': report,
  289. 'run_form': ConfirmationForm(),
  290. })
  291. class ReportRunView(PermissionRequiredMixin, View):
  292. """
  293. Run a Report and record a new ReportResult.
  294. """
  295. permission_required = 'extras.add_reportresult'
  296. def post(self, request, name):
  297. # Retrieve the Report by "<module>.<report>"
  298. module_name, report_name = name.split('.')
  299. report = get_report(module_name, report_name)
  300. if report is None:
  301. raise Http404
  302. form = ConfirmationForm(request.POST)
  303. if form.is_valid():
  304. # Run the Report. A new ReportResult is created.
  305. report.run()
  306. result = 'failed' if report.failed else 'passed'
  307. msg = "Ran report {} ({})".format(report.full_name, result)
  308. messages.success(request, mark_safe(msg))
  309. return redirect('extras:report', name=report.full_name)
  310. #
  311. # Scripts
  312. #
  313. class ScriptListView(PermissionRequiredMixin, View):
  314. permission_required = 'extras.view_script'
  315. def get(self, request):
  316. return render(request, 'extras/script_list.html', {
  317. 'scripts': get_scripts(use_names=True),
  318. })
  319. class ScriptView(PermissionRequiredMixin, View):
  320. permission_required = 'extras.view_script'
  321. def _get_script(self, module, name):
  322. scripts = get_scripts()
  323. try:
  324. return scripts[module][name]()
  325. except KeyError:
  326. raise Http404
  327. def get(self, request, module, name):
  328. script = self._get_script(module, name)
  329. form = script.as_form(initial=request.GET)
  330. return render(request, 'extras/script.html', {
  331. 'module': module,
  332. 'script': script,
  333. 'form': form,
  334. })
  335. def post(self, request, module, name):
  336. # Permissions check
  337. if not request.user.has_perm('extras.run_script'):
  338. return HttpResponseForbidden()
  339. script = self._get_script(module, name)
  340. form = script.as_form(request.POST, request.FILES)
  341. output = None
  342. execution_time = None
  343. if form.is_valid():
  344. commit = form.cleaned_data.pop('_commit')
  345. output, execution_time = run_script(script, form.cleaned_data, request, commit)
  346. return render(request, 'extras/script.html', {
  347. 'module': module,
  348. 'script': script,
  349. 'form': form,
  350. 'output': output,
  351. 'execution_time': execution_time,
  352. })