views.py 65 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930
  1. from datetime import datetime
  2. from django.contrib import messages
  3. from django.contrib.auth.mixins import LoginRequiredMixin
  4. from django.contrib.contenttypes.models import ContentType
  5. from django.core.paginator import EmptyPage
  6. from django.db.models import Count, Q
  7. from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
  8. from django.shortcuts import get_object_or_404, redirect, render
  9. from django.urls import reverse
  10. from django.utils import timezone
  11. from django.utils.module_loading import import_string
  12. from django.utils.translation import gettext_lazy as _
  13. from django.views.generic import View
  14. from core.choices import ManagedFileRootPathChoices
  15. from core.models import Job
  16. from core.object_actions import BulkSync
  17. from dcim.models import Device, DeviceRole, Platform
  18. from extras.choices import LogLevelChoices
  19. from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
  20. from extras.dashboard.utils import get_widget_class
  21. from extras.utils import SharedObjectViewMixin
  22. from netbox.object_actions import *
  23. from netbox.ui import layout
  24. from netbox.ui.panels import (
  25. CommentsPanel,
  26. ContextTablePanel,
  27. JSONPanel,
  28. TemplatePanel,
  29. TextCodePanel,
  30. )
  31. from netbox.views import generic
  32. from netbox.views.generic.mixins import TableMixin
  33. from utilities.forms import ConfirmationForm, get_field_value
  34. from utilities.htmx import htmx_maybe_redirect_current_page, htmx_partial
  35. from utilities.paginator import EnhancedPaginator, get_paginate_count
  36. from utilities.query import count_related
  37. from utilities.querydict import normalize_querydict
  38. from utilities.request import copy_safe_request
  39. from utilities.rqworker import any_workers_for_queue
  40. from utilities.templatetags.builtins.filters import render_markdown
  41. from utilities.views import ContentTypePermissionRequiredMixin, get_action_url, register_model_view
  42. from virtualization.models import VirtualMachine
  43. from . import filtersets, forms, tables
  44. from .constants import LOG_LEVEL_RANK
  45. from .models import *
  46. from .tables import ReportResultsTable, ScriptJobTable, ScriptResultsTable
  47. from .ui import panels
  48. #
  49. # Custom fields
  50. #
  51. @register_model_view(CustomField, 'list', path='', detail=False)
  52. class CustomFieldListView(generic.ObjectListView):
  53. queryset = CustomField.objects.select_related('choice_set')
  54. filterset = filtersets.CustomFieldFilterSet
  55. filterset_form = forms.CustomFieldFilterForm
  56. table = tables.CustomFieldTable
  57. @register_model_view(CustomField)
  58. class CustomFieldView(generic.ObjectView):
  59. queryset = CustomField.objects.select_related('choice_set')
  60. template_name = 'generic/object.html'
  61. layout = layout.SimpleLayout(
  62. left_panels=[
  63. panels.CustomFieldPanel(),
  64. panels.CustomFieldBehaviorPanel(),
  65. CommentsPanel(),
  66. ],
  67. right_panels=[
  68. panels.CustomFieldObjectTypesPanel(),
  69. panels.CustomFieldValidationPanel(),
  70. panels.CustomFieldRelatedObjectsPanel(),
  71. ],
  72. )
  73. def get_extra_context(self, request, instance):
  74. related_models = ()
  75. for object_type in instance.object_types.all():
  76. related_models += (
  77. object_type.model_class().objects.restrict(request.user, 'view').exclude(
  78. Q(**{f'custom_field_data__{instance.name}': ''}) |
  79. Q(**{f'custom_field_data__{instance.name}': None})
  80. ),
  81. )
  82. return {
  83. 'related_models': related_models
  84. }
  85. @register_model_view(CustomField, 'add', detail=False)
  86. @register_model_view(CustomField, 'edit')
  87. class CustomFieldEditView(generic.ObjectEditView):
  88. queryset = CustomField.objects.select_related('choice_set')
  89. form = forms.CustomFieldForm
  90. @register_model_view(CustomField, 'delete')
  91. class CustomFieldDeleteView(generic.ObjectDeleteView):
  92. queryset = CustomField.objects.select_related('choice_set')
  93. @register_model_view(CustomField, 'bulk_import', path='import', detail=False)
  94. class CustomFieldBulkImportView(generic.BulkImportView):
  95. queryset = CustomField.objects.select_related('choice_set')
  96. model_form = forms.CustomFieldImportForm
  97. @register_model_view(CustomField, 'bulk_edit', path='edit', detail=False)
  98. class CustomFieldBulkEditView(generic.BulkEditView):
  99. queryset = CustomField.objects.select_related('choice_set')
  100. filterset = filtersets.CustomFieldFilterSet
  101. table = tables.CustomFieldTable
  102. form = forms.CustomFieldBulkEditForm
  103. @register_model_view(CustomField, 'bulk_rename', path='rename', detail=False)
  104. class CustomFieldBulkRenameView(generic.BulkRenameView):
  105. queryset = CustomField.objects.all()
  106. filterset = filtersets.CustomFieldFilterSet
  107. @register_model_view(CustomField, 'bulk_delete', path='delete', detail=False)
  108. class CustomFieldBulkDeleteView(generic.BulkDeleteView):
  109. queryset = CustomField.objects.select_related('choice_set')
  110. filterset = filtersets.CustomFieldFilterSet
  111. table = tables.CustomFieldTable
  112. #
  113. # Custom field choices
  114. #
  115. @register_model_view(CustomFieldChoiceSet, 'list', path='', detail=False)
  116. class CustomFieldChoiceSetListView(generic.ObjectListView):
  117. queryset = CustomFieldChoiceSet.objects.all()
  118. filterset = filtersets.CustomFieldChoiceSetFilterSet
  119. filterset_form = forms.CustomFieldChoiceSetFilterForm
  120. table = tables.CustomFieldChoiceSetTable
  121. @register_model_view(CustomFieldChoiceSet)
  122. class CustomFieldChoiceSetView(generic.ObjectView):
  123. queryset = CustomFieldChoiceSet.objects.all()
  124. template_name = 'generic/object.html'
  125. layout = layout.SimpleLayout(
  126. left_panels=[
  127. panels.CustomFieldChoiceSetPanel(),
  128. ],
  129. right_panels=[
  130. panels.CustomFieldChoiceSetChoicesPanel(),
  131. ],
  132. )
  133. def get_extra_context(self, request, instance):
  134. # Paginate choices list
  135. per_page = get_paginate_count(request)
  136. try:
  137. page_number = request.GET.get('page', 1)
  138. except ValueError:
  139. page_number = 1
  140. choice_rows = [
  141. {
  142. 'value': value,
  143. 'label': label,
  144. 'color': instance.get_choice_color(value),
  145. }
  146. for value, label in instance.choices
  147. ]
  148. paginator = EnhancedPaginator(choice_rows, per_page)
  149. try:
  150. choices = paginator.page(page_number)
  151. except EmptyPage:
  152. choices = paginator.page(paginator.num_pages)
  153. return {
  154. 'paginator': paginator,
  155. 'choices': choices,
  156. }
  157. @register_model_view(CustomFieldChoiceSet, 'add', detail=False)
  158. @register_model_view(CustomFieldChoiceSet, 'edit')
  159. class CustomFieldChoiceSetEditView(generic.ObjectEditView):
  160. queryset = CustomFieldChoiceSet.objects.all()
  161. form = forms.CustomFieldChoiceSetForm
  162. @register_model_view(CustomFieldChoiceSet, 'delete')
  163. class CustomFieldChoiceSetDeleteView(generic.ObjectDeleteView):
  164. queryset = CustomFieldChoiceSet.objects.all()
  165. @register_model_view(CustomFieldChoiceSet, 'bulk_import', path='import', detail=False)
  166. class CustomFieldChoiceSetBulkImportView(generic.BulkImportView):
  167. queryset = CustomFieldChoiceSet.objects.all()
  168. model_form = forms.CustomFieldChoiceSetImportForm
  169. @register_model_view(CustomFieldChoiceSet, 'bulk_edit', path='edit', detail=False)
  170. class CustomFieldChoiceSetBulkEditView(generic.BulkEditView):
  171. queryset = CustomFieldChoiceSet.objects.all()
  172. filterset = filtersets.CustomFieldChoiceSetFilterSet
  173. table = tables.CustomFieldChoiceSetTable
  174. form = forms.CustomFieldChoiceSetBulkEditForm
  175. @register_model_view(CustomFieldChoiceSet, 'bulk_rename', path='rename', detail=False)
  176. class CustomFieldChoiceSetBulkRenameView(generic.BulkRenameView):
  177. queryset = CustomFieldChoiceSet.objects.all()
  178. filterset = filtersets.CustomFieldChoiceSetFilterSet
  179. @register_model_view(CustomFieldChoiceSet, 'bulk_delete', path='delete', detail=False)
  180. class CustomFieldChoiceSetBulkDeleteView(generic.BulkDeleteView):
  181. queryset = CustomFieldChoiceSet.objects.all()
  182. filterset = filtersets.CustomFieldChoiceSetFilterSet
  183. table = tables.CustomFieldChoiceSetTable
  184. #
  185. # Custom links
  186. #
  187. @register_model_view(CustomLink, 'list', path='', detail=False)
  188. class CustomLinkListView(generic.ObjectListView):
  189. queryset = CustomLink.objects.all()
  190. filterset = filtersets.CustomLinkFilterSet
  191. filterset_form = forms.CustomLinkFilterForm
  192. table = tables.CustomLinkTable
  193. @register_model_view(CustomLink)
  194. class CustomLinkView(generic.ObjectView):
  195. queryset = CustomLink.objects.all()
  196. template_name = 'generic/object.html'
  197. layout = layout.SimpleLayout(
  198. left_panels=[
  199. panels.CustomLinkPanel(),
  200. panels.ObjectTypesPanel(title=_('Assigned Models')),
  201. ],
  202. right_panels=[
  203. TextCodePanel('link_text', title=_('Link Text')),
  204. TextCodePanel('link_url', title=_('Link URL')),
  205. ],
  206. )
  207. @register_model_view(CustomLink, 'add', detail=False)
  208. @register_model_view(CustomLink, 'edit')
  209. class CustomLinkEditView(generic.ObjectEditView):
  210. queryset = CustomLink.objects.all()
  211. form = forms.CustomLinkForm
  212. @register_model_view(CustomLink, 'delete')
  213. class CustomLinkDeleteView(generic.ObjectDeleteView):
  214. queryset = CustomLink.objects.all()
  215. @register_model_view(CustomLink, 'bulk_import', path='import', detail=False)
  216. class CustomLinkBulkImportView(generic.BulkImportView):
  217. queryset = CustomLink.objects.all()
  218. model_form = forms.CustomLinkImportForm
  219. @register_model_view(CustomLink, 'bulk_edit', path='edit', detail=False)
  220. class CustomLinkBulkEditView(generic.BulkEditView):
  221. queryset = CustomLink.objects.all()
  222. filterset = filtersets.CustomLinkFilterSet
  223. table = tables.CustomLinkTable
  224. form = forms.CustomLinkBulkEditForm
  225. @register_model_view(CustomLink, 'bulk_rename', path='rename', detail=False)
  226. class CustomLinkBulkRenameView(generic.BulkRenameView):
  227. queryset = CustomLink.objects.all()
  228. filterset = filtersets.CustomLinkFilterSet
  229. @register_model_view(CustomLink, 'bulk_delete', path='delete', detail=False)
  230. class CustomLinkBulkDeleteView(generic.BulkDeleteView):
  231. queryset = CustomLink.objects.all()
  232. filterset = filtersets.CustomLinkFilterSet
  233. table = tables.CustomLinkTable
  234. #
  235. # Export templates
  236. #
  237. @register_model_view(ExportTemplate, 'list', path='', detail=False)
  238. class ExportTemplateListView(generic.ObjectListView):
  239. queryset = ExportTemplate.objects.all()
  240. filterset = filtersets.ExportTemplateFilterSet
  241. filterset_form = forms.ExportTemplateFilterForm
  242. table = tables.ExportTemplateTable
  243. actions = (AddObject, BulkImport, BulkSync, BulkExport, BulkEdit, BulkRename, BulkDelete)
  244. @register_model_view(ExportTemplate)
  245. class ExportTemplateView(generic.ObjectView):
  246. queryset = ExportTemplate.objects.all()
  247. template_name = 'generic/object.html'
  248. layout = layout.SimpleLayout(
  249. left_panels=[
  250. panels.ExportTemplatePanel(),
  251. TemplatePanel('core/inc/datafile_panel.html'),
  252. ],
  253. right_panels=[
  254. panels.ObjectTypesPanel(title=_('Assigned Models')),
  255. JSONPanel('environment_params', title=_('Environment Parameters')),
  256. ],
  257. bottom_panels=[
  258. TextCodePanel('template_code', title=_('Template'), show_sync_warning=True),
  259. ],
  260. )
  261. @register_model_view(ExportTemplate, 'add', detail=False)
  262. @register_model_view(ExportTemplate, 'edit')
  263. class ExportTemplateEditView(generic.ObjectEditView):
  264. queryset = ExportTemplate.objects.all()
  265. form = forms.ExportTemplateForm
  266. @register_model_view(ExportTemplate, 'delete')
  267. class ExportTemplateDeleteView(generic.ObjectDeleteView):
  268. queryset = ExportTemplate.objects.all()
  269. @register_model_view(ExportTemplate, 'bulk_import', path='import', detail=False)
  270. class ExportTemplateBulkImportView(generic.BulkImportView):
  271. queryset = ExportTemplate.objects.all()
  272. model_form = forms.ExportTemplateImportForm
  273. @register_model_view(ExportTemplate, 'bulk_edit', path='edit', detail=False)
  274. class ExportTemplateBulkEditView(generic.BulkEditView):
  275. queryset = ExportTemplate.objects.all()
  276. filterset = filtersets.ExportTemplateFilterSet
  277. table = tables.ExportTemplateTable
  278. form = forms.ExportTemplateBulkEditForm
  279. @register_model_view(ExportTemplate, 'bulk_rename', path='rename', detail=False)
  280. class ExportTemplateBulkRenameView(generic.BulkRenameView):
  281. queryset = ExportTemplate.objects.all()
  282. filterset = filtersets.ExportTemplateFilterSet
  283. @register_model_view(ExportTemplate, 'bulk_delete', path='delete', detail=False)
  284. class ExportTemplateBulkDeleteView(generic.BulkDeleteView):
  285. queryset = ExportTemplate.objects.all()
  286. filterset = filtersets.ExportTemplateFilterSet
  287. table = tables.ExportTemplateTable
  288. @register_model_view(ExportTemplate, 'bulk_sync', path='sync', detail=False)
  289. class ExportTemplateBulkSyncDataView(generic.BulkSyncDataView):
  290. queryset = ExportTemplate.objects.all()
  291. #
  292. # Saved filters
  293. #
  294. @register_model_view(SavedFilter, 'list', path='', detail=False)
  295. class SavedFilterListView(SharedObjectViewMixin, generic.ObjectListView):
  296. queryset = SavedFilter.objects.all()
  297. filterset = filtersets.SavedFilterFilterSet
  298. filterset_form = forms.SavedFilterFilterForm
  299. table = tables.SavedFilterTable
  300. @register_model_view(SavedFilter)
  301. class SavedFilterView(SharedObjectViewMixin, generic.ObjectView):
  302. queryset = SavedFilter.objects.all()
  303. template_name = 'generic/object.html'
  304. layout = layout.SimpleLayout(
  305. left_panels=[
  306. panels.SavedFilterPanel(),
  307. panels.SavedFilterObjectTypesPanel(),
  308. ],
  309. right_panels=[
  310. JSONPanel('parameters', title=_('Parameters')),
  311. ],
  312. )
  313. @register_model_view(SavedFilter, 'add', detail=False)
  314. @register_model_view(SavedFilter, 'edit')
  315. class SavedFilterEditView(SharedObjectViewMixin, generic.ObjectEditView):
  316. queryset = SavedFilter.objects.all()
  317. form = forms.SavedFilterForm
  318. def alter_object(self, obj, request, url_args, url_kwargs):
  319. if not obj.pk:
  320. obj.user = request.user
  321. return obj
  322. @register_model_view(SavedFilter, 'delete')
  323. class SavedFilterDeleteView(SharedObjectViewMixin, generic.ObjectDeleteView):
  324. queryset = SavedFilter.objects.all()
  325. @register_model_view(SavedFilter, 'bulk_import', path='import', detail=False)
  326. class SavedFilterBulkImportView(SharedObjectViewMixin, generic.BulkImportView):
  327. queryset = SavedFilter.objects.all()
  328. model_form = forms.SavedFilterImportForm
  329. @register_model_view(SavedFilter, 'bulk_edit', path='edit', detail=False)
  330. class SavedFilterBulkEditView(SharedObjectViewMixin, generic.BulkEditView):
  331. queryset = SavedFilter.objects.all()
  332. filterset = filtersets.SavedFilterFilterSet
  333. table = tables.SavedFilterTable
  334. form = forms.SavedFilterBulkEditForm
  335. @register_model_view(SavedFilter, 'bulk_rename', path='rename', detail=False)
  336. class SavedFilterBulkRenameView(generic.BulkRenameView):
  337. queryset = SavedFilter.objects.all()
  338. filterset = filtersets.SavedFilterFilterSet
  339. @register_model_view(SavedFilter, 'bulk_delete', path='delete', detail=False)
  340. class SavedFilterBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView):
  341. queryset = SavedFilter.objects.all()
  342. filterset = filtersets.SavedFilterFilterSet
  343. table = tables.SavedFilterTable
  344. #
  345. # Table configs
  346. #
  347. @register_model_view(TableConfig, 'list', path='', detail=False)
  348. class TableConfigListView(SharedObjectViewMixin, generic.ObjectListView):
  349. queryset = TableConfig.objects.all()
  350. filterset = filtersets.TableConfigFilterSet
  351. filterset_form = forms.TableConfigFilterForm
  352. table = tables.TableConfigTable
  353. actions = (BulkExport, BulkEdit, BulkRename, BulkDelete)
  354. @register_model_view(TableConfig)
  355. class TableConfigView(SharedObjectViewMixin, generic.ObjectView):
  356. queryset = TableConfig.objects.all()
  357. template_name = 'generic/object.html'
  358. layout = layout.SimpleLayout(
  359. left_panels=[
  360. panels.TableConfigPanel(),
  361. ],
  362. right_panels=[
  363. panels.TableConfigColumnsPanel(),
  364. panels.TableConfigOrderingPanel(),
  365. ],
  366. )
  367. def get_extra_context(self, request, instance):
  368. table = instance.table_class([])
  369. return {
  370. 'columns': dict(table.columns.items()),
  371. }
  372. @register_model_view(TableConfig, 'add', detail=False)
  373. @register_model_view(TableConfig, 'edit')
  374. class TableConfigEditView(SharedObjectViewMixin, generic.ObjectEditView):
  375. queryset = TableConfig.objects.all()
  376. form = forms.TableConfigForm
  377. template_name = 'extras/tableconfig_edit.html'
  378. def alter_object(self, obj, request, url_args, url_kwargs):
  379. if not obj.pk:
  380. obj.user = request.user
  381. return obj
  382. @register_model_view(TableConfig, 'delete')
  383. class TableConfigDeleteView(SharedObjectViewMixin, generic.ObjectDeleteView):
  384. queryset = TableConfig.objects.all()
  385. @register_model_view(TableConfig, 'bulk_edit', path='edit', detail=False)
  386. class TableConfigBulkEditView(SharedObjectViewMixin, generic.BulkEditView):
  387. queryset = TableConfig.objects.all()
  388. filterset = filtersets.TableConfigFilterSet
  389. table = tables.TableConfigTable
  390. form = forms.TableConfigBulkEditForm
  391. @register_model_view(TableConfig, 'bulk_rename', path='rename', detail=False)
  392. class TableConfigBulkRenameView(generic.BulkRenameView):
  393. queryset = TableConfig.objects.all()
  394. filterset = filtersets.TableConfigFilterSet
  395. @register_model_view(TableConfig, 'bulk_delete', path='delete', detail=False)
  396. class TableConfigBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView):
  397. queryset = TableConfig.objects.all()
  398. filterset = filtersets.TableConfigFilterSet
  399. table = tables.TableConfigTable
  400. #
  401. # Bookmarks
  402. #
  403. @register_model_view(Bookmark, 'add', detail=False)
  404. class BookmarkCreateView(generic.ObjectEditView):
  405. form = forms.BookmarkForm
  406. def get_queryset(self, request):
  407. return Bookmark.objects.filter(user=request.user)
  408. def alter_object(self, obj, request, url_args, url_kwargs):
  409. obj.user = request.user
  410. return obj
  411. @register_model_view(Bookmark, 'delete')
  412. class BookmarkDeleteView(generic.ObjectDeleteView):
  413. def get_queryset(self, request):
  414. return Bookmark.objects.filter(user=request.user)
  415. @register_model_view(Bookmark, 'bulk_delete', path='delete', detail=False)
  416. class BookmarkBulkDeleteView(generic.BulkDeleteView):
  417. table = tables.BookmarkTable
  418. def get_queryset(self, request):
  419. return Bookmark.objects.filter(user=request.user)
  420. #
  421. # Notification groups
  422. #
  423. @register_model_view(NotificationGroup, 'list', path='', detail=False)
  424. class NotificationGroupListView(generic.ObjectListView):
  425. queryset = NotificationGroup.objects.all()
  426. filterset = filtersets.NotificationGroupFilterSet
  427. filterset_form = forms.NotificationGroupFilterForm
  428. table = tables.NotificationGroupTable
  429. @register_model_view(NotificationGroup)
  430. class NotificationGroupView(generic.ObjectView):
  431. queryset = NotificationGroup.objects.all()
  432. template_name = 'generic/object.html'
  433. layout = layout.SimpleLayout(
  434. left_panels=[
  435. panels.NotificationGroupPanel(),
  436. ],
  437. right_panels=[
  438. panels.NotificationGroupGroupsPanel(),
  439. panels.NotificationGroupUsersPanel(),
  440. ],
  441. )
  442. @register_model_view(NotificationGroup, 'add', detail=False)
  443. @register_model_view(NotificationGroup, 'edit')
  444. class NotificationGroupEditView(generic.ObjectEditView):
  445. queryset = NotificationGroup.objects.all()
  446. form = forms.NotificationGroupForm
  447. @register_model_view(NotificationGroup, 'delete')
  448. class NotificationGroupDeleteView(generic.ObjectDeleteView):
  449. queryset = NotificationGroup.objects.all()
  450. @register_model_view(NotificationGroup, 'bulk_import', path='import', detail=False)
  451. class NotificationGroupBulkImportView(generic.BulkImportView):
  452. queryset = NotificationGroup.objects.all()
  453. model_form = forms.NotificationGroupImportForm
  454. @register_model_view(NotificationGroup, 'bulk_edit', path='edit', detail=False)
  455. class NotificationGroupBulkEditView(generic.BulkEditView):
  456. queryset = NotificationGroup.objects.all()
  457. filterset = filtersets.NotificationGroupFilterSet
  458. table = tables.NotificationGroupTable
  459. form = forms.NotificationGroupBulkEditForm
  460. @register_model_view(NotificationGroup, 'bulk_rename', path='rename', detail=False)
  461. class NotificationGroupBulkRenameView(generic.BulkRenameView):
  462. queryset = NotificationGroup.objects.all()
  463. filterset = filtersets.NotificationGroupFilterSet
  464. @register_model_view(NotificationGroup, 'bulk_delete', path='delete', detail=False)
  465. class NotificationGroupBulkDeleteView(generic.BulkDeleteView):
  466. queryset = NotificationGroup.objects.all()
  467. filterset = filtersets.NotificationGroupFilterSet
  468. table = tables.NotificationGroupTable
  469. #
  470. # Notifications
  471. #
  472. class NotificationsView(LoginRequiredMixin, View):
  473. """
  474. HTMX-only user-specific notifications list.
  475. """
  476. def get(self, request):
  477. return render(request, 'htmx/notifications.html', {
  478. 'notifications': request.user.notifications.unread()[:10],
  479. 'total_count': request.user.notifications.count(),
  480. 'unread_count': request.user.notifications.unread().count(),
  481. })
  482. @register_model_view(Notification, 'read')
  483. class NotificationReadView(LoginRequiredMixin, View):
  484. """
  485. Mark the Notification read and redirect the user to its attached object.
  486. """
  487. def get(self, request, pk):
  488. # Mark the Notification as read
  489. notification = get_object_or_404(request.user.notifications, pk=pk)
  490. notification.read = timezone.now()
  491. notification.save()
  492. # Redirect to the object if it has a URL (deleted objects will not)
  493. if hasattr(notification.object, 'get_absolute_url'):
  494. return redirect(notification.object.get_absolute_url())
  495. return redirect('account:notifications')
  496. @register_model_view(Notification, name='dismiss_all', path='dismiss-all', detail=False)
  497. class NotificationDismissAllView(LoginRequiredMixin, View):
  498. """
  499. Convenience view to clear all *unread* notifications for the current user.
  500. """
  501. def get(self, request):
  502. request.user.notifications.unread().delete()
  503. if htmx_partial(request):
  504. # If a user is currently on the notification page, redirect there (full repaint)
  505. redirect_resp = htmx_maybe_redirect_current_page(request, 'account:notifications', preserve_query=True)
  506. if redirect_resp:
  507. return redirect_resp
  508. return render(request, 'htmx/notifications.html', {
  509. 'notifications': request.user.notifications.unread()[:10],
  510. 'total_count': request.user.notifications.count(),
  511. 'unread_count': request.user.notifications.unread().count(),
  512. })
  513. return redirect('account:notifications')
  514. @register_model_view(Notification, 'dismiss')
  515. class NotificationDismissView(LoginRequiredMixin, View):
  516. """
  517. A convenience view which allows deleting notifications with one click.
  518. """
  519. def get(self, request, pk):
  520. notification = get_object_or_404(request.user.notifications, pk=pk)
  521. notification.delete()
  522. if htmx_partial(request):
  523. # If a user is currently on the notification page, redirect there (full repaint)
  524. redirect_resp = htmx_maybe_redirect_current_page(request, 'account:notifications', preserve_query=True)
  525. if redirect_resp:
  526. return redirect_resp
  527. return render(request, 'htmx/notifications.html', {
  528. 'notifications': request.user.notifications.unread()[:10],
  529. 'total_count': request.user.notifications.count(),
  530. 'unread_count': request.user.notifications.unread().count(),
  531. })
  532. return redirect('account:notifications')
  533. @register_model_view(Notification, 'delete')
  534. class NotificationDeleteView(generic.ObjectDeleteView):
  535. def get_queryset(self, request):
  536. return Notification.objects.filter(user=request.user)
  537. @register_model_view(Notification, 'bulk_delete', path='delete', detail=False)
  538. class NotificationBulkDeleteView(generic.BulkDeleteView):
  539. table = tables.NotificationTable
  540. def get_queryset(self, request):
  541. return Notification.objects.filter(user=request.user)
  542. #
  543. # Subscriptions
  544. #
  545. @register_model_view(Subscription, 'add', detail=False)
  546. class SubscriptionCreateView(generic.ObjectEditView):
  547. form = forms.SubscriptionForm
  548. def get_queryset(self, request):
  549. return Subscription.objects.filter(user=request.user)
  550. def alter_object(self, obj, request, url_args, url_kwargs):
  551. obj.user = request.user
  552. return obj
  553. @register_model_view(Subscription, 'delete')
  554. class SubscriptionDeleteView(generic.ObjectDeleteView):
  555. def get_queryset(self, request):
  556. return Subscription.objects.filter(user=request.user)
  557. @register_model_view(Subscription, 'bulk_delete', path='delete', detail=False)
  558. class SubscriptionBulkDeleteView(generic.BulkDeleteView):
  559. table = tables.SubscriptionTable
  560. def get_queryset(self, request):
  561. return Subscription.objects.filter(user=request.user)
  562. #
  563. # Webhooks
  564. #
  565. @register_model_view(Webhook, 'list', path='', detail=False)
  566. class WebhookListView(generic.ObjectListView):
  567. queryset = Webhook.objects.all()
  568. filterset = filtersets.WebhookFilterSet
  569. filterset_form = forms.WebhookFilterForm
  570. table = tables.WebhookTable
  571. @register_model_view(Webhook)
  572. class WebhookView(generic.ObjectView):
  573. queryset = Webhook.objects.all()
  574. template_name = 'generic/object.html'
  575. layout = layout.SimpleLayout(
  576. left_panels=[
  577. panels.WebhookPanel(),
  578. panels.WebhookHTTPPanel(),
  579. panels.WebhookSSLPanel(),
  580. ],
  581. right_panels=[
  582. TextCodePanel('additional_headers', title=_('Additional Headers')),
  583. TextCodePanel('body_template', title=_('Body Template')),
  584. panels.CustomFieldsPanel(),
  585. panels.TagsPanel(),
  586. ],
  587. )
  588. @register_model_view(Webhook, 'add', detail=False)
  589. @register_model_view(Webhook, 'edit')
  590. class WebhookEditView(generic.ObjectEditView):
  591. queryset = Webhook.objects.all()
  592. form = forms.WebhookForm
  593. @register_model_view(Webhook, 'delete')
  594. class WebhookDeleteView(generic.ObjectDeleteView):
  595. queryset = Webhook.objects.all()
  596. @register_model_view(Webhook, 'bulk_import', path='import', detail=False)
  597. class WebhookBulkImportView(generic.BulkImportView):
  598. queryset = Webhook.objects.all()
  599. model_form = forms.WebhookImportForm
  600. @register_model_view(Webhook, 'bulk_edit', path='edit', detail=False)
  601. class WebhookBulkEditView(generic.BulkEditView):
  602. queryset = Webhook.objects.all()
  603. filterset = filtersets.WebhookFilterSet
  604. table = tables.WebhookTable
  605. form = forms.WebhookBulkEditForm
  606. @register_model_view(Webhook, 'bulk_rename', path='rename', detail=False)
  607. class WebhookBulkRenameView(generic.BulkRenameView):
  608. queryset = Webhook.objects.all()
  609. filterset = filtersets.WebhookFilterSet
  610. @register_model_view(Webhook, 'bulk_delete', path='delete', detail=False)
  611. class WebhookBulkDeleteView(generic.BulkDeleteView):
  612. queryset = Webhook.objects.all()
  613. filterset = filtersets.WebhookFilterSet
  614. table = tables.WebhookTable
  615. #
  616. # Event Rules
  617. #
  618. @register_model_view(EventRule, 'list', path='', detail=False)
  619. class EventRuleListView(generic.ObjectListView):
  620. queryset = EventRule.objects.all()
  621. filterset = filtersets.EventRuleFilterSet
  622. filterset_form = forms.EventRuleFilterForm
  623. table = tables.EventRuleTable
  624. @register_model_view(EventRule)
  625. class EventRuleView(generic.ObjectView):
  626. queryset = EventRule.objects.all()
  627. template_name = 'generic/object.html'
  628. layout = layout.SimpleLayout(
  629. left_panels=[
  630. panels.EventRulePanel(),
  631. panels.ObjectTypesPanel(),
  632. panels.EventRuleEventTypesPanel(),
  633. ],
  634. right_panels=[
  635. JSONPanel('conditions', title=_('Conditions')),
  636. panels.EventRuleActionPanel(),
  637. panels.CustomFieldsPanel(),
  638. panels.TagsPanel(),
  639. ],
  640. )
  641. @register_model_view(EventRule, 'add', detail=False)
  642. @register_model_view(EventRule, 'edit')
  643. class EventRuleEditView(generic.ObjectEditView):
  644. queryset = EventRule.objects.all()
  645. form = forms.EventRuleForm
  646. @register_model_view(EventRule, 'delete')
  647. class EventRuleDeleteView(generic.ObjectDeleteView):
  648. queryset = EventRule.objects.all()
  649. @register_model_view(EventRule, 'bulk_import', path='import', detail=False)
  650. class EventRuleBulkImportView(generic.BulkImportView):
  651. queryset = EventRule.objects.all()
  652. model_form = forms.EventRuleImportForm
  653. @register_model_view(EventRule, 'bulk_edit', path='edit', detail=False)
  654. class EventRuleBulkEditView(generic.BulkEditView):
  655. queryset = EventRule.objects.all()
  656. filterset = filtersets.EventRuleFilterSet
  657. table = tables.EventRuleTable
  658. form = forms.EventRuleBulkEditForm
  659. @register_model_view(EventRule, 'bulk_rename', path='rename', detail=False)
  660. class EventRuleBulkRenameView(generic.BulkRenameView):
  661. queryset = EventRule.objects.all()
  662. filterset = filtersets.EventRuleFilterSet
  663. @register_model_view(EventRule, 'bulk_delete', path='delete', detail=False)
  664. class EventRuleBulkDeleteView(generic.BulkDeleteView):
  665. queryset = EventRule.objects.all()
  666. filterset = filtersets.EventRuleFilterSet
  667. table = tables.EventRuleTable
  668. #
  669. # Tags
  670. #
  671. @register_model_view(Tag, 'list', path='', detail=False)
  672. class TagListView(generic.ObjectListView):
  673. queryset = Tag.objects.annotate(
  674. items=count_related(TaggedItem, 'tag')
  675. )
  676. filterset = filtersets.TagFilterSet
  677. filterset_form = forms.TagFilterForm
  678. table = tables.TagTable
  679. @register_model_view(Tag)
  680. class TagView(generic.ObjectView):
  681. queryset = Tag.objects.all()
  682. template_name = 'generic/object.html'
  683. layout = layout.SimpleLayout(
  684. left_panels=[
  685. panels.TagPanel(),
  686. ],
  687. right_panels=[
  688. panels.TagObjectTypesPanel(),
  689. panels.TagItemTypesPanel(),
  690. ],
  691. bottom_panels=[
  692. ContextTablePanel('taggeditem_table', title=_('Tagged Objects')),
  693. ],
  694. )
  695. def get_extra_context(self, request, instance):
  696. tagged_items = TaggedItem.objects.filter(tag=instance)
  697. taggeditem_table = tables.TaggedItemTable(
  698. data=tagged_items,
  699. orderable=False
  700. )
  701. taggeditem_table.configure(request)
  702. object_types = [
  703. {
  704. 'content_type': ContentType.objects.get(pk=ti['content_type']),
  705. 'item_count': ti['item_count']
  706. } for ti in tagged_items.values('content_type').annotate(item_count=Count('pk'))
  707. ]
  708. return {
  709. 'taggeditem_table': taggeditem_table,
  710. 'tagged_item_count': tagged_items.count(),
  711. 'object_types': object_types,
  712. }
  713. @register_model_view(Tag, 'add', detail=False)
  714. @register_model_view(Tag, 'edit')
  715. class TagEditView(generic.ObjectEditView):
  716. queryset = Tag.objects.all()
  717. form = forms.TagForm
  718. @register_model_view(Tag, 'delete')
  719. class TagDeleteView(generic.ObjectDeleteView):
  720. queryset = Tag.objects.all()
  721. @register_model_view(Tag, 'bulk_import', path='import', detail=False)
  722. class TagBulkImportView(generic.BulkImportView):
  723. queryset = Tag.objects.all()
  724. model_form = forms.TagImportForm
  725. @register_model_view(Tag, 'bulk_edit', path='edit', detail=False)
  726. class TagBulkEditView(generic.BulkEditView):
  727. queryset = Tag.objects.annotate(
  728. items=count_related(TaggedItem, 'tag')
  729. )
  730. table = tables.TagTable
  731. form = forms.TagBulkEditForm
  732. @register_model_view(Tag, 'bulk_rename', path='rename', detail=False)
  733. class TagBulkRenameView(generic.BulkRenameView):
  734. queryset = Tag.objects.all()
  735. @register_model_view(Tag, 'bulk_delete', path='delete', detail=False)
  736. class TagBulkDeleteView(generic.BulkDeleteView):
  737. queryset = Tag.objects.annotate(
  738. items=count_related(TaggedItem, 'tag')
  739. )
  740. table = tables.TagTable
  741. #
  742. # Config context profiles
  743. #
  744. @register_model_view(ConfigContextProfile, 'list', path='', detail=False)
  745. class ConfigContextProfileListView(generic.ObjectListView):
  746. queryset = ConfigContextProfile.objects.all()
  747. filterset = filtersets.ConfigContextProfileFilterSet
  748. filterset_form = forms.ConfigContextProfileFilterForm
  749. table = tables.ConfigContextProfileTable
  750. actions = (AddObject, BulkSync, BulkEdit, BulkRename, BulkDelete)
  751. @register_model_view(ConfigContextProfile)
  752. class ConfigContextProfileView(generic.ObjectView):
  753. queryset = ConfigContextProfile.objects.all()
  754. template_name = 'generic/object.html'
  755. layout = layout.SimpleLayout(
  756. left_panels=[
  757. panels.ConfigContextProfilePanel(),
  758. TemplatePanel('core/inc/datafile_panel.html'),
  759. panels.CustomFieldsPanel(),
  760. panels.TagsPanel(),
  761. CommentsPanel(),
  762. ],
  763. right_panels=[
  764. JSONPanel('schema', title=_('JSON Schema')),
  765. ],
  766. )
  767. @register_model_view(ConfigContextProfile, 'add', detail=False)
  768. @register_model_view(ConfigContextProfile, 'edit')
  769. class ConfigContextProfileEditView(generic.ObjectEditView):
  770. queryset = ConfigContextProfile.objects.all()
  771. form = forms.ConfigContextProfileForm
  772. @register_model_view(ConfigContextProfile, 'delete')
  773. class ConfigContextProfileDeleteView(generic.ObjectDeleteView):
  774. queryset = ConfigContextProfile.objects.all()
  775. @register_model_view(ConfigContextProfile, 'bulk_import', path='import', detail=False)
  776. class ConfigContextProfileBulkImportView(generic.BulkImportView):
  777. queryset = ConfigContextProfile.objects.all()
  778. model_form = forms.ConfigContextProfileImportForm
  779. @register_model_view(ConfigContextProfile, 'bulk_edit', path='edit', detail=False)
  780. class ConfigContextProfileBulkEditView(generic.BulkEditView):
  781. queryset = ConfigContextProfile.objects.all()
  782. filterset = filtersets.ConfigContextProfileFilterSet
  783. table = tables.ConfigContextProfileTable
  784. form = forms.ConfigContextProfileBulkEditForm
  785. @register_model_view(ConfigContextProfile, 'bulk_rename', path='rename', detail=False)
  786. class ConfigContextProfileBulkRenameView(generic.BulkRenameView):
  787. queryset = ConfigContextProfile.objects.all()
  788. filterset = filtersets.ConfigContextProfileFilterSet
  789. @register_model_view(ConfigContextProfile, 'bulk_delete', path='delete', detail=False)
  790. class ConfigContextProfileBulkDeleteView(generic.BulkDeleteView):
  791. queryset = ConfigContextProfile.objects.all()
  792. filterset = filtersets.ConfigContextProfileFilterSet
  793. table = tables.ConfigContextProfileTable
  794. @register_model_view(ConfigContextProfile, 'bulk_sync', path='sync', detail=False)
  795. class ConfigContextProfileBulkSyncDataView(generic.BulkSyncDataView):
  796. queryset = ConfigContextProfile.objects.all()
  797. #
  798. # Config contexts
  799. #
  800. @register_model_view(ConfigContext, 'list', path='', detail=False)
  801. class ConfigContextListView(generic.ObjectListView):
  802. queryset = ConfigContext.objects.all()
  803. filterset = filtersets.ConfigContextFilterSet
  804. filterset_form = forms.ConfigContextFilterForm
  805. table = tables.ConfigContextTable
  806. actions = (AddObject, BulkSync, BulkEdit, BulkRename, BulkDelete)
  807. @register_model_view(ConfigContext)
  808. class ConfigContextView(generic.ObjectView):
  809. queryset = ConfigContext.objects.all()
  810. template_name = 'generic/object.html'
  811. layout = layout.SimpleLayout(
  812. left_panels=[
  813. panels.ConfigContextPanel(),
  814. TemplatePanel('core/inc/datafile_panel.html'),
  815. panels.ConfigContextAssignmentPanel(),
  816. ],
  817. right_panels=[
  818. TemplatePanel('extras/panels/configcontext_data.html'),
  819. ],
  820. )
  821. def get_extra_context(self, request, instance):
  822. # Gather assigned objects for parsing in the template
  823. assigned_objects = (
  824. ('Regions', instance.regions.all),
  825. ('Site Groups', instance.site_groups.all),
  826. ('Sites', instance.sites.all),
  827. ('Locations', instance.locations.all),
  828. ('Device Types', instance.device_types.all),
  829. ('Roles', instance.roles.all),
  830. ('Platforms', instance.platforms.all),
  831. ('Cluster Types', instance.cluster_types.all),
  832. ('Cluster Groups', instance.cluster_groups.all),
  833. ('Clusters', instance.clusters.all),
  834. ('Tenant Groups', instance.tenant_groups.all),
  835. ('Tenants', instance.tenants.all),
  836. ('Tags', instance.tags.all),
  837. )
  838. # Determine user's preferred output format
  839. if request.GET.get('format') in ['json', 'yaml']:
  840. format = request.GET.get('format')
  841. if request.user.is_authenticated:
  842. request.user.config.set('data_format', format, commit=True)
  843. elif request.user.is_authenticated:
  844. format = request.user.config.get('data_format', 'json')
  845. else:
  846. format = 'json'
  847. return {
  848. 'assigned_objects': assigned_objects,
  849. 'format': format,
  850. }
  851. @register_model_view(ConfigContext, 'add', detail=False)
  852. @register_model_view(ConfigContext, 'edit')
  853. class ConfigContextEditView(generic.ObjectEditView):
  854. queryset = ConfigContext.objects.all()
  855. form = forms.ConfigContextForm
  856. @register_model_view(ConfigContext, 'delete')
  857. class ConfigContextDeleteView(generic.ObjectDeleteView):
  858. queryset = ConfigContext.objects.all()
  859. @register_model_view(ConfigContext, 'bulk_edit', path='edit', detail=False)
  860. class ConfigContextBulkEditView(generic.BulkEditView):
  861. queryset = ConfigContext.objects.all()
  862. filterset = filtersets.ConfigContextFilterSet
  863. table = tables.ConfigContextTable
  864. form = forms.ConfigContextBulkEditForm
  865. @register_model_view(ConfigContext, 'bulk_rename', path='rename', detail=False)
  866. class ConfigContextBulkRenameView(generic.BulkRenameView):
  867. queryset = ConfigContext.objects.all()
  868. filterset = filtersets.ConfigContextFilterSet
  869. @register_model_view(ConfigContext, 'bulk_delete', path='delete', detail=False)
  870. class ConfigContextBulkDeleteView(generic.BulkDeleteView):
  871. queryset = ConfigContext.objects.all()
  872. filterset = filtersets.ConfigContextFilterSet
  873. table = tables.ConfigContextTable
  874. @register_model_view(ConfigContext, 'bulk_sync', path='sync', detail=False)
  875. class ConfigContextBulkSyncDataView(generic.BulkSyncDataView):
  876. queryset = ConfigContext.objects.all()
  877. class ObjectConfigContextView(generic.ObjectView):
  878. base_template = None
  879. template_name = 'extras/object_configcontext.html'
  880. def get_extra_context(self, request, instance):
  881. source_contexts = ConfigContext.objects.restrict(request.user, 'view').get_for_object(instance)
  882. # Determine user's preferred output format
  883. if request.GET.get('format') in ['json', 'yaml']:
  884. format = request.GET.get('format')
  885. if request.user.is_authenticated:
  886. request.user.config.set('data_format', format, commit=True)
  887. elif request.user.is_authenticated:
  888. format = request.user.config.get('data_format', 'json')
  889. else:
  890. format = 'json'
  891. return {
  892. 'rendered_context': instance.get_config_context(),
  893. 'source_contexts': source_contexts,
  894. 'format': format,
  895. 'base_template': self.base_template,
  896. }
  897. #
  898. # Config templates
  899. #
  900. @register_model_view(ConfigTemplate, 'list', path='', detail=False)
  901. class ConfigTemplateListView(generic.ObjectListView):
  902. queryset = ConfigTemplate.objects.annotate(
  903. device_count=count_related(Device, 'config_template'),
  904. vm_count=count_related(VirtualMachine, 'config_template'),
  905. role_count=count_related(DeviceRole, 'config_template'),
  906. platform_count=count_related(Platform, 'config_template'),
  907. )
  908. filterset = filtersets.ConfigTemplateFilterSet
  909. filterset_form = forms.ConfigTemplateFilterForm
  910. table = tables.ConfigTemplateTable
  911. actions = (AddObject, BulkImport, BulkExport, BulkSync, BulkEdit, BulkRename, BulkDelete)
  912. @register_model_view(ConfigTemplate)
  913. class ConfigTemplateView(generic.ObjectView):
  914. queryset = ConfigTemplate.objects.all()
  915. template_name = 'generic/object.html'
  916. layout = layout.SimpleLayout(
  917. left_panels=[
  918. panels.ConfigTemplatePanel(),
  919. panels.TagsPanel(),
  920. ],
  921. right_panels=[
  922. JSONPanel('environment_params', title=_('Environment Parameters')),
  923. ],
  924. bottom_panels=[
  925. TextCodePanel('template_code', title=_('Template'), show_sync_warning=True),
  926. ],
  927. )
  928. @register_model_view(ConfigTemplate, 'add', detail=False)
  929. @register_model_view(ConfigTemplate, 'edit')
  930. class ConfigTemplateEditView(generic.ObjectEditView):
  931. queryset = ConfigTemplate.objects.all()
  932. form = forms.ConfigTemplateForm
  933. @register_model_view(ConfigTemplate, 'delete')
  934. class ConfigTemplateDeleteView(generic.ObjectDeleteView):
  935. queryset = ConfigTemplate.objects.all()
  936. @register_model_view(ConfigTemplate, 'bulk_import', path='import', detail=False)
  937. class ConfigTemplateBulkImportView(generic.BulkImportView):
  938. queryset = ConfigTemplate.objects.all()
  939. model_form = forms.ConfigTemplateImportForm
  940. @register_model_view(ConfigTemplate, 'bulk_edit', path='edit', detail=False)
  941. class ConfigTemplateBulkEditView(generic.BulkEditView):
  942. queryset = ConfigTemplate.objects.all()
  943. filterset = filtersets.ConfigTemplateFilterSet
  944. table = tables.ConfigTemplateTable
  945. form = forms.ConfigTemplateBulkEditForm
  946. @register_model_view(ConfigTemplate, 'bulk_rename', path='rename', detail=False)
  947. class ConfigTemplateBulkRenameView(generic.BulkRenameView):
  948. queryset = ConfigTemplate.objects.all()
  949. filterset = filtersets.ConfigTemplateFilterSet
  950. @register_model_view(ConfigTemplate, 'bulk_delete', path='delete', detail=False)
  951. class ConfigTemplateBulkDeleteView(generic.BulkDeleteView):
  952. queryset = ConfigTemplate.objects.all()
  953. filterset = filtersets.ConfigTemplateFilterSet
  954. table = tables.ConfigTemplateTable
  955. @register_model_view(ConfigTemplate, 'bulk_sync', path='sync', detail=False)
  956. class ConfigTemplateBulkSyncDataView(generic.BulkSyncDataView):
  957. queryset = ConfigTemplate.objects.all()
  958. class ObjectRenderConfigView(generic.ObjectView):
  959. base_template = None
  960. template_name = 'extras/object_render_config.html'
  961. def get(self, request, **kwargs):
  962. instance = self.get_object(**kwargs)
  963. context = self.get_extra_context(request, instance)
  964. # If a direct export has been requested, return the rendered template content as a
  965. # downloadable file.
  966. if request.GET.get('export'):
  967. content = context['rendered_config'] or context['error_message']
  968. response = HttpResponse(content, content_type='text')
  969. filename = f"{instance.name or 'config'}.txt"
  970. response['Content-Disposition'] = f'attachment; filename="{filename}"'
  971. return response
  972. return render(
  973. request,
  974. self.get_template_name(),
  975. {
  976. 'object': instance,
  977. 'tab': self.tab,
  978. **context,
  979. },
  980. )
  981. def get_extra_context_data(self, request, instance):
  982. return {
  983. f'{instance._meta.model_name}': instance,
  984. }
  985. def get_extra_context(self, request, instance):
  986. # Compile context data
  987. context_data = instance.get_config_context()
  988. context_data.update(self.get_extra_context_data(request, instance))
  989. # Check for an optional config_template_id override in the query params
  990. config_template = None
  991. error_message = ''
  992. if config_template_id := request.GET.get('config_template_id'):
  993. try:
  994. config_template = ConfigTemplate.objects.restrict(request.user, 'view').get(pk=config_template_id)
  995. except (ConfigTemplate.DoesNotExist, ValueError):
  996. error_message = _("Config template with ID {id} not found.").format(id=config_template_id)
  997. else:
  998. config_template = instance.get_config_template()
  999. # Render the config template
  1000. rendered_config = None
  1001. if config_template:
  1002. try:
  1003. rendered_config = config_template.render(context=context_data)
  1004. except Exception as e:
  1005. error_message = config_template.format_render_error(e)
  1006. return {
  1007. 'base_template': self.base_template,
  1008. 'config_template': config_template,
  1009. 'context_data': context_data,
  1010. 'rendered_config': rendered_config,
  1011. 'error_message': error_message,
  1012. }
  1013. #
  1014. # Image attachments
  1015. #
  1016. @register_model_view(ImageAttachment, 'list', path='', detail=False)
  1017. class ImageAttachmentListView(generic.ObjectListView):
  1018. queryset = ImageAttachment.objects.all()
  1019. filterset = filtersets.ImageAttachmentFilterSet
  1020. filterset_form = forms.ImageAttachmentFilterForm
  1021. table = tables.ImageAttachmentTable
  1022. actions = (BulkExport, BulkEdit, BulkRename, BulkDelete)
  1023. @register_model_view(ImageAttachment)
  1024. class ImageAttachmentView(generic.ObjectView):
  1025. queryset = ImageAttachment.objects.all()
  1026. template_name = 'generic/object.html'
  1027. layout = layout.SimpleLayout(
  1028. left_panels=[
  1029. panels.ImageAttachmentPanel(),
  1030. ],
  1031. right_panels=[
  1032. panels.ImageAttachmentFilePanel(),
  1033. ],
  1034. bottom_panels=[
  1035. panels.ImageAttachmentImagePanel(),
  1036. ],
  1037. )
  1038. @register_model_view(ImageAttachment, 'add', detail=False)
  1039. @register_model_view(ImageAttachment, 'edit')
  1040. class ImageAttachmentEditView(generic.ObjectEditView):
  1041. queryset = ImageAttachment.objects.all()
  1042. form = forms.ImageAttachmentForm
  1043. def alter_object(self, instance, request, args, kwargs):
  1044. if not instance.pk:
  1045. # Assign the parent object based on URL kwargs
  1046. object_type = get_object_or_404(ContentType, pk=request.GET.get('object_type'))
  1047. instance.parent = get_object_or_404(object_type.model_class(), pk=request.GET.get('object_id'))
  1048. return instance
  1049. def get_extra_addanother_params(self, request):
  1050. return {
  1051. 'object_type': request.GET.get('object_type'),
  1052. 'object_id': request.GET.get('object_id'),
  1053. }
  1054. @register_model_view(ImageAttachment, 'delete')
  1055. class ImageAttachmentDeleteView(generic.ObjectDeleteView):
  1056. queryset = ImageAttachment.objects.all()
  1057. @register_model_view(ImageAttachment, 'bulk_edit', path='edit', detail=False)
  1058. class ImageAttachmentBulkEditView(generic.BulkEditView):
  1059. queryset = ImageAttachment.objects.all()
  1060. filterset = filtersets.ImageAttachmentFilterSet
  1061. table = tables.ImageAttachmentTable
  1062. form = forms.ImageAttachmentBulkEditForm
  1063. @register_model_view(ImageAttachment, 'bulk_rename', path='rename', detail=False)
  1064. class ImageAttachmentBulkRenameView(generic.BulkRenameView):
  1065. queryset = ImageAttachment.objects.all()
  1066. filterset = filtersets.ImageAttachmentFilterSet
  1067. @register_model_view(ImageAttachment, 'bulk_delete', path='delete', detail=False)
  1068. class ImageAttachmentBulkDeleteView(generic.BulkDeleteView):
  1069. queryset = ImageAttachment.objects.all()
  1070. filterset = filtersets.ImageAttachmentFilterSet
  1071. table = tables.ImageAttachmentTable
  1072. #
  1073. # Journal entries
  1074. #
  1075. @register_model_view(JournalEntry, 'list', path='', detail=False)
  1076. class JournalEntryListView(generic.ObjectListView):
  1077. queryset = JournalEntry.objects.all()
  1078. filterset = filtersets.JournalEntryFilterSet
  1079. filterset_form = forms.JournalEntryFilterForm
  1080. table = tables.JournalEntryTable
  1081. actions = (BulkImport, BulkEdit, BulkDelete)
  1082. @register_model_view(JournalEntry)
  1083. class JournalEntryView(generic.ObjectView):
  1084. queryset = JournalEntry.objects.all()
  1085. layout = layout.SimpleLayout(
  1086. left_panels=[
  1087. panels.JournalEntryPanel(),
  1088. panels.CustomFieldsPanel(),
  1089. panels.TagsPanel(),
  1090. ],
  1091. right_panels=[
  1092. CommentsPanel(),
  1093. ],
  1094. )
  1095. @register_model_view(JournalEntry, 'add', detail=False)
  1096. @register_model_view(JournalEntry, 'edit')
  1097. class JournalEntryEditView(generic.ObjectEditView):
  1098. queryset = JournalEntry.objects.all()
  1099. form = forms.JournalEntryForm
  1100. def alter_object(self, obj, request, args, kwargs):
  1101. if not obj.pk:
  1102. obj.created_by = request.user
  1103. return obj
  1104. def get_return_url(self, request, instance):
  1105. if not instance.assigned_object:
  1106. return reverse('extras:journalentry_list')
  1107. obj = instance.assigned_object
  1108. return get_action_url(obj, action='journal', kwargs={'pk': obj.pk})
  1109. @register_model_view(JournalEntry, 'delete')
  1110. class JournalEntryDeleteView(generic.ObjectDeleteView):
  1111. queryset = JournalEntry.objects.all()
  1112. def get_return_url(self, request, instance):
  1113. obj = instance.assigned_object
  1114. return get_action_url(obj, action='journal', kwargs={'pk': obj.pk})
  1115. @register_model_view(JournalEntry, 'bulk_import', path='import', detail=False)
  1116. class JournalEntryBulkImportView(generic.BulkImportView):
  1117. queryset = JournalEntry.objects.all()
  1118. model_form = forms.JournalEntryImportForm
  1119. @register_model_view(JournalEntry, 'bulk_edit', path='edit', detail=False)
  1120. class JournalEntryBulkEditView(generic.BulkEditView):
  1121. queryset = JournalEntry.objects.all()
  1122. filterset = filtersets.JournalEntryFilterSet
  1123. table = tables.JournalEntryTable
  1124. form = forms.JournalEntryBulkEditForm
  1125. @register_model_view(JournalEntry, 'bulk_delete', path='delete', detail=False)
  1126. class JournalEntryBulkDeleteView(generic.BulkDeleteView):
  1127. queryset = JournalEntry.objects.all()
  1128. filterset = filtersets.JournalEntryFilterSet
  1129. table = tables.JournalEntryTable
  1130. #
  1131. # Dashboard & widgets
  1132. #
  1133. class DashboardResetView(LoginRequiredMixin, View):
  1134. template_name = 'extras/dashboard/reset.html'
  1135. def get(self, request):
  1136. get_object_or_404(Dashboard.objects.all(), user=request.user)
  1137. form = ConfirmationForm()
  1138. return render(request, self.template_name, {
  1139. 'form': form,
  1140. 'return_url': reverse('home'),
  1141. })
  1142. def post(self, request):
  1143. dashboard = get_object_or_404(Dashboard.objects.all(), user=request.user)
  1144. form = ConfirmationForm(request.POST)
  1145. if form.is_valid():
  1146. dashboard.delete()
  1147. messages.success(request, _("Your dashboard has been reset."))
  1148. return redirect(reverse('home'))
  1149. return render(request, self.template_name, {
  1150. 'form': form,
  1151. 'return_url': reverse('home'),
  1152. })
  1153. class DashboardWidgetAddView(LoginRequiredMixin, View):
  1154. template_name = 'extras/dashboard/widget_add.html'
  1155. def get(self, request):
  1156. if not request.htmx:
  1157. return redirect('home')
  1158. initial = {
  1159. 'widget_class': request.GET.get('widget_class') or 'extras.NoteWidget',
  1160. }
  1161. widget_form = DashboardWidgetAddForm(initial=initial)
  1162. widget_name = get_field_value(widget_form, 'widget_class')
  1163. widget_class = get_widget_class(widget_name)
  1164. config_form = widget_class.ConfigForm(initial=widget_class.default_config, prefix='config')
  1165. return render(request, self.template_name, {
  1166. 'widget_class': widget_class,
  1167. 'widget_form': widget_form,
  1168. 'config_form': config_form,
  1169. })
  1170. def post(self, request):
  1171. widget_form = DashboardWidgetAddForm(request.POST)
  1172. config_form = None
  1173. widget_class = None
  1174. if widget_form.is_valid():
  1175. widget_class = get_widget_class(widget_form.cleaned_data['widget_class'])
  1176. config_form = widget_class.ConfigForm(request.POST, prefix='config')
  1177. if config_form.is_valid():
  1178. data = widget_form.cleaned_data
  1179. data.pop('widget_class')
  1180. data['config'] = config_form.cleaned_data
  1181. widget = widget_class(**data)
  1182. request.user.dashboard.add_widget(widget)
  1183. request.user.dashboard.save()
  1184. messages.success(request, _('Added widget: ') + str(widget.id))
  1185. return HttpResponse(headers={
  1186. 'HX-Redirect': reverse('home'),
  1187. })
  1188. return render(request, self.template_name, {
  1189. 'widget_class': widget_class,
  1190. 'widget_form': widget_form,
  1191. 'config_form': config_form,
  1192. })
  1193. class DashboardWidgetConfigView(LoginRequiredMixin, View):
  1194. template_name = 'extras/dashboard/widget_config.html'
  1195. def get(self, request, id):
  1196. if not request.htmx:
  1197. return redirect('home')
  1198. widget = request.user.dashboard.get_widget(id)
  1199. widget_form = DashboardWidgetForm(initial=widget.form_data)
  1200. config_form = widget.ConfigForm(initial=widget.form_data.get('config'), prefix='config')
  1201. return render(request, self.template_name, {
  1202. 'widget_class': widget.__class__,
  1203. 'widget_form': widget_form,
  1204. 'config_form': config_form,
  1205. 'form_url': reverse('extras:dashboardwidget_config', kwargs={'id': id})
  1206. })
  1207. def post(self, request, id):
  1208. widget = request.user.dashboard.get_widget(id)
  1209. widget_form = DashboardWidgetForm(request.POST)
  1210. config_form = widget.ConfigForm(request.POST, prefix='config')
  1211. if widget_form.is_valid() and config_form.is_valid():
  1212. data = widget_form.cleaned_data
  1213. data['config'] = config_form.cleaned_data
  1214. request.user.dashboard.config[str(id)].update(data)
  1215. request.user.dashboard.save()
  1216. messages.success(request, _('Updated widget: ') + str(widget.id))
  1217. return HttpResponse(headers={
  1218. 'HX-Redirect': reverse('home'),
  1219. })
  1220. return render(request, self.template_name, {
  1221. 'widget_form': widget_form,
  1222. 'config_form': config_form,
  1223. 'form_url': reverse('extras:dashboardwidget_config', kwargs={'id': id})
  1224. })
  1225. class DashboardWidgetDeleteView(LoginRequiredMixin, View):
  1226. template_name = 'generic/object_delete.html'
  1227. def get(self, request, id):
  1228. if not request.htmx:
  1229. return redirect('home')
  1230. widget = request.user.dashboard.get_widget(id)
  1231. form = ConfirmationForm(initial=request.GET)
  1232. return render(request, 'htmx/delete_form.html', {
  1233. 'object_type': widget.__class__.__name__,
  1234. 'object': widget,
  1235. 'form': form,
  1236. 'form_url': reverse('extras:dashboardwidget_delete', kwargs={'id': id})
  1237. })
  1238. def post(self, request, id):
  1239. form = ConfirmationForm(request.POST)
  1240. if form.is_valid():
  1241. request.user.dashboard.delete_widget(id)
  1242. request.user.dashboard.save()
  1243. messages.success(request, _('Deleted widget: ') + str(id))
  1244. else:
  1245. messages.error(request, _('Error deleting widget: ') + str(form.errors[0]))
  1246. return redirect(reverse('home'))
  1247. #
  1248. # Scripts
  1249. #
  1250. @register_model_view(ScriptModule, 'edit')
  1251. class ScriptModuleCreateView(generic.ObjectEditView):
  1252. queryset = ScriptModule.objects.all()
  1253. form = forms.ScriptFileForm
  1254. def alter_object(self, obj, *args, **kwargs):
  1255. obj.file_root = ManagedFileRootPathChoices.SCRIPTS
  1256. return obj
  1257. @register_model_view(ScriptModule, 'delete')
  1258. class ScriptModuleDeleteView(generic.ObjectDeleteView):
  1259. queryset = ScriptModule.objects.all()
  1260. default_return_url = 'extras:script_list'
  1261. class ScriptListView(ContentTypePermissionRequiredMixin, View):
  1262. def get_required_permission(self):
  1263. return 'extras.view_script'
  1264. def get(self, request):
  1265. available_scripts = Script.objects.restrict(request.user)
  1266. module_ids = {s.module_id for s in available_scripts}
  1267. script_modules = ScriptModule.objects.restrict(request.user).filter(pk__in=module_ids).prefetch_related(
  1268. 'data_source', 'data_file',
  1269. )
  1270. context = {
  1271. 'model': ScriptModule,
  1272. 'script_modules': script_modules,
  1273. 'available_scripts': available_scripts,
  1274. }
  1275. # Use partial template for dashboard widgets
  1276. template_name = 'extras/script_list.html'
  1277. if request.GET.get('embedded'):
  1278. template_name = 'extras/inc/script_list_content.html'
  1279. context['embedded'] = True
  1280. return render(request, template_name, context)
  1281. class BaseScriptView(generic.ObjectView):
  1282. queryset = Script.objects.all()
  1283. def get_object(self, **kwargs):
  1284. if pk := kwargs.get('pk', False):
  1285. return get_object_or_404(self.queryset, pk=pk)
  1286. if (module := kwargs.get('module')) and (name := kwargs.get('name', False)):
  1287. return get_object_or_404(self.queryset, module__file_path=f'{module}.py', name=name)
  1288. raise Http404
  1289. def _get_script_class(self, script):
  1290. """
  1291. Return an instance of the Script's Python class
  1292. """
  1293. if script_class := script.python_class:
  1294. return script_class()
  1295. return None
  1296. class ScriptView(BaseScriptView):
  1297. def get(self, request, **kwargs):
  1298. script = self.get_object(**kwargs)
  1299. script_class = self._get_script_class(script)
  1300. if not script_class:
  1301. return render(request, 'extras/script.html', {
  1302. 'object': script,
  1303. 'script': script,
  1304. })
  1305. form = script_class.as_form(initial=normalize_querydict(request.GET))
  1306. return render(request, 'extras/script.html', {
  1307. 'object': script,
  1308. 'script': script,
  1309. 'script_class': script_class,
  1310. 'form': form,
  1311. 'job_count': script.jobs.count(),
  1312. })
  1313. def post(self, request, **kwargs):
  1314. script = self.get_object(**kwargs)
  1315. if not request.user.has_perm('extras.run_script', obj=script):
  1316. return HttpResponseForbidden()
  1317. script_class = self._get_script_class(script)
  1318. if not script_class:
  1319. return render(request, 'extras/script.html', {
  1320. 'object': script,
  1321. 'script': script,
  1322. })
  1323. # Populate missing variables with their default values, if defined
  1324. post_data = request.POST.copy()
  1325. for name, var in script_class._get_vars().items():
  1326. if name not in post_data and (initial := var.field_attrs.get('initial')) is not None:
  1327. post_data[name] = initial
  1328. form = script_class.as_form(post_data, request.FILES)
  1329. # Allow execution only if RQ worker process is running
  1330. if not any_workers_for_queue('default'):
  1331. messages.error(request, _("Unable to run script: RQ worker process not running."))
  1332. elif form.is_valid():
  1333. ScriptJob = import_string("extras.jobs.ScriptJob")
  1334. job = ScriptJob.enqueue(
  1335. instance=script,
  1336. user=request.user,
  1337. schedule_at=form.cleaned_data.pop('_schedule_at'),
  1338. interval=form.cleaned_data.pop('_interval'),
  1339. notifications=form.cleaned_data.pop('_notifications'),
  1340. data=form.cleaned_data,
  1341. request=copy_safe_request(request),
  1342. job_timeout=script.python_class.job_timeout,
  1343. commit=form.cleaned_data.pop('_commit'),
  1344. )
  1345. return redirect('extras:script_result', job_pk=job.pk)
  1346. else:
  1347. fieldset_fields = {field for _, fields in script_class.get_fieldsets() for field in fields}
  1348. hidden_errors = {
  1349. field: errors for field, errors in form.errors.items()
  1350. if field not in fieldset_fields
  1351. }
  1352. if hidden_errors:
  1353. error_msg = '; '.join(f"{field}: {', '.join(errors)}" for field, errors in hidden_errors.items())
  1354. messages.error(request, error_msg)
  1355. return render(request, 'extras/script.html', {
  1356. 'object': script,
  1357. 'script': script,
  1358. 'script_class': script.python_class(),
  1359. 'form': form,
  1360. 'job_count': script.jobs.count(),
  1361. })
  1362. class ScriptSourceView(BaseScriptView):
  1363. queryset = Script.objects.all()
  1364. def get(self, request, **kwargs):
  1365. script = self.get_object(**kwargs)
  1366. script_class = self._get_script_class(script)
  1367. return render(request, 'extras/script/source.html', {
  1368. 'script': script,
  1369. 'script_class': script_class,
  1370. 'job_count': script.jobs.count(),
  1371. 'tab': 'source',
  1372. })
  1373. class ScriptJobsView(BaseScriptView):
  1374. queryset = Script.objects.all()
  1375. def get(self, request, **kwargs):
  1376. script = self.get_object(**kwargs)
  1377. jobs_table = ScriptJobTable(data=script.jobs.all(), orderable=False)
  1378. jobs_table.configure(request)
  1379. return render(request, 'extras/script/jobs.html', {
  1380. 'script': script,
  1381. 'table': jobs_table,
  1382. 'job_count': script.jobs.count(),
  1383. 'tab': 'jobs',
  1384. })
  1385. class ScriptResultView(TableMixin, generic.ObjectView):
  1386. queryset = Job.objects.all()
  1387. def get_required_permission(self):
  1388. return 'extras.view_script'
  1389. def get_queryset(self, request):
  1390. return Job.objects.restrict(request.user, 'view')
  1391. def get_table(self, job, request, bulk_actions=True):
  1392. data = []
  1393. tests = None
  1394. table = None
  1395. index = 0
  1396. try:
  1397. log_threshold = LOG_LEVEL_RANK[request.GET.get('log_threshold', LogLevelChoices.LOG_INFO)]
  1398. except KeyError:
  1399. log_threshold = LOG_LEVEL_RANK[LogLevelChoices.LOG_INFO]
  1400. if job.data:
  1401. if 'log' in job.data:
  1402. if 'tests' in job.data:
  1403. tests = job.data['tests']
  1404. for log in job.data['log']:
  1405. log_level = LOG_LEVEL_RANK.get(log.get('status'), LogLevelChoices.LOG_INFO)
  1406. if log_level >= log_threshold:
  1407. index += 1
  1408. result = {
  1409. 'index': index,
  1410. 'time': datetime.fromisoformat(log.get('time')),
  1411. 'status': log.get('status'),
  1412. 'message': log.get('message'),
  1413. 'object': log.get('obj'),
  1414. 'url': log.get('url'),
  1415. }
  1416. data.append(result)
  1417. table = ScriptResultsTable(data)
  1418. table.configure(request)
  1419. else:
  1420. # for legacy reports
  1421. tests = job.data
  1422. if tests:
  1423. for method, test_data in tests.items():
  1424. if 'log' in test_data:
  1425. for time, status, obj, url, message in test_data['log']:
  1426. log_level = LOG_LEVEL_RANK.get(status, LogLevelChoices.LOG_INFO)
  1427. if log_level >= log_threshold:
  1428. index += 1
  1429. result = {
  1430. 'index': index,
  1431. 'method': method,
  1432. 'time': time,
  1433. 'status': status,
  1434. 'object': obj,
  1435. 'url': url,
  1436. 'message': message,
  1437. }
  1438. data.append(result)
  1439. table = ReportResultsTable(data)
  1440. table.configure(request)
  1441. return table
  1442. def get(self, request, **kwargs):
  1443. table = None
  1444. job = get_object_or_404(self.queryset, pk=kwargs.get('job_pk'))
  1445. # If a direct export output has been requested, return the job data content as a
  1446. # downloadable file.
  1447. if job.completed and request.GET.get('export') == 'output':
  1448. content = (job.data.get("output") or "").encode()
  1449. response = HttpResponse(content, content_type='text')
  1450. filename = f"{job.object.name or 'script-output'}_{job.completed.strftime('%Y-%m-%d_%H%M%S')}.txt"
  1451. response['Content-Disposition'] = f'attachment; filename="{filename}"'
  1452. return response
  1453. if job.completed:
  1454. table = self.get_table(job, request, bulk_actions=False)
  1455. log_threshold = request.GET.get('log_threshold', LogLevelChoices.LOG_INFO)
  1456. if log_threshold not in LOG_LEVEL_RANK:
  1457. log_threshold = LogLevelChoices.LOG_INFO
  1458. context = {
  1459. 'script': job.object,
  1460. 'job': job,
  1461. 'table': table,
  1462. 'log_levels': dict(LogLevelChoices),
  1463. 'log_threshold': log_threshold,
  1464. }
  1465. if job.data and 'log' in job.data:
  1466. # Script
  1467. context['tests'] = job.data.get('tests', {})
  1468. elif job.data:
  1469. # Legacy Report
  1470. context['tests'] = {
  1471. name: data for name, data in job.data.items()
  1472. if name.startswith('test_')
  1473. }
  1474. # If this is an HTMX request, return only the result HTML
  1475. if htmx_partial(request):
  1476. if request.GET.get('log'):
  1477. # If log=True, render only the log table
  1478. return render(request, 'htmx/table.html', context)
  1479. response = render(request, 'extras/htmx/script_result.html', context)
  1480. if job.completed or not job.started:
  1481. response.status_code = 286
  1482. return response
  1483. return render(request, 'extras/script_result.html', context)
  1484. #
  1485. # Markdown
  1486. #
  1487. class RenderMarkdownView(LoginRequiredMixin, View):
  1488. def post(self, request):
  1489. form = forms.RenderMarkdownForm(request.POST)
  1490. if not form.is_valid():
  1491. HttpResponseBadRequest()
  1492. rendered = render_markdown(form.cleaned_data['text'])
  1493. return HttpResponse(rendered)