views.py 15 KB

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