views.py 58 KB

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