views.py 42 KB

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