views.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. from collections import OrderedDict
  2. from django.contrib.contenttypes.models import ContentType
  3. from django.db.models import Count
  4. from django.http import Http404
  5. from django_rq.queues import get_connection
  6. from rest_framework import status
  7. from rest_framework.decorators import action
  8. from rest_framework.exceptions import PermissionDenied
  9. from rest_framework.response import Response
  10. from rest_framework.routers import APIRootView
  11. from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
  12. from rq import Worker
  13. from extras import filters
  14. from extras.choices import JobResultStatusChoices
  15. from extras.models import (
  16. ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, JobResult, Tag,
  17. )
  18. from extras.reports import get_report, get_reports, run_report
  19. from extras.scripts import get_script, get_scripts, run_script
  20. from utilities.api import IsAuthenticatedOrLoginNotRequired, ModelViewSet
  21. from utilities.exceptions import RQWorkerNotRunningException
  22. from utilities.metadata import ContentTypeMetadata
  23. from utilities.utils import copy_safe_request
  24. from . import serializers
  25. class ExtrasRootView(APIRootView):
  26. """
  27. Extras API root view
  28. """
  29. def get_view_name(self):
  30. return 'Extras'
  31. #
  32. # Custom field choices
  33. #
  34. class CustomFieldChoicesViewSet(ViewSet):
  35. """
  36. """
  37. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  38. def __init__(self, *args, **kwargs):
  39. super(CustomFieldChoicesViewSet, self).__init__(*args, **kwargs)
  40. self._fields = OrderedDict()
  41. for cfc in CustomFieldChoice.objects.all():
  42. self._fields.setdefault(cfc.field.name, {})
  43. self._fields[cfc.field.name][cfc.value] = cfc.pk
  44. def list(self, request):
  45. return Response(self._fields)
  46. def retrieve(self, request, pk):
  47. if pk not in self._fields:
  48. raise Http404
  49. return Response(self._fields[pk])
  50. def get_view_name(self):
  51. return "Custom Field choices"
  52. #
  53. # Custom fields
  54. #
  55. class CustomFieldModelViewSet(ModelViewSet):
  56. """
  57. Include the applicable set of CustomFields in the ModelViewSet context.
  58. """
  59. def get_serializer_context(self):
  60. # Gather all custom fields for the model
  61. content_type = ContentType.objects.get_for_model(self.queryset.model)
  62. custom_fields = content_type.custom_fields.prefetch_related('choices')
  63. # Cache all relevant CustomFieldChoices. This saves us from having to do a lookup per select field per object.
  64. custom_field_choices = {}
  65. for field in custom_fields:
  66. for cfc in field.choices.all():
  67. custom_field_choices[cfc.id] = cfc.value
  68. custom_field_choices = custom_field_choices
  69. context = super().get_serializer_context()
  70. context.update({
  71. 'custom_fields': custom_fields,
  72. 'custom_field_choices': custom_field_choices,
  73. })
  74. return context
  75. def get_queryset(self):
  76. # Prefetch custom field values
  77. return super().get_queryset().prefetch_related('custom_field_values__field')
  78. #
  79. # Graphs
  80. #
  81. class GraphViewSet(ModelViewSet):
  82. metadata_class = ContentTypeMetadata
  83. queryset = Graph.objects.all()
  84. serializer_class = serializers.GraphSerializer
  85. filterset_class = filters.GraphFilterSet
  86. #
  87. # Export templates
  88. #
  89. class ExportTemplateViewSet(ModelViewSet):
  90. metadata_class = ContentTypeMetadata
  91. queryset = ExportTemplate.objects.all()
  92. serializer_class = serializers.ExportTemplateSerializer
  93. filterset_class = filters.ExportTemplateFilterSet
  94. #
  95. # Tags
  96. #
  97. class TagViewSet(ModelViewSet):
  98. queryset = Tag.objects.annotate(
  99. tagged_items=Count('extras_taggeditem_items')
  100. ).order_by(*Tag._meta.ordering)
  101. serializer_class = serializers.TagSerializer
  102. filterset_class = filters.TagFilterSet
  103. #
  104. # Image attachments
  105. #
  106. class ImageAttachmentViewSet(ModelViewSet):
  107. metadata_class = ContentTypeMetadata
  108. queryset = ImageAttachment.objects.all()
  109. serializer_class = serializers.ImageAttachmentSerializer
  110. #
  111. # Config contexts
  112. #
  113. class ConfigContextViewSet(ModelViewSet):
  114. queryset = ConfigContext.objects.prefetch_related(
  115. 'regions', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
  116. )
  117. serializer_class = serializers.ConfigContextSerializer
  118. filterset_class = filters.ConfigContextFilterSet
  119. #
  120. # Reports
  121. #
  122. class ReportViewSet(ViewSet):
  123. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  124. _ignore_model_permissions = True
  125. exclude_from_schema = True
  126. lookup_value_regex = '[^/]+' # Allow dots
  127. def _retrieve_report(self, pk):
  128. # Read the PK as "<module>.<report>"
  129. if '.' not in pk:
  130. raise Http404
  131. module_name, report_name = pk.split('.', 1)
  132. # Raise a 404 on an invalid Report module/name
  133. report = get_report(module_name, report_name)
  134. if report is None:
  135. raise Http404
  136. return report
  137. def list(self, request):
  138. """
  139. Compile all reports and their related results (if any). Result data is deferred in the list view.
  140. """
  141. report_list = []
  142. report_content_type = ContentType.objects.get(app_label='extras', model='report')
  143. results = {
  144. r.name: r
  145. for r in JobResult.objects.filter(
  146. obj_type=report_content_type,
  147. status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
  148. ).defer('data')
  149. }
  150. # Iterate through all available Reports.
  151. for module_name, reports in get_reports():
  152. for report in reports:
  153. # Attach the relevant JobResult (if any) to each Report.
  154. report.result = results.get(report.full_name, None)
  155. report_list.append(report)
  156. serializer = serializers.ReportSerializer(report_list, many=True, context={
  157. 'request': request,
  158. })
  159. return Response(serializer.data)
  160. def retrieve(self, request, pk):
  161. """
  162. Retrieve a single Report identified as "<module>.<report>".
  163. """
  164. # Retrieve the Report and JobResult, if any.
  165. report = self._retrieve_report(pk)
  166. report_content_type = ContentType.objects.get(app_label='extras', model='report')
  167. report.result = JobResult.objects.filter(
  168. obj_type=report_content_type,
  169. name=report.full_name,
  170. status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
  171. ).first()
  172. serializer = serializers.ReportDetailSerializer(report, context={
  173. 'request': request
  174. })
  175. return Response(serializer.data)
  176. @action(detail=True, methods=['post'])
  177. def run(self, request, pk):
  178. """
  179. Run a Report identified as "<module>.<script>" and return the pending JobResult as the result
  180. """
  181. # Check that the user has permission to run reports.
  182. if not request.user.has_perm('extras.run_script'):
  183. raise PermissionDenied("This user does not have permission to run reports.")
  184. # Check that at least one RQ worker is running
  185. if not Worker.count(get_connection('default')):
  186. raise RQWorkerNotRunningException()
  187. # Retrieve and run the Report. This will create a new JobResult.
  188. report = self._retrieve_report(pk)
  189. report_content_type = ContentType.objects.get(app_label='extras', model='report')
  190. job_result = JobResult.enqueue_job(
  191. run_report,
  192. report.full_name,
  193. report_content_type,
  194. request.user
  195. )
  196. report.result = job_result
  197. serializer = serializers.ReportDetailSerializer(report, context={'request': request})
  198. return Response(serializer.data)
  199. #
  200. # Scripts
  201. #
  202. class ScriptViewSet(ViewSet):
  203. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  204. _ignore_model_permissions = True
  205. exclude_from_schema = True
  206. lookup_value_regex = '[^/]+' # Allow dots
  207. def _get_script(self, pk):
  208. module_name, script_name = pk.split('.')
  209. script = get_script(module_name, script_name)
  210. if script is None:
  211. raise Http404
  212. return script
  213. def list(self, request):
  214. script_content_type = ContentType.objects.get(app_label='extras', model='script')
  215. results = {
  216. r.name: r
  217. for r in JobResult.objects.filter(
  218. obj_type=script_content_type,
  219. status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
  220. ).defer('data').order_by('created')
  221. }
  222. flat_list = []
  223. for script_list in get_scripts().values():
  224. flat_list.extend(script_list.values())
  225. # Attach JobResult objects to each script (if any)
  226. for script in flat_list:
  227. script.result = results.get(script.full_name, None)
  228. serializer = serializers.ScriptSerializer(flat_list, many=True, context={'request': request})
  229. return Response(serializer.data)
  230. def retrieve(self, request, pk):
  231. script = self._get_script(pk)
  232. script_content_type = ContentType.objects.get(app_label='extras', model='script')
  233. script.result = JobResult.objects.filter(
  234. obj_type=script_content_type,
  235. name=script.full_name,
  236. status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
  237. ).first()
  238. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  239. return Response(serializer.data)
  240. def post(self, request, pk):
  241. """
  242. Run a Script identified as "<module>.<script>" and return the pending JobResult as the result
  243. """
  244. script = self._get_script(pk)()
  245. input_serializer = serializers.ScriptInputSerializer(data=request.data)
  246. # Check that at least one RQ worker is running
  247. if not Worker.count(get_connection('default')):
  248. raise RQWorkerNotRunningException()
  249. if input_serializer.is_valid():
  250. data = input_serializer.data['data']
  251. commit = input_serializer.data['commit']
  252. script_content_type = ContentType.objects.get(app_label='extras', model='script')
  253. job_result = JobResult.enqueue_job(
  254. run_script,
  255. script.full_name,
  256. script_content_type,
  257. request.user,
  258. data=data,
  259. request=copy_safe_request(request),
  260. commit=commit
  261. )
  262. script.result = job_result
  263. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  264. return Response(serializer.data)
  265. return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  266. #
  267. # Change logging
  268. #
  269. class ObjectChangeViewSet(ReadOnlyModelViewSet):
  270. """
  271. Retrieve a list of recent changes.
  272. """
  273. metadata_class = ContentTypeMetadata
  274. queryset = ObjectChange.objects.prefetch_related('user')
  275. serializer_class = serializers.ObjectChangeSerializer
  276. filterset_class = filters.ObjectChangeFilterSet
  277. #
  278. # Job Results
  279. #
  280. class JobResultViewSet(ReadOnlyModelViewSet):
  281. """
  282. Retrieve a list of job results
  283. """
  284. queryset = JobResult.objects.prefetch_related('user')
  285. serializer_class = serializers.JobResultSerializer
  286. filterset_class = filters.JobResultFilterSet