views.py 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420
  1. from django.contrib import messages
  2. from django.contrib.auth.mixins import LoginRequiredMixin
  3. from django.contrib.contenttypes.models import ContentType
  4. from django.core.paginator import EmptyPage
  5. from django.db.models import Count, Q
  6. from django.http import HttpResponseBadRequest, HttpResponseForbidden, HttpResponse
  7. from django.shortcuts import get_object_or_404, redirect, render
  8. from django.urls import reverse
  9. from django.utils import timezone
  10. from django.utils.module_loading import import_string
  11. from django.utils.translation import gettext as _
  12. from django.views.generic import View
  13. from core.choices import ManagedFileRootPathChoices
  14. from core.forms import ManagedFileForm
  15. from core.models import Job
  16. from core.tables import JobTable
  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 netbox.constants import DEFAULT_ACTION_PERMISSIONS
  22. from netbox.views import generic
  23. from netbox.views.generic.mixins import TableMixin
  24. from utilities.forms import ConfirmationForm, get_field_value
  25. from utilities.htmx import htmx_partial
  26. from utilities.paginator import EnhancedPaginator, get_paginate_count
  27. from utilities.query import count_related
  28. from utilities.querydict import normalize_querydict
  29. from utilities.request import copy_safe_request
  30. from utilities.rqworker import get_workers_for_queue
  31. from utilities.templatetags.builtins.filters import render_markdown
  32. from utilities.views import ContentTypePermissionRequiredMixin, get_viewname, register_model_view
  33. from virtualization.models import VirtualMachine
  34. from . import filtersets, forms, tables
  35. from .constants import LOG_LEVEL_RANK
  36. from .models import *
  37. from .tables import ReportResultsTable, ScriptResultsTable
  38. #
  39. # Custom fields
  40. #
  41. @register_model_view(CustomField, 'list', path='', detail=False)
  42. class CustomFieldListView(generic.ObjectListView):
  43. queryset = CustomField.objects.select_related('choice_set')
  44. filterset = filtersets.CustomFieldFilterSet
  45. filterset_form = forms.CustomFieldFilterForm
  46. table = tables.CustomFieldTable
  47. @register_model_view(CustomField)
  48. class CustomFieldView(generic.ObjectView):
  49. queryset = CustomField.objects.select_related('choice_set')
  50. def get_extra_context(self, request, instance):
  51. related_models = ()
  52. for object_type in instance.object_types.all():
  53. related_models += (
  54. object_type.model_class().objects.restrict(request.user, 'view').exclude(
  55. Q(**{f'custom_field_data__{instance.name}': ''}) |
  56. Q(**{f'custom_field_data__{instance.name}': None})
  57. ),
  58. )
  59. return {
  60. 'related_models': related_models
  61. }
  62. @register_model_view(CustomField, 'add', detail=False)
  63. @register_model_view(CustomField, 'edit')
  64. class CustomFieldEditView(generic.ObjectEditView):
  65. queryset = CustomField.objects.select_related('choice_set')
  66. form = forms.CustomFieldForm
  67. @register_model_view(CustomField, 'delete')
  68. class CustomFieldDeleteView(generic.ObjectDeleteView):
  69. queryset = CustomField.objects.select_related('choice_set')
  70. @register_model_view(CustomField, 'bulk_import', detail=False)
  71. class CustomFieldBulkImportView(generic.BulkImportView):
  72. queryset = CustomField.objects.select_related('choice_set')
  73. model_form = forms.CustomFieldImportForm
  74. @register_model_view(CustomField, 'bulk_edit', path='edit', detail=False)
  75. class CustomFieldBulkEditView(generic.BulkEditView):
  76. queryset = CustomField.objects.select_related('choice_set')
  77. filterset = filtersets.CustomFieldFilterSet
  78. table = tables.CustomFieldTable
  79. form = forms.CustomFieldBulkEditForm
  80. @register_model_view(CustomField, 'bulk_delete', path='delete', detail=False)
  81. class CustomFieldBulkDeleteView(generic.BulkDeleteView):
  82. queryset = CustomField.objects.select_related('choice_set')
  83. filterset = filtersets.CustomFieldFilterSet
  84. table = tables.CustomFieldTable
  85. #
  86. # Custom field choices
  87. #
  88. @register_model_view(CustomFieldChoiceSet, 'list', path='', detail=False)
  89. class CustomFieldChoiceSetListView(generic.ObjectListView):
  90. queryset = CustomFieldChoiceSet.objects.all()
  91. filterset = filtersets.CustomFieldChoiceSetFilterSet
  92. filterset_form = forms.CustomFieldChoiceSetFilterForm
  93. table = tables.CustomFieldChoiceSetTable
  94. @register_model_view(CustomFieldChoiceSet)
  95. class CustomFieldChoiceSetView(generic.ObjectView):
  96. queryset = CustomFieldChoiceSet.objects.all()
  97. def get_extra_context(self, request, instance):
  98. # Paginate choices list
  99. per_page = get_paginate_count(request)
  100. try:
  101. page_number = request.GET.get('page', 1)
  102. except ValueError:
  103. page_number = 1
  104. paginator = EnhancedPaginator(instance.choices, per_page)
  105. try:
  106. choices = paginator.page(page_number)
  107. except EmptyPage:
  108. choices = paginator.page(paginator.num_pages)
  109. return {
  110. 'paginator': paginator,
  111. 'choices': choices,
  112. }
  113. @register_model_view(CustomFieldChoiceSet, 'add', detail=False)
  114. @register_model_view(CustomFieldChoiceSet, 'edit')
  115. class CustomFieldChoiceSetEditView(generic.ObjectEditView):
  116. queryset = CustomFieldChoiceSet.objects.all()
  117. form = forms.CustomFieldChoiceSetForm
  118. @register_model_view(CustomFieldChoiceSet, 'delete')
  119. class CustomFieldChoiceSetDeleteView(generic.ObjectDeleteView):
  120. queryset = CustomFieldChoiceSet.objects.all()
  121. @register_model_view(CustomFieldChoiceSet, 'bulk_import', detail=False)
  122. class CustomFieldChoiceSetBulkImportView(generic.BulkImportView):
  123. queryset = CustomFieldChoiceSet.objects.all()
  124. model_form = forms.CustomFieldChoiceSetImportForm
  125. @register_model_view(CustomFieldChoiceSet, 'bulk_edit', path='edit', detail=False)
  126. class CustomFieldChoiceSetBulkEditView(generic.BulkEditView):
  127. queryset = CustomFieldChoiceSet.objects.all()
  128. filterset = filtersets.CustomFieldChoiceSetFilterSet
  129. table = tables.CustomFieldChoiceSetTable
  130. form = forms.CustomFieldChoiceSetBulkEditForm
  131. @register_model_view(CustomFieldChoiceSet, 'bulk_delete', path='delete', detail=False)
  132. class CustomFieldChoiceSetBulkDeleteView(generic.BulkDeleteView):
  133. queryset = CustomFieldChoiceSet.objects.all()
  134. filterset = filtersets.CustomFieldChoiceSetFilterSet
  135. table = tables.CustomFieldChoiceSetTable
  136. #
  137. # Custom links
  138. #
  139. @register_model_view(CustomLink, 'list', path='', detail=False)
  140. class CustomLinkListView(generic.ObjectListView):
  141. queryset = CustomLink.objects.all()
  142. filterset = filtersets.CustomLinkFilterSet
  143. filterset_form = forms.CustomLinkFilterForm
  144. table = tables.CustomLinkTable
  145. @register_model_view(CustomLink)
  146. class CustomLinkView(generic.ObjectView):
  147. queryset = CustomLink.objects.all()
  148. @register_model_view(CustomLink, 'add', detail=False)
  149. @register_model_view(CustomLink, 'edit')
  150. class CustomLinkEditView(generic.ObjectEditView):
  151. queryset = CustomLink.objects.all()
  152. form = forms.CustomLinkForm
  153. @register_model_view(CustomLink, 'delete')
  154. class CustomLinkDeleteView(generic.ObjectDeleteView):
  155. queryset = CustomLink.objects.all()
  156. @register_model_view(CustomLink, 'bulk_import', detail=False)
  157. class CustomLinkBulkImportView(generic.BulkImportView):
  158. queryset = CustomLink.objects.all()
  159. model_form = forms.CustomLinkImportForm
  160. @register_model_view(CustomLink, 'bulk_edit', path='edit', detail=False)
  161. class CustomLinkBulkEditView(generic.BulkEditView):
  162. queryset = CustomLink.objects.all()
  163. filterset = filtersets.CustomLinkFilterSet
  164. table = tables.CustomLinkTable
  165. form = forms.CustomLinkBulkEditForm
  166. @register_model_view(CustomLink, 'bulk_delete', path='delete', detail=False)
  167. class CustomLinkBulkDeleteView(generic.BulkDeleteView):
  168. queryset = CustomLink.objects.all()
  169. filterset = filtersets.CustomLinkFilterSet
  170. table = tables.CustomLinkTable
  171. #
  172. # Export templates
  173. #
  174. @register_model_view(ExportTemplate, 'list', path='', detail=False)
  175. class ExportTemplateListView(generic.ObjectListView):
  176. queryset = ExportTemplate.objects.all()
  177. filterset = filtersets.ExportTemplateFilterSet
  178. filterset_form = forms.ExportTemplateFilterForm
  179. table = tables.ExportTemplateTable
  180. template_name = 'extras/exporttemplate_list.html'
  181. actions = {
  182. **DEFAULT_ACTION_PERMISSIONS,
  183. 'bulk_sync': {'sync'},
  184. }
  185. @register_model_view(ExportTemplate)
  186. class ExportTemplateView(generic.ObjectView):
  187. queryset = ExportTemplate.objects.all()
  188. @register_model_view(ExportTemplate, 'add', detail=False)
  189. @register_model_view(ExportTemplate, 'edit')
  190. class ExportTemplateEditView(generic.ObjectEditView):
  191. queryset = ExportTemplate.objects.all()
  192. form = forms.ExportTemplateForm
  193. @register_model_view(ExportTemplate, 'delete')
  194. class ExportTemplateDeleteView(generic.ObjectDeleteView):
  195. queryset = ExportTemplate.objects.all()
  196. @register_model_view(ExportTemplate, 'bulk_import', detail=False)
  197. class ExportTemplateBulkImportView(generic.BulkImportView):
  198. queryset = ExportTemplate.objects.all()
  199. model_form = forms.ExportTemplateImportForm
  200. @register_model_view(ExportTemplate, 'bulk_edit', path='edit', detail=False)
  201. class ExportTemplateBulkEditView(generic.BulkEditView):
  202. queryset = ExportTemplate.objects.all()
  203. filterset = filtersets.ExportTemplateFilterSet
  204. table = tables.ExportTemplateTable
  205. form = forms.ExportTemplateBulkEditForm
  206. @register_model_view(ExportTemplate, 'bulk_delete', path='delete', detail=False)
  207. class ExportTemplateBulkDeleteView(generic.BulkDeleteView):
  208. queryset = ExportTemplate.objects.all()
  209. filterset = filtersets.ExportTemplateFilterSet
  210. table = tables.ExportTemplateTable
  211. @register_model_view(ExportTemplate, 'bulk_sync', path='sync', detail=False)
  212. class ExportTemplateBulkSyncDataView(generic.BulkSyncDataView):
  213. queryset = ExportTemplate.objects.all()
  214. #
  215. # Saved filters
  216. #
  217. class SavedFilterMixin:
  218. def get_queryset(self, request):
  219. """
  220. Return only shared SavedFilters, or those owned by the current user, unless
  221. this is a superuser.
  222. """
  223. queryset = SavedFilter.objects.all()
  224. user = request.user
  225. if user.is_superuser:
  226. return queryset
  227. if user.is_anonymous:
  228. return queryset.filter(shared=True)
  229. return queryset.filter(
  230. Q(shared=True) | Q(user=user)
  231. )
  232. @register_model_view(SavedFilter, 'list', path='', detail=False)
  233. class SavedFilterListView(SavedFilterMixin, generic.ObjectListView):
  234. filterset = filtersets.SavedFilterFilterSet
  235. filterset_form = forms.SavedFilterFilterForm
  236. table = tables.SavedFilterTable
  237. @register_model_view(SavedFilter)
  238. class SavedFilterView(SavedFilterMixin, generic.ObjectView):
  239. queryset = SavedFilter.objects.all()
  240. @register_model_view(SavedFilter, 'add', detail=False)
  241. @register_model_view(SavedFilter, 'edit')
  242. class SavedFilterEditView(SavedFilterMixin, generic.ObjectEditView):
  243. queryset = SavedFilter.objects.all()
  244. form = forms.SavedFilterForm
  245. def alter_object(self, obj, request, url_args, url_kwargs):
  246. if not obj.pk:
  247. obj.user = request.user
  248. return obj
  249. @register_model_view(SavedFilter, 'delete')
  250. class SavedFilterDeleteView(SavedFilterMixin, generic.ObjectDeleteView):
  251. queryset = SavedFilter.objects.all()
  252. @register_model_view(SavedFilter, 'bulk_import', detail=False)
  253. class SavedFilterBulkImportView(SavedFilterMixin, generic.BulkImportView):
  254. queryset = SavedFilter.objects.all()
  255. model_form = forms.SavedFilterImportForm
  256. @register_model_view(SavedFilter, 'bulk_edit', path='edit', detail=False)
  257. class SavedFilterBulkEditView(SavedFilterMixin, generic.BulkEditView):
  258. queryset = SavedFilter.objects.all()
  259. filterset = filtersets.SavedFilterFilterSet
  260. table = tables.SavedFilterTable
  261. form = forms.SavedFilterBulkEditForm
  262. @register_model_view(SavedFilter, 'bulk_delete', path='delete', detail=False)
  263. class SavedFilterBulkDeleteView(SavedFilterMixin, generic.BulkDeleteView):
  264. queryset = SavedFilter.objects.all()
  265. filterset = filtersets.SavedFilterFilterSet
  266. table = tables.SavedFilterTable
  267. #
  268. # Bookmarks
  269. #
  270. @register_model_view(Bookmark, 'add', detail=False)
  271. class BookmarkCreateView(generic.ObjectEditView):
  272. form = forms.BookmarkForm
  273. def get_queryset(self, request):
  274. return Bookmark.objects.filter(user=request.user)
  275. def alter_object(self, obj, request, url_args, url_kwargs):
  276. obj.user = request.user
  277. return obj
  278. @register_model_view(Bookmark, 'delete')
  279. class BookmarkDeleteView(generic.ObjectDeleteView):
  280. def get_queryset(self, request):
  281. return Bookmark.objects.filter(user=request.user)
  282. @register_model_view(Bookmark, 'bulk_delete', path='delete', detail=False)
  283. class BookmarkBulkDeleteView(generic.BulkDeleteView):
  284. table = tables.BookmarkTable
  285. def get_queryset(self, request):
  286. return Bookmark.objects.filter(user=request.user)
  287. #
  288. # Notification groups
  289. #
  290. @register_model_view(NotificationGroup, 'list', path='', detail=False)
  291. class NotificationGroupListView(generic.ObjectListView):
  292. queryset = NotificationGroup.objects.all()
  293. filterset = filtersets.NotificationGroupFilterSet
  294. filterset_form = forms.NotificationGroupFilterForm
  295. table = tables.NotificationGroupTable
  296. @register_model_view(NotificationGroup)
  297. class NotificationGroupView(generic.ObjectView):
  298. queryset = NotificationGroup.objects.all()
  299. @register_model_view(NotificationGroup, 'add', detail=False)
  300. @register_model_view(NotificationGroup, 'edit')
  301. class NotificationGroupEditView(generic.ObjectEditView):
  302. queryset = NotificationGroup.objects.all()
  303. form = forms.NotificationGroupForm
  304. @register_model_view(NotificationGroup, 'delete')
  305. class NotificationGroupDeleteView(generic.ObjectDeleteView):
  306. queryset = NotificationGroup.objects.all()
  307. @register_model_view(NotificationGroup, 'bulk_import', detail=False)
  308. class NotificationGroupBulkImportView(generic.BulkImportView):
  309. queryset = NotificationGroup.objects.all()
  310. model_form = forms.NotificationGroupImportForm
  311. @register_model_view(NotificationGroup, 'bulk_edit', path='edit', detail=False)
  312. class NotificationGroupBulkEditView(generic.BulkEditView):
  313. queryset = NotificationGroup.objects.all()
  314. filterset = filtersets.NotificationGroupFilterSet
  315. table = tables.NotificationGroupTable
  316. form = forms.NotificationGroupBulkEditForm
  317. @register_model_view(NotificationGroup, 'bulk_delete', path='delete', detail=False)
  318. class NotificationGroupBulkDeleteView(generic.BulkDeleteView):
  319. queryset = NotificationGroup.objects.all()
  320. filterset = filtersets.NotificationGroupFilterSet
  321. table = tables.NotificationGroupTable
  322. #
  323. # Notifications
  324. #
  325. class NotificationsView(LoginRequiredMixin, View):
  326. """
  327. HTMX-only user-specific notifications list.
  328. """
  329. def get(self, request):
  330. return render(request, 'htmx/notifications.html', {
  331. 'notifications': request.user.notifications.unread(),
  332. 'total_count': request.user.notifications.count(),
  333. })
  334. @register_model_view(Notification, 'read')
  335. class NotificationReadView(LoginRequiredMixin, View):
  336. """
  337. Mark the Notification read and redirect the user to its attached object.
  338. """
  339. def get(self, request, pk):
  340. # Mark the Notification as read
  341. notification = get_object_or_404(request.user.notifications, pk=pk)
  342. notification.read = timezone.now()
  343. notification.save()
  344. # Redirect to the object if it has a URL (deleted objects will not)
  345. if hasattr(notification.object, 'get_absolute_url'):
  346. return redirect(notification.object.get_absolute_url())
  347. return redirect('account:notifications')
  348. @register_model_view(Notification, 'dismiss')
  349. class NotificationDismissView(LoginRequiredMixin, View):
  350. """
  351. A convenience view which allows deleting notifications with one click.
  352. """
  353. def get(self, request, pk):
  354. notification = get_object_or_404(request.user.notifications, pk=pk)
  355. notification.delete()
  356. if htmx_partial(request):
  357. return render(request, 'htmx/notifications.html', {
  358. 'notifications': request.user.notifications.unread()[:10],
  359. })
  360. return redirect('account:notifications')
  361. @register_model_view(Notification, 'delete')
  362. class NotificationDeleteView(generic.ObjectDeleteView):
  363. def get_queryset(self, request):
  364. return Notification.objects.filter(user=request.user)
  365. @register_model_view(Notification, 'bulk_delete', path='delete', detail=False)
  366. class NotificationBulkDeleteView(generic.BulkDeleteView):
  367. table = tables.NotificationTable
  368. def get_queryset(self, request):
  369. return Notification.objects.filter(user=request.user)
  370. #
  371. # Subscriptions
  372. #
  373. @register_model_view(Subscription, 'add', detail=False)
  374. class SubscriptionCreateView(generic.ObjectEditView):
  375. form = forms.SubscriptionForm
  376. def get_queryset(self, request):
  377. return Subscription.objects.filter(user=request.user)
  378. def alter_object(self, obj, request, url_args, url_kwargs):
  379. obj.user = request.user
  380. return obj
  381. @register_model_view(Subscription, 'delete')
  382. class SubscriptionDeleteView(generic.ObjectDeleteView):
  383. def get_queryset(self, request):
  384. return Subscription.objects.filter(user=request.user)
  385. @register_model_view(Subscription, 'bulk_delete', path='delete', detail=False)
  386. class SubscriptionBulkDeleteView(generic.BulkDeleteView):
  387. table = tables.SubscriptionTable
  388. def get_queryset(self, request):
  389. return Subscription.objects.filter(user=request.user)
  390. #
  391. # Webhooks
  392. #
  393. @register_model_view(Webhook, 'list', path='', detail=False)
  394. class WebhookListView(generic.ObjectListView):
  395. queryset = Webhook.objects.all()
  396. filterset = filtersets.WebhookFilterSet
  397. filterset_form = forms.WebhookFilterForm
  398. table = tables.WebhookTable
  399. @register_model_view(Webhook)
  400. class WebhookView(generic.ObjectView):
  401. queryset = Webhook.objects.all()
  402. @register_model_view(Webhook, 'add', detail=False)
  403. @register_model_view(Webhook, 'edit')
  404. class WebhookEditView(generic.ObjectEditView):
  405. queryset = Webhook.objects.all()
  406. form = forms.WebhookForm
  407. @register_model_view(Webhook, 'delete')
  408. class WebhookDeleteView(generic.ObjectDeleteView):
  409. queryset = Webhook.objects.all()
  410. @register_model_view(Webhook, 'bulk_import', detail=False)
  411. class WebhookBulkImportView(generic.BulkImportView):
  412. queryset = Webhook.objects.all()
  413. model_form = forms.WebhookImportForm
  414. @register_model_view(Webhook, 'bulk_edit', path='edit', detail=False)
  415. class WebhookBulkEditView(generic.BulkEditView):
  416. queryset = Webhook.objects.all()
  417. filterset = filtersets.WebhookFilterSet
  418. table = tables.WebhookTable
  419. form = forms.WebhookBulkEditForm
  420. @register_model_view(Webhook, 'bulk_delete', path='delete', detail=False)
  421. class WebhookBulkDeleteView(generic.BulkDeleteView):
  422. queryset = Webhook.objects.all()
  423. filterset = filtersets.WebhookFilterSet
  424. table = tables.WebhookTable
  425. #
  426. # Event Rules
  427. #
  428. @register_model_view(EventRule, 'list', path='', detail=False)
  429. class EventRuleListView(generic.ObjectListView):
  430. queryset = EventRule.objects.all()
  431. filterset = filtersets.EventRuleFilterSet
  432. filterset_form = forms.EventRuleFilterForm
  433. table = tables.EventRuleTable
  434. @register_model_view(EventRule)
  435. class EventRuleView(generic.ObjectView):
  436. queryset = EventRule.objects.all()
  437. @register_model_view(EventRule, 'add', detail=False)
  438. @register_model_view(EventRule, 'edit')
  439. class EventRuleEditView(generic.ObjectEditView):
  440. queryset = EventRule.objects.all()
  441. form = forms.EventRuleForm
  442. @register_model_view(EventRule, 'delete')
  443. class EventRuleDeleteView(generic.ObjectDeleteView):
  444. queryset = EventRule.objects.all()
  445. @register_model_view(EventRule, 'bulk_import', detail=False)
  446. class EventRuleBulkImportView(generic.BulkImportView):
  447. queryset = EventRule.objects.all()
  448. model_form = forms.EventRuleImportForm
  449. @register_model_view(EventRule, 'bulk_edit', path='edit', detail=False)
  450. class EventRuleBulkEditView(generic.BulkEditView):
  451. queryset = EventRule.objects.all()
  452. filterset = filtersets.EventRuleFilterSet
  453. table = tables.EventRuleTable
  454. form = forms.EventRuleBulkEditForm
  455. @register_model_view(EventRule, 'bulk_delete', path='delete', detail=False)
  456. class EventRuleBulkDeleteView(generic.BulkDeleteView):
  457. queryset = EventRule.objects.all()
  458. filterset = filtersets.EventRuleFilterSet
  459. table = tables.EventRuleTable
  460. #
  461. # Tags
  462. #
  463. @register_model_view(Tag, 'list', path='', detail=False)
  464. class TagListView(generic.ObjectListView):
  465. queryset = Tag.objects.annotate(
  466. items=count_related(TaggedItem, 'tag')
  467. )
  468. filterset = filtersets.TagFilterSet
  469. filterset_form = forms.TagFilterForm
  470. table = tables.TagTable
  471. @register_model_view(Tag)
  472. class TagView(generic.ObjectView):
  473. queryset = Tag.objects.all()
  474. def get_extra_context(self, request, instance):
  475. tagged_items = TaggedItem.objects.filter(tag=instance)
  476. taggeditem_table = tables.TaggedItemTable(
  477. data=tagged_items,
  478. orderable=False
  479. )
  480. taggeditem_table.configure(request)
  481. object_types = [
  482. {
  483. 'content_type': ContentType.objects.get(pk=ti['content_type']),
  484. 'item_count': ti['item_count']
  485. } for ti in tagged_items.values('content_type').annotate(item_count=Count('pk'))
  486. ]
  487. return {
  488. 'taggeditem_table': taggeditem_table,
  489. 'tagged_item_count': tagged_items.count(),
  490. 'object_types': object_types,
  491. }
  492. @register_model_view(Tag, 'add', detail=False)
  493. @register_model_view(Tag, 'edit')
  494. class TagEditView(generic.ObjectEditView):
  495. queryset = Tag.objects.all()
  496. form = forms.TagForm
  497. @register_model_view(Tag, 'delete')
  498. class TagDeleteView(generic.ObjectDeleteView):
  499. queryset = Tag.objects.all()
  500. @register_model_view(Tag, 'bulk_import', detail=False)
  501. class TagBulkImportView(generic.BulkImportView):
  502. queryset = Tag.objects.all()
  503. model_form = forms.TagImportForm
  504. @register_model_view(Tag, 'bulk_edit', path='edit', detail=False)
  505. class TagBulkEditView(generic.BulkEditView):
  506. queryset = Tag.objects.annotate(
  507. items=count_related(TaggedItem, 'tag')
  508. )
  509. table = tables.TagTable
  510. form = forms.TagBulkEditForm
  511. @register_model_view(Tag, 'bulk_delete', path='delete', detail=False)
  512. class TagBulkDeleteView(generic.BulkDeleteView):
  513. queryset = Tag.objects.annotate(
  514. items=count_related(TaggedItem, 'tag')
  515. )
  516. table = tables.TagTable
  517. #
  518. # Config contexts
  519. #
  520. @register_model_view(ConfigContext, 'list', path='', detail=False)
  521. class ConfigContextListView(generic.ObjectListView):
  522. queryset = ConfigContext.objects.all()
  523. filterset = filtersets.ConfigContextFilterSet
  524. filterset_form = forms.ConfigContextFilterForm
  525. table = tables.ConfigContextTable
  526. template_name = 'extras/configcontext_list.html'
  527. actions = {
  528. 'add': {'add'},
  529. 'bulk_edit': {'change'},
  530. 'bulk_delete': {'delete'},
  531. 'bulk_sync': {'sync'},
  532. }
  533. @register_model_view(ConfigContext)
  534. class ConfigContextView(generic.ObjectView):
  535. queryset = ConfigContext.objects.all()
  536. def get_extra_context(self, request, instance):
  537. # Gather assigned objects for parsing in the template
  538. assigned_objects = (
  539. ('Regions', instance.regions.all),
  540. ('Site Groups', instance.site_groups.all),
  541. ('Sites', instance.sites.all),
  542. ('Locations', instance.locations.all),
  543. ('Device Types', instance.device_types.all),
  544. ('Roles', instance.roles.all),
  545. ('Platforms', instance.platforms.all),
  546. ('Cluster Types', instance.cluster_types.all),
  547. ('Cluster Groups', instance.cluster_groups.all),
  548. ('Clusters', instance.clusters.all),
  549. ('Tenant Groups', instance.tenant_groups.all),
  550. ('Tenants', instance.tenants.all),
  551. ('Tags', instance.tags.all),
  552. )
  553. # Determine user's preferred output format
  554. if request.GET.get('format') in ['json', 'yaml']:
  555. format = request.GET.get('format')
  556. if request.user.is_authenticated:
  557. request.user.config.set('data_format', format, commit=True)
  558. elif request.user.is_authenticated:
  559. format = request.user.config.get('data_format', 'json')
  560. else:
  561. format = 'json'
  562. return {
  563. 'assigned_objects': assigned_objects,
  564. 'format': format,
  565. }
  566. @register_model_view(ConfigContext, 'add', detail=False)
  567. @register_model_view(ConfigContext, 'edit')
  568. class ConfigContextEditView(generic.ObjectEditView):
  569. queryset = ConfigContext.objects.all()
  570. form = forms.ConfigContextForm
  571. @register_model_view(ConfigContext, 'delete')
  572. class ConfigContextDeleteView(generic.ObjectDeleteView):
  573. queryset = ConfigContext.objects.all()
  574. @register_model_view(ConfigContext, 'bulk_edit', path='edit', detail=False)
  575. class ConfigContextBulkEditView(generic.BulkEditView):
  576. queryset = ConfigContext.objects.all()
  577. filterset = filtersets.ConfigContextFilterSet
  578. table = tables.ConfigContextTable
  579. form = forms.ConfigContextBulkEditForm
  580. @register_model_view(ConfigContext, 'bulk_delete', path='delete', detail=False)
  581. class ConfigContextBulkDeleteView(generic.BulkDeleteView):
  582. queryset = ConfigContext.objects.all()
  583. filterset = filtersets.ConfigContextFilterSet
  584. table = tables.ConfigContextTable
  585. @register_model_view(ConfigContext, 'bulk_sync', path='sync', detail=False)
  586. class ConfigContextBulkSyncDataView(generic.BulkSyncDataView):
  587. queryset = ConfigContext.objects.all()
  588. class ObjectConfigContextView(generic.ObjectView):
  589. base_template = None
  590. template_name = 'extras/object_configcontext.html'
  591. def get_extra_context(self, request, instance):
  592. source_contexts = ConfigContext.objects.restrict(request.user, 'view').get_for_object(instance)
  593. # Determine user's preferred output format
  594. if request.GET.get('format') in ['json', 'yaml']:
  595. format = request.GET.get('format')
  596. if request.user.is_authenticated:
  597. request.user.config.set('data_format', format, commit=True)
  598. elif request.user.is_authenticated:
  599. format = request.user.config.get('data_format', 'json')
  600. else:
  601. format = 'json'
  602. return {
  603. 'rendered_context': instance.get_config_context(),
  604. 'source_contexts': source_contexts,
  605. 'format': format,
  606. 'base_template': self.base_template,
  607. }
  608. #
  609. # Config templates
  610. #
  611. @register_model_view(ConfigTemplate, 'list', path='', detail=False)
  612. class ConfigTemplateListView(generic.ObjectListView):
  613. queryset = ConfigTemplate.objects.annotate(
  614. device_count=count_related(Device, 'config_template'),
  615. vm_count=count_related(VirtualMachine, 'config_template'),
  616. role_count=count_related(DeviceRole, 'config_template'),
  617. platform_count=count_related(Platform, 'config_template'),
  618. )
  619. filterset = filtersets.ConfigTemplateFilterSet
  620. filterset_form = forms.ConfigTemplateFilterForm
  621. table = tables.ConfigTemplateTable
  622. template_name = 'extras/configtemplate_list.html'
  623. actions = {
  624. **DEFAULT_ACTION_PERMISSIONS,
  625. 'bulk_sync': {'sync'},
  626. }
  627. @register_model_view(ConfigTemplate)
  628. class ConfigTemplateView(generic.ObjectView):
  629. queryset = ConfigTemplate.objects.all()
  630. @register_model_view(ConfigTemplate, 'add', detail=False)
  631. @register_model_view(ConfigTemplate, 'edit')
  632. class ConfigTemplateEditView(generic.ObjectEditView):
  633. queryset = ConfigTemplate.objects.all()
  634. form = forms.ConfigTemplateForm
  635. @register_model_view(ConfigTemplate, 'delete')
  636. class ConfigTemplateDeleteView(generic.ObjectDeleteView):
  637. queryset = ConfigTemplate.objects.all()
  638. @register_model_view(ConfigTemplate, 'bulk_import', detail=False)
  639. class ConfigTemplateBulkImportView(generic.BulkImportView):
  640. queryset = ConfigTemplate.objects.all()
  641. model_form = forms.ConfigTemplateImportForm
  642. @register_model_view(ConfigTemplate, 'bulk_edit', path='edit', detail=False)
  643. class ConfigTemplateBulkEditView(generic.BulkEditView):
  644. queryset = ConfigTemplate.objects.all()
  645. filterset = filtersets.ConfigTemplateFilterSet
  646. table = tables.ConfigTemplateTable
  647. form = forms.ConfigTemplateBulkEditForm
  648. @register_model_view(ConfigTemplate, 'bulk_delete', path='delete', detail=False)
  649. class ConfigTemplateBulkDeleteView(generic.BulkDeleteView):
  650. queryset = ConfigTemplate.objects.all()
  651. filterset = filtersets.ConfigTemplateFilterSet
  652. table = tables.ConfigTemplateTable
  653. @register_model_view(ConfigTemplate, 'bulk_sync', path='sync', detail=False)
  654. class ConfigTemplateBulkSyncDataView(generic.BulkSyncDataView):
  655. queryset = ConfigTemplate.objects.all()
  656. #
  657. # Image attachments
  658. #
  659. @register_model_view(ImageAttachment, 'list', path='', detail=False)
  660. class ImageAttachmentListView(generic.ObjectListView):
  661. queryset = ImageAttachment.objects.all()
  662. filterset = filtersets.ImageAttachmentFilterSet
  663. filterset_form = forms.ImageAttachmentFilterForm
  664. table = tables.ImageAttachmentTable
  665. actions = {
  666. 'export': {'view'},
  667. }
  668. @register_model_view(ImageAttachment, 'add', detail=False)
  669. @register_model_view(ImageAttachment, 'edit')
  670. class ImageAttachmentEditView(generic.ObjectEditView):
  671. queryset = ImageAttachment.objects.all()
  672. form = forms.ImageAttachmentForm
  673. def alter_object(self, instance, request, args, kwargs):
  674. if not instance.pk:
  675. # Assign the parent object based on URL kwargs
  676. object_type = get_object_or_404(ContentType, pk=request.GET.get('object_type'))
  677. instance.parent = get_object_or_404(object_type.model_class(), pk=request.GET.get('object_id'))
  678. return instance
  679. def get_return_url(self, request, obj=None):
  680. return obj.parent.get_absolute_url() if obj else super().get_return_url(request)
  681. def get_extra_addanother_params(self, request):
  682. return {
  683. 'object_type': request.GET.get('object_type'),
  684. 'object_id': request.GET.get('object_id'),
  685. }
  686. @register_model_view(ImageAttachment, 'delete')
  687. class ImageAttachmentDeleteView(generic.ObjectDeleteView):
  688. queryset = ImageAttachment.objects.all()
  689. def get_return_url(self, request, obj=None):
  690. return obj.parent.get_absolute_url() if obj else super().get_return_url(request)
  691. #
  692. # Journal entries
  693. #
  694. @register_model_view(JournalEntry, 'list', path='', detail=False)
  695. class JournalEntryListView(generic.ObjectListView):
  696. queryset = JournalEntry.objects.all()
  697. filterset = filtersets.JournalEntryFilterSet
  698. filterset_form = forms.JournalEntryFilterForm
  699. table = tables.JournalEntryTable
  700. actions = {
  701. 'export': {'view'},
  702. 'bulk_import': {'add'},
  703. 'bulk_edit': {'change'},
  704. 'bulk_delete': {'delete'},
  705. }
  706. @register_model_view(JournalEntry)
  707. class JournalEntryView(generic.ObjectView):
  708. queryset = JournalEntry.objects.all()
  709. @register_model_view(JournalEntry, 'add', detail=False)
  710. @register_model_view(JournalEntry, 'edit')
  711. class JournalEntryEditView(generic.ObjectEditView):
  712. queryset = JournalEntry.objects.all()
  713. form = forms.JournalEntryForm
  714. def alter_object(self, obj, request, args, kwargs):
  715. if not obj.pk:
  716. obj.created_by = request.user
  717. return obj
  718. def get_return_url(self, request, instance):
  719. if not instance.assigned_object:
  720. return reverse('extras:journalentry_list')
  721. obj = instance.assigned_object
  722. viewname = get_viewname(obj, 'journal')
  723. return reverse(viewname, kwargs={'pk': obj.pk})
  724. @register_model_view(JournalEntry, 'delete')
  725. class JournalEntryDeleteView(generic.ObjectDeleteView):
  726. queryset = JournalEntry.objects.all()
  727. def get_return_url(self, request, instance):
  728. obj = instance.assigned_object
  729. viewname = get_viewname(obj, 'journal')
  730. return reverse(viewname, kwargs={'pk': obj.pk})
  731. @register_model_view(JournalEntry, 'bulk_import', detail=False)
  732. class JournalEntryBulkImportView(generic.BulkImportView):
  733. queryset = JournalEntry.objects.all()
  734. model_form = forms.JournalEntryImportForm
  735. @register_model_view(JournalEntry, 'bulk_edit', path='edit', detail=False)
  736. class JournalEntryBulkEditView(generic.BulkEditView):
  737. queryset = JournalEntry.objects.all()
  738. filterset = filtersets.JournalEntryFilterSet
  739. table = tables.JournalEntryTable
  740. form = forms.JournalEntryBulkEditForm
  741. @register_model_view(JournalEntry, 'bulk_delete', path='delete', detail=False)
  742. class JournalEntryBulkDeleteView(generic.BulkDeleteView):
  743. queryset = JournalEntry.objects.all()
  744. filterset = filtersets.JournalEntryFilterSet
  745. table = tables.JournalEntryTable
  746. #
  747. # Dashboard & widgets
  748. #
  749. class DashboardResetView(LoginRequiredMixin, View):
  750. template_name = 'extras/dashboard/reset.html'
  751. def get(self, request):
  752. get_object_or_404(Dashboard.objects.all(), user=request.user)
  753. form = ConfirmationForm()
  754. return render(request, self.template_name, {
  755. 'form': form,
  756. 'return_url': reverse('home'),
  757. })
  758. def post(self, request):
  759. dashboard = get_object_or_404(Dashboard.objects.all(), user=request.user)
  760. form = ConfirmationForm(request.POST)
  761. if form.is_valid():
  762. dashboard.delete()
  763. messages.success(request, _("Your dashboard has been reset."))
  764. return redirect(reverse('home'))
  765. return render(request, self.template_name, {
  766. 'form': form,
  767. 'return_url': reverse('home'),
  768. })
  769. class DashboardWidgetAddView(LoginRequiredMixin, View):
  770. template_name = 'extras/dashboard/widget_add.html'
  771. def get(self, request):
  772. if not request.htmx:
  773. return redirect('home')
  774. initial = request.GET or {
  775. 'widget_class': 'extras.NoteWidget',
  776. }
  777. widget_form = DashboardWidgetAddForm(initial=initial)
  778. widget_name = get_field_value(widget_form, 'widget_class')
  779. widget_class = get_widget_class(widget_name)
  780. config_form = widget_class.ConfigForm(initial=widget_class.default_config, prefix='config')
  781. return render(request, self.template_name, {
  782. 'widget_class': widget_class,
  783. 'widget_form': widget_form,
  784. 'config_form': config_form,
  785. })
  786. def post(self, request):
  787. widget_form = DashboardWidgetAddForm(request.POST)
  788. config_form = None
  789. widget_class = None
  790. if widget_form.is_valid():
  791. widget_class = get_widget_class(widget_form.cleaned_data['widget_class'])
  792. config_form = widget_class.ConfigForm(request.POST, prefix='config')
  793. if config_form.is_valid():
  794. data = widget_form.cleaned_data
  795. data.pop('widget_class')
  796. data['config'] = config_form.cleaned_data
  797. widget = widget_class(**data)
  798. request.user.dashboard.add_widget(widget)
  799. request.user.dashboard.save()
  800. messages.success(request, _('Added widget: ') + str(widget.id))
  801. return HttpResponse(headers={
  802. 'HX-Redirect': reverse('home'),
  803. })
  804. return render(request, self.template_name, {
  805. 'widget_class': widget_class,
  806. 'widget_form': widget_form,
  807. 'config_form': config_form,
  808. })
  809. class DashboardWidgetConfigView(LoginRequiredMixin, View):
  810. template_name = 'extras/dashboard/widget_config.html'
  811. def get(self, request, id):
  812. if not request.htmx:
  813. return redirect('home')
  814. widget = request.user.dashboard.get_widget(id)
  815. widget_form = DashboardWidgetForm(initial=widget.form_data)
  816. config_form = widget.ConfigForm(initial=widget.form_data.get('config'), prefix='config')
  817. return render(request, self.template_name, {
  818. 'widget_class': widget.__class__,
  819. 'widget_form': widget_form,
  820. 'config_form': config_form,
  821. 'form_url': reverse('extras:dashboardwidget_config', kwargs={'id': id})
  822. })
  823. def post(self, request, id):
  824. widget = request.user.dashboard.get_widget(id)
  825. widget_form = DashboardWidgetForm(request.POST)
  826. config_form = widget.ConfigForm(request.POST, prefix='config')
  827. if widget_form.is_valid() and config_form.is_valid():
  828. data = widget_form.cleaned_data
  829. data['config'] = config_form.cleaned_data
  830. request.user.dashboard.config[str(id)].update(data)
  831. request.user.dashboard.save()
  832. messages.success(request, _('Updated widget: ') + str(widget.id))
  833. return HttpResponse(headers={
  834. 'HX-Redirect': reverse('home'),
  835. })
  836. return render(request, self.template_name, {
  837. 'widget_form': widget_form,
  838. 'config_form': config_form,
  839. 'form_url': reverse('extras:dashboardwidget_config', kwargs={'id': id})
  840. })
  841. class DashboardWidgetDeleteView(LoginRequiredMixin, View):
  842. template_name = 'generic/object_delete.html'
  843. def get(self, request, id):
  844. if not request.htmx:
  845. return redirect('home')
  846. widget = request.user.dashboard.get_widget(id)
  847. form = ConfirmationForm(initial=request.GET)
  848. return render(request, 'htmx/delete_form.html', {
  849. 'object_type': widget.__class__.__name__,
  850. 'object': widget,
  851. 'form': form,
  852. 'form_url': reverse('extras:dashboardwidget_delete', kwargs={'id': id})
  853. })
  854. def post(self, request, id):
  855. form = ConfirmationForm(request.POST)
  856. if form.is_valid():
  857. request.user.dashboard.delete_widget(id)
  858. request.user.dashboard.save()
  859. messages.success(request, _('Deleted widget: ') + str(id))
  860. else:
  861. messages.error(request, _('Error deleting widget: ') + str(form.errors[0]))
  862. return redirect(reverse('home'))
  863. #
  864. # Scripts
  865. #
  866. @register_model_view(ScriptModule, 'edit')
  867. class ScriptModuleCreateView(generic.ObjectEditView):
  868. queryset = ScriptModule.objects.all()
  869. form = ManagedFileForm
  870. def alter_object(self, obj, *args, **kwargs):
  871. obj.file_root = ManagedFileRootPathChoices.SCRIPTS
  872. return obj
  873. @register_model_view(ScriptModule, 'delete')
  874. class ScriptModuleDeleteView(generic.ObjectDeleteView):
  875. queryset = ScriptModule.objects.all()
  876. default_return_url = 'extras:script_list'
  877. class ScriptListView(ContentTypePermissionRequiredMixin, View):
  878. def get_required_permission(self):
  879. return 'extras.view_script'
  880. def get(self, request):
  881. script_modules = ScriptModule.objects.restrict(request.user).prefetch_related(
  882. 'data_source', 'data_file', 'jobs'
  883. )
  884. return render(request, 'extras/script_list.html', {
  885. 'model': ScriptModule,
  886. 'script_modules': script_modules,
  887. })
  888. class BaseScriptView(generic.ObjectView):
  889. queryset = Script.objects.all()
  890. def _get_script_class(self, script):
  891. """
  892. Return an instance of the Script's Python class
  893. """
  894. if script_class := script.python_class:
  895. return script_class()
  896. class ScriptView(BaseScriptView):
  897. def get(self, request, **kwargs):
  898. script = self.get_object(**kwargs)
  899. script_class = self._get_script_class(script)
  900. if not script_class:
  901. return render(request, 'extras/script.html', {
  902. 'script': script,
  903. })
  904. form = script_class.as_form(initial=normalize_querydict(request.GET))
  905. return render(request, 'extras/script.html', {
  906. 'script': script,
  907. 'script_class': script_class,
  908. 'form': form,
  909. 'job_count': script.jobs.count(),
  910. })
  911. def post(self, request, **kwargs):
  912. script = self.get_object(**kwargs)
  913. if not request.user.has_perm('extras.run_script', obj=script):
  914. return HttpResponseForbidden()
  915. script_class = self._get_script_class(script)
  916. if not script_class:
  917. return render(request, 'extras/script.html', {
  918. 'script': script,
  919. })
  920. form = script_class.as_form(request.POST, request.FILES)
  921. # Allow execution only if RQ worker process is running
  922. if not get_workers_for_queue('default'):
  923. messages.error(request, _("Unable to run script: RQ worker process not running."))
  924. elif form.is_valid():
  925. ScriptJob = import_string("extras.jobs.ScriptJob")
  926. job = ScriptJob.enqueue(
  927. instance=script,
  928. user=request.user,
  929. schedule_at=form.cleaned_data.pop('_schedule_at'),
  930. interval=form.cleaned_data.pop('_interval'),
  931. data=form.cleaned_data,
  932. request=copy_safe_request(request),
  933. job_timeout=script.python_class.job_timeout,
  934. commit=form.cleaned_data.pop('_commit'),
  935. )
  936. return redirect('extras:script_result', job_pk=job.pk)
  937. return render(request, 'extras/script.html', {
  938. 'script': script,
  939. 'script_class': script.python_class(),
  940. 'form': form,
  941. 'job_count': script.jobs.count(),
  942. })
  943. class ScriptSourceView(BaseScriptView):
  944. queryset = Script.objects.all()
  945. def get(self, request, **kwargs):
  946. script = self.get_object(**kwargs)
  947. script_class = self._get_script_class(script)
  948. return render(request, 'extras/script/source.html', {
  949. 'script': script,
  950. 'script_class': script_class,
  951. 'job_count': script.jobs.count(),
  952. 'tab': 'source',
  953. })
  954. class ScriptJobsView(BaseScriptView):
  955. queryset = Script.objects.all()
  956. def get(self, request, **kwargs):
  957. script = self.get_object(**kwargs)
  958. jobs_table = JobTable(
  959. data=script.jobs.all(),
  960. orderable=False,
  961. user=request.user
  962. )
  963. jobs_table.configure(request)
  964. return render(request, 'extras/script/jobs.html', {
  965. 'script': script,
  966. 'table': jobs_table,
  967. 'job_count': script.jobs.count(),
  968. 'tab': 'jobs',
  969. })
  970. class ScriptResultView(TableMixin, generic.ObjectView):
  971. queryset = Job.objects.all()
  972. def get_required_permission(self):
  973. return 'extras.view_script'
  974. def get_table(self, job, request, bulk_actions=True):
  975. data = []
  976. tests = None
  977. table = None
  978. index = 0
  979. try:
  980. log_threshold = LOG_LEVEL_RANK[request.GET.get('log_threshold', LogLevelChoices.LOG_DEBUG)]
  981. except KeyError:
  982. log_threshold = LOG_LEVEL_RANK[LogLevelChoices.LOG_DEBUG]
  983. if job.data:
  984. if 'log' in job.data:
  985. if 'tests' in job.data:
  986. tests = job.data['tests']
  987. for log in job.data['log']:
  988. log_level = LOG_LEVEL_RANK.get(log.get('status'), LogLevelChoices.LOG_DEFAULT)
  989. if log_level >= log_threshold:
  990. index += 1
  991. result = {
  992. 'index': index,
  993. 'time': log.get('time'),
  994. 'status': log.get('status'),
  995. 'message': log.get('message'),
  996. 'object': log.get('obj'),
  997. 'url': log.get('url'),
  998. }
  999. data.append(result)
  1000. table = ScriptResultsTable(data, user=request.user)
  1001. table.configure(request)
  1002. else:
  1003. # for legacy reports
  1004. tests = job.data
  1005. if tests:
  1006. for method, test_data in tests.items():
  1007. if 'log' in test_data:
  1008. for time, status, obj, url, message in test_data['log']:
  1009. log_level = LOG_LEVEL_RANK.get(status, LogLevelChoices.LOG_DEFAULT)
  1010. if log_level >= log_threshold:
  1011. index += 1
  1012. result = {
  1013. 'index': index,
  1014. 'method': method,
  1015. 'time': time,
  1016. 'status': status,
  1017. 'object': obj,
  1018. 'url': url,
  1019. 'message': message,
  1020. }
  1021. data.append(result)
  1022. table = ReportResultsTable(data, user=request.user)
  1023. table.configure(request)
  1024. return table
  1025. def get(self, request, **kwargs):
  1026. table = None
  1027. job = get_object_or_404(Job.objects.all(), pk=kwargs.get('job_pk'))
  1028. if job.completed:
  1029. table = self.get_table(job, request, bulk_actions=False)
  1030. log_threshold = request.GET.get('log_threshold', LogLevelChoices.LOG_DEBUG)
  1031. if log_threshold not in LOG_LEVEL_RANK:
  1032. log_threshold = LogLevelChoices.LOG_DEBUG
  1033. context = {
  1034. 'script': job.object,
  1035. 'job': job,
  1036. 'table': table,
  1037. 'log_levels': dict(LogLevelChoices),
  1038. 'log_threshold': log_threshold,
  1039. }
  1040. if job.data and 'log' in job.data:
  1041. # Script
  1042. context['tests'] = job.data.get('tests', {})
  1043. elif job.data:
  1044. # Legacy Report
  1045. context['tests'] = {
  1046. name: data for name, data in job.data.items()
  1047. if name.startswith('test_')
  1048. }
  1049. # If this is an HTMX request, return only the result HTML
  1050. if htmx_partial(request):
  1051. if request.GET.get('log'):
  1052. # If log=True, render only the log table
  1053. return render(request, 'htmx/table.html', context)
  1054. response = render(request, 'extras/htmx/script_result.html', context)
  1055. if job.completed or not job.started:
  1056. response.status_code = 286
  1057. return response
  1058. return render(request, 'extras/script_result.html', context)
  1059. #
  1060. # Markdown
  1061. #
  1062. class RenderMarkdownView(LoginRequiredMixin, View):
  1063. def post(self, request):
  1064. form = forms.RenderMarkdownForm(request.POST)
  1065. if not form.is_valid():
  1066. HttpResponseBadRequest()
  1067. rendered = render_markdown(form.cleaned_data['text'])
  1068. return HttpResponse(rendered)