views.py 50 KB

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