views.py 37 KB

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