1from django.contrib import messages 2from django.contrib.contenttypes.models import ContentType 3from django.db.models import Count, Q 4from django.http import Http404, HttpResponseForbidden 5from django.shortcuts import get_object_or_404, redirect, render 6from django.urls import reverse 7from django.views.generic import View 8from django_rq.queues import get_connection 9from rq import Worker 10 11from netbox.views import generic 12from utilities.forms import ConfirmationForm 13from utilities.tables import paginate_table 14from utilities.utils import copy_safe_request, count_related, normalize_querydict, shallow_compare_dict 15from utilities.views import ContentTypePermissionRequiredMixin 16from . import filtersets, forms, tables 17from .choices import JobResultStatusChoices 18from .models import * 19from .reports import get_report, get_reports, run_report 20from .scripts import get_scripts, run_script 21 22 23# 24# Custom fields 25# 26 27class CustomFieldListView(generic.ObjectListView): 28 queryset = CustomField.objects.all() 29 filterset = filtersets.CustomFieldFilterSet 30 filterset_form = forms.CustomFieldFilterForm 31 table = tables.CustomFieldTable 32 33 34class CustomFieldView(generic.ObjectView): 35 queryset = CustomField.objects.all() 36 37 38class CustomFieldEditView(generic.ObjectEditView): 39 queryset = CustomField.objects.all() 40 model_form = forms.CustomFieldForm 41 42 43class CustomFieldDeleteView(generic.ObjectDeleteView): 44 queryset = CustomField.objects.all() 45 46 47class CustomFieldBulkImportView(generic.BulkImportView): 48 queryset = CustomField.objects.all() 49 model_form = forms.CustomFieldCSVForm 50 table = tables.CustomFieldTable 51 52 53class CustomFieldBulkEditView(generic.BulkEditView): 54 queryset = CustomField.objects.all() 55 filterset = filtersets.CustomFieldFilterSet 56 table = tables.CustomFieldTable 57 form = forms.CustomFieldBulkEditForm 58 59 60class CustomFieldBulkDeleteView(generic.BulkDeleteView): 61 queryset = CustomField.objects.all() 62 filterset = filtersets.CustomFieldFilterSet 63 table = tables.CustomFieldTable 64 65 66# 67# Custom links 68# 69 70class CustomLinkListView(generic.ObjectListView): 71 queryset = CustomLink.objects.all() 72 filterset = filtersets.CustomLinkFilterSet 73 filterset_form = forms.CustomLinkFilterForm 74 table = tables.CustomLinkTable 75 76 77class CustomLinkView(generic.ObjectView): 78 queryset = CustomLink.objects.all() 79 80 81class CustomLinkEditView(generic.ObjectEditView): 82 queryset = CustomLink.objects.all() 83 model_form = forms.CustomLinkForm 84 85 86class CustomLinkDeleteView(generic.ObjectDeleteView): 87 queryset = CustomLink.objects.all() 88 89 90class CustomLinkBulkImportView(generic.BulkImportView): 91 queryset = CustomLink.objects.all() 92 model_form = forms.CustomLinkCSVForm 93 table = tables.CustomLinkTable 94 95 96class CustomLinkBulkEditView(generic.BulkEditView): 97 queryset = CustomLink.objects.all() 98 filterset = filtersets.CustomLinkFilterSet 99 table = tables.CustomLinkTable 100 form = forms.CustomLinkBulkEditForm 101 102 103class CustomLinkBulkDeleteView(generic.BulkDeleteView): 104 queryset = CustomLink.objects.all() 105 filterset = filtersets.CustomLinkFilterSet 106 table = tables.CustomLinkTable 107 108 109# 110# Export templates 111# 112 113class ExportTemplateListView(generic.ObjectListView): 114 queryset = ExportTemplate.objects.all() 115 filterset = filtersets.ExportTemplateFilterSet 116 filterset_form = forms.ExportTemplateFilterForm 117 table = tables.ExportTemplateTable 118 119 120class ExportTemplateView(generic.ObjectView): 121 queryset = ExportTemplate.objects.all() 122 123 124class ExportTemplateEditView(generic.ObjectEditView): 125 queryset = ExportTemplate.objects.all() 126 model_form = forms.ExportTemplateForm 127 128 129class ExportTemplateDeleteView(generic.ObjectDeleteView): 130 queryset = ExportTemplate.objects.all() 131 132 133class ExportTemplateBulkImportView(generic.BulkImportView): 134 queryset = ExportTemplate.objects.all() 135 model_form = forms.ExportTemplateCSVForm 136 table = tables.ExportTemplateTable 137 138 139class ExportTemplateBulkEditView(generic.BulkEditView): 140 queryset = ExportTemplate.objects.all() 141 filterset = filtersets.ExportTemplateFilterSet 142 table = tables.ExportTemplateTable 143 form = forms.ExportTemplateBulkEditForm 144 145 146class ExportTemplateBulkDeleteView(generic.BulkDeleteView): 147 queryset = ExportTemplate.objects.all() 148 filterset = filtersets.ExportTemplateFilterSet 149 table = tables.ExportTemplateTable 150 151 152# 153# Webhooks 154# 155 156class WebhookListView(generic.ObjectListView): 157 queryset = Webhook.objects.all() 158 filterset = filtersets.WebhookFilterSet 159 filterset_form = forms.WebhookFilterForm 160 table = tables.WebhookTable 161 162 163class WebhookView(generic.ObjectView): 164 queryset = Webhook.objects.all() 165 166 167class WebhookEditView(generic.ObjectEditView): 168 queryset = Webhook.objects.all() 169 model_form = forms.WebhookForm 170 171 172class WebhookDeleteView(generic.ObjectDeleteView): 173 queryset = Webhook.objects.all() 174 175 176class WebhookBulkImportView(generic.BulkImportView): 177 queryset = Webhook.objects.all() 178 model_form = forms.WebhookCSVForm 179 table = tables.WebhookTable 180 181 182class WebhookBulkEditView(generic.BulkEditView): 183 queryset = Webhook.objects.all() 184 filterset = filtersets.WebhookFilterSet 185 table = tables.WebhookTable 186 form = forms.WebhookBulkEditForm 187 188 189class WebhookBulkDeleteView(generic.BulkDeleteView): 190 queryset = Webhook.objects.all() 191 filterset = filtersets.WebhookFilterSet 192 table = tables.WebhookTable 193 194 195# 196# Tags 197# 198 199class TagListView(generic.ObjectListView): 200 queryset = Tag.objects.annotate( 201 items=count_related(TaggedItem, 'tag') 202 ) 203 filterset = filtersets.TagFilterSet 204 filterset_form = forms.TagFilterForm 205 table = tables.TagTable 206 207 208class TagView(generic.ObjectView): 209 queryset = Tag.objects.all() 210 211 def get_extra_context(self, request, instance): 212 tagged_items = TaggedItem.objects.filter(tag=instance) 213 taggeditem_table = tables.TaggedItemTable( 214 data=tagged_items, 215 orderable=False 216 ) 217 paginate_table(taggeditem_table, request) 218 219 object_types = [ 220 { 221 'content_type': ContentType.objects.get(pk=ti['content_type']), 222 'item_count': ti['item_count'] 223 } for ti in tagged_items.values('content_type').annotate(item_count=Count('pk')) 224 ] 225 226 return { 227 'taggeditem_table': taggeditem_table, 228 'tagged_item_count': tagged_items.count(), 229 'object_types': object_types, 230 } 231 232 233class TagEditView(generic.ObjectEditView): 234 queryset = Tag.objects.all() 235 model_form = forms.TagForm 236 237 238class TagDeleteView(generic.ObjectDeleteView): 239 queryset = Tag.objects.all() 240 241 242class TagBulkImportView(generic.BulkImportView): 243 queryset = Tag.objects.all() 244 model_form = forms.TagCSVForm 245 table = tables.TagTable 246 247 248class TagBulkEditView(generic.BulkEditView): 249 queryset = Tag.objects.annotate( 250 items=count_related(TaggedItem, 'tag') 251 ) 252 table = tables.TagTable 253 form = forms.TagBulkEditForm 254 255 256class TagBulkDeleteView(generic.BulkDeleteView): 257 queryset = Tag.objects.annotate( 258 items=count_related(TaggedItem, 'tag') 259 ) 260 table = tables.TagTable 261 262 263# 264# Config contexts 265# 266 267class ConfigContextListView(generic.ObjectListView): 268 queryset = ConfigContext.objects.all() 269 filterset = filtersets.ConfigContextFilterSet 270 filterset_form = forms.ConfigContextFilterForm 271 table = tables.ConfigContextTable 272 action_buttons = ('add',) 273 274 275class ConfigContextView(generic.ObjectView): 276 queryset = ConfigContext.objects.all() 277 278 def get_extra_context(self, request, instance): 279 # Gather assigned objects for parsing in the template 280 assigned_objects = ( 281 ('Regions', instance.regions.all), 282 ('Site Groups', instance.site_groups.all), 283 ('Sites', instance.sites.all), 284 ('Device Types', instance.device_types.all), 285 ('Roles', instance.roles.all), 286 ('Platforms', instance.platforms.all), 287 ('Cluster Groups', instance.cluster_groups.all), 288 ('Clusters', instance.clusters.all), 289 ('Tenant Groups', instance.tenant_groups.all), 290 ('Tenants', instance.tenants.all), 291 ('Tags', instance.tags.all), 292 ) 293 294 # Determine user's preferred output format 295 if request.GET.get('format') in ['json', 'yaml']: 296 format = request.GET.get('format') 297 if request.user.is_authenticated: 298 request.user.config.set('extras.configcontext.format', format, commit=True) 299 elif request.user.is_authenticated: 300 format = request.user.config.get('extras.configcontext.format', 'json') 301 else: 302 format = 'json' 303 304 return { 305 'assigned_objects': assigned_objects, 306 'format': format, 307 } 308 309 310class ConfigContextEditView(generic.ObjectEditView): 311 queryset = ConfigContext.objects.all() 312 model_form = forms.ConfigContextForm 313 template_name = 'extras/configcontext_edit.html' 314 315 316class ConfigContextBulkEditView(generic.BulkEditView): 317 queryset = ConfigContext.objects.all() 318 filterset = filtersets.ConfigContextFilterSet 319 table = tables.ConfigContextTable 320 form = forms.ConfigContextBulkEditForm 321 322 323class ConfigContextDeleteView(generic.ObjectDeleteView): 324 queryset = ConfigContext.objects.all() 325 326 327class ConfigContextBulkDeleteView(generic.BulkDeleteView): 328 queryset = ConfigContext.objects.all() 329 table = tables.ConfigContextTable 330 331 332class ObjectConfigContextView(generic.ObjectView): 333 base_template = None 334 template_name = 'extras/object_configcontext.html' 335 336 def get_extra_context(self, request, instance): 337 source_contexts = ConfigContext.objects.restrict(request.user, 'view').get_for_object(instance) 338 339 # Determine user's preferred output format 340 if request.GET.get('format') in ['json', 'yaml']: 341 format = request.GET.get('format') 342 if request.user.is_authenticated: 343 request.user.config.set('extras.configcontext.format', format, commit=True) 344 elif request.user.is_authenticated: 345 format = request.user.config.get('extras.configcontext.format', 'json') 346 else: 347 format = 'json' 348 349 return { 350 'rendered_context': instance.get_config_context(), 351 'source_contexts': source_contexts, 352 'format': format, 353 'base_template': self.base_template, 354 'active_tab': 'config-context', 355 } 356 357 358# 359# Change logging 360# 361 362class ObjectChangeListView(generic.ObjectListView): 363 queryset = ObjectChange.objects.all() 364 filterset = filtersets.ObjectChangeFilterSet 365 filterset_form = forms.ObjectChangeFilterForm 366 table = tables.ObjectChangeTable 367 template_name = 'extras/objectchange_list.html' 368 action_buttons = ('export',) 369 370 371class ObjectChangeView(generic.ObjectView): 372 queryset = ObjectChange.objects.all() 373 374 def get_extra_context(self, request, instance): 375 related_changes = ObjectChange.objects.restrict(request.user, 'view').filter( 376 request_id=instance.request_id 377 ).exclude( 378 pk=instance.pk 379 ) 380 related_changes_table = tables.ObjectChangeTable( 381 data=related_changes[:50], 382 orderable=False 383 ) 384 385 objectchanges = ObjectChange.objects.restrict(request.user, 'view').filter( 386 changed_object_type=instance.changed_object_type, 387 changed_object_id=instance.changed_object_id, 388 ) 389 390 next_change = objectchanges.filter(time__gt=instance.time).order_by('time').first() 391 prev_change = objectchanges.filter(time__lt=instance.time).order_by('-time').first() 392 393 if not instance.prechange_data and instance.action in ['update', 'delete'] and prev_change: 394 non_atomic_change = True 395 prechange_data = prev_change.postchange_data 396 else: 397 non_atomic_change = False 398 prechange_data = instance.prechange_data 399 400 if prechange_data and instance.postchange_data: 401 diff_added = shallow_compare_dict( 402 prechange_data or dict(), 403 instance.postchange_data or dict(), 404 exclude=['last_updated'], 405 ) 406 diff_removed = { 407 x: prechange_data.get(x) for x in diff_added 408 } if prechange_data else {} 409 else: 410 diff_added = None 411 diff_removed = None 412 413 return { 414 'diff_added': diff_added, 415 'diff_removed': diff_removed, 416 'next_change': next_change, 417 'prev_change': prev_change, 418 'related_changes_table': related_changes_table, 419 'related_changes_count': related_changes.count(), 420 'non_atomic_change': non_atomic_change 421 } 422 423 424class ObjectChangeLogView(View): 425 """ 426 Present a history of changes made to a particular object. 427 428 base_template: The name of the template to extend. If not provided, "<app>/<model>.html" will be used. 429 """ 430 base_template = None 431 432 def get(self, request, model, **kwargs): 433 434 # Handle QuerySet restriction of parent object if needed 435 if hasattr(model.objects, 'restrict'): 436 obj = get_object_or_404(model.objects.restrict(request.user, 'view'), **kwargs) 437 else: 438 obj = get_object_or_404(model, **kwargs) 439 440 # Gather all changes for this object (and its related objects) 441 content_type = ContentType.objects.get_for_model(model) 442 objectchanges = ObjectChange.objects.restrict(request.user, 'view').prefetch_related( 443 'user', 'changed_object_type' 444 ).filter( 445 Q(changed_object_type=content_type, changed_object_id=obj.pk) | 446 Q(related_object_type=content_type, related_object_id=obj.pk) 447 ) 448 objectchanges_table = tables.ObjectChangeTable( 449 data=objectchanges, 450 orderable=False 451 ) 452 paginate_table(objectchanges_table, request) 453 454 # Default to using "<app>/<model>.html" as the template, if it exists. Otherwise, 455 # fall back to using base.html. 456 if self.base_template is None: 457 self.base_template = f"{model._meta.app_label}/{model._meta.model_name}.html" 458 459 return render(request, 'extras/object_changelog.html', { 460 'object': obj, 461 'table': objectchanges_table, 462 'base_template': self.base_template, 463 'active_tab': 'changelog', 464 }) 465 466 467# 468# Image attachments 469# 470 471class ImageAttachmentEditView(generic.ObjectEditView): 472 queryset = ImageAttachment.objects.all() 473 model_form = forms.ImageAttachmentForm 474 475 def alter_obj(self, instance, request, args, kwargs): 476 if not instance.pk: 477 # Assign the parent object based on URL kwargs 478 content_type = get_object_or_404(ContentType, pk=request.GET.get('content_type')) 479 instance.parent = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id')) 480 return instance 481 482 def get_return_url(self, request, obj=None): 483 return obj.parent.get_absolute_url() if obj else super().get_return_url(request) 484 485 486class ImageAttachmentDeleteView(generic.ObjectDeleteView): 487 queryset = ImageAttachment.objects.all() 488 489 def get_return_url(self, request, obj=None): 490 return obj.parent.get_absolute_url() if obj else super().get_return_url(request) 491 492 493# 494# Journal entries 495# 496 497class JournalEntryListView(generic.ObjectListView): 498 queryset = JournalEntry.objects.all() 499 filterset = filtersets.JournalEntryFilterSet 500 filterset_form = forms.JournalEntryFilterForm 501 table = tables.JournalEntryTable 502 action_buttons = ('export',) 503 504 505class JournalEntryView(generic.ObjectView): 506 queryset = JournalEntry.objects.all() 507 508 509class JournalEntryEditView(generic.ObjectEditView): 510 queryset = JournalEntry.objects.all() 511 model_form = forms.JournalEntryForm 512 513 def alter_obj(self, obj, request, args, kwargs): 514 if not obj.pk: 515 obj.created_by = request.user 516 return obj 517 518 def get_return_url(self, request, instance): 519 if not instance.assigned_object: 520 return reverse('extras:journalentry_list') 521 obj = instance.assigned_object 522 viewname = f'{obj._meta.app_label}:{obj._meta.model_name}_journal' 523 return reverse(viewname, kwargs={'pk': obj.pk}) 524 525 526class JournalEntryDeleteView(generic.ObjectDeleteView): 527 queryset = JournalEntry.objects.all() 528 529 def get_return_url(self, request, instance): 530 obj = instance.assigned_object 531 viewname = f'{obj._meta.app_label}:{obj._meta.model_name}_journal' 532 return reverse(viewname, kwargs={'pk': obj.pk}) 533 534 535class JournalEntryBulkEditView(generic.BulkEditView): 536 queryset = JournalEntry.objects.prefetch_related('created_by') 537 filterset = filtersets.JournalEntryFilterSet 538 table = tables.JournalEntryTable 539 form = forms.JournalEntryBulkEditForm 540 541 542class JournalEntryBulkDeleteView(generic.BulkDeleteView): 543 queryset = JournalEntry.objects.prefetch_related('created_by') 544 filterset = filtersets.JournalEntryFilterSet 545 table = tables.JournalEntryTable 546 547 548class ObjectJournalView(View): 549 """ 550 Show all journal entries for an object. 551 552 base_template: The name of the template to extend. If not provided, "<app>/<model>.html" will be used. 553 """ 554 base_template = None 555 556 def get(self, request, model, **kwargs): 557 558 # Handle QuerySet restriction of parent object if needed 559 if hasattr(model.objects, 'restrict'): 560 obj = get_object_or_404(model.objects.restrict(request.user, 'view'), **kwargs) 561 else: 562 obj = get_object_or_404(model, **kwargs) 563 564 # Gather all changes for this object (and its related objects) 565 content_type = ContentType.objects.get_for_model(model) 566 journalentries = JournalEntry.objects.restrict(request.user, 'view').prefetch_related('created_by').filter( 567 assigned_object_type=content_type, 568 assigned_object_id=obj.pk 569 ) 570 journalentry_table = tables.ObjectJournalTable(journalentries) 571 paginate_table(journalentry_table, request) 572 573 if request.user.has_perm('extras.add_journalentry'): 574 form = forms.JournalEntryForm( 575 initial={ 576 'assigned_object_type': ContentType.objects.get_for_model(obj), 577 'assigned_object_id': obj.pk 578 } 579 ) 580 else: 581 form = None 582 583 # Default to using "<app>/<model>.html" as the template, if it exists. Otherwise, 584 # fall back to using base.html. 585 if self.base_template is None: 586 self.base_template = f"{model._meta.app_label}/{model._meta.model_name}.html" 587 588 return render(request, 'extras/object_journal.html', { 589 'object': obj, 590 'form': form, 591 'table': journalentry_table, 592 'base_template': self.base_template, 593 'active_tab': 'journal', 594 }) 595 596 597# 598# Reports 599# 600 601class ReportListView(ContentTypePermissionRequiredMixin, View): 602 """ 603 Retrieve all of the available reports from disk and the recorded JobResult (if any) for each. 604 """ 605 def get_required_permission(self): 606 return 'extras.view_report' 607 608 def get(self, request): 609 610 reports = get_reports() 611 report_content_type = ContentType.objects.get(app_label='extras', model='report') 612 results = { 613 r.name: r 614 for r in JobResult.objects.filter( 615 obj_type=report_content_type, 616 status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES 617 ).defer('data') 618 } 619 620 ret = [] 621 for module, report_list in reports: 622 module_reports = [] 623 for report in report_list: 624 report.result = results.get(report.full_name, None) 625 module_reports.append(report) 626 ret.append((module, module_reports)) 627 628 return render(request, 'extras/report_list.html', { 629 'reports': ret, 630 }) 631 632 633class ReportView(ContentTypePermissionRequiredMixin, View): 634 """ 635 Display a single Report and its associated JobResult (if any). 636 """ 637 def get_required_permission(self): 638 return 'extras.view_report' 639 640 def get(self, request, module, name): 641 642 report = get_report(module, name) 643 if report is None: 644 raise Http404 645 646 report_content_type = ContentType.objects.get(app_label='extras', model='report') 647 report.result = JobResult.objects.filter( 648 obj_type=report_content_type, 649 name=report.full_name, 650 status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES 651 ).first() 652 653 return render(request, 'extras/report.html', { 654 'report': report, 655 'run_form': ConfirmationForm(), 656 }) 657 658 def post(self, request, module, name): 659 660 # Permissions check 661 if not request.user.has_perm('extras.run_report'): 662 return HttpResponseForbidden() 663 664 report = get_report(module, name) 665 if report is None: 666 raise Http404 667 668 # Allow execution only if RQ worker process is running 669 if not Worker.count(get_connection('default')): 670 messages.error(request, "Unable to run report: RQ worker process not running.") 671 return render(request, 'extras/report.html', { 672 'report': report, 673 }) 674 675 # Run the Report. A new JobResult is created. 676 report_content_type = ContentType.objects.get(app_label='extras', model='report') 677 job_result = JobResult.enqueue_job( 678 run_report, 679 report.full_name, 680 report_content_type, 681 request.user 682 ) 683 684 return redirect('extras:report_result', job_result_pk=job_result.pk) 685 686 687class ReportResultView(ContentTypePermissionRequiredMixin, View): 688 """ 689 Display a JobResult pertaining to the execution of a Report. 690 """ 691 def get_required_permission(self): 692 return 'extras.view_report' 693 694 def get(self, request, job_result_pk): 695 report_content_type = ContentType.objects.get(app_label='extras', model='report') 696 jobresult = get_object_or_404(JobResult.objects.all(), pk=job_result_pk, obj_type=report_content_type) 697 698 # Retrieve the Report and attach the JobResult to it 699 module, report_name = jobresult.name.split('.') 700 report = get_report(module, report_name) 701 report.result = jobresult 702 703 return render(request, 'extras/report_result.html', { 704 'report': report, 705 'result': jobresult, 706 }) 707 708 709# 710# Scripts 711# 712 713class GetScriptMixin: 714 def _get_script(self, name, module=None): 715 if module is None: 716 module, name = name.split('.', 1) 717 scripts = get_scripts() 718 try: 719 return scripts[module][name]() 720 except KeyError: 721 raise Http404 722 723 724class ScriptListView(ContentTypePermissionRequiredMixin, View): 725 726 def get_required_permission(self): 727 return 'extras.view_script' 728 729 def get(self, request): 730 731 scripts = get_scripts(use_names=True) 732 script_content_type = ContentType.objects.get(app_label='extras', model='script') 733 results = { 734 r.name: r 735 for r in JobResult.objects.filter( 736 obj_type=script_content_type, 737 status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES 738 ).defer('data') 739 } 740 741 for _scripts in scripts.values(): 742 for script in _scripts.values(): 743 script.result = results.get(script.full_name) 744 745 return render(request, 'extras/script_list.html', { 746 'scripts': scripts, 747 }) 748 749 750class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View): 751 752 def get_required_permission(self): 753 return 'extras.view_script' 754 755 def get(self, request, module, name): 756 script = self._get_script(name, module) 757 form = script.as_form(initial=normalize_querydict(request.GET)) 758 759 # Look for a pending JobResult (use the latest one by creation timestamp) 760 script_content_type = ContentType.objects.get(app_label='extras', model='script') 761 script.result = JobResult.objects.filter( 762 obj_type=script_content_type, 763 name=script.full_name, 764 ).exclude( 765 status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES 766 ).first() 767 768 return render(request, 'extras/script.html', { 769 'module': module, 770 'script': script, 771 'form': form, 772 }) 773 774 def post(self, request, module, name): 775 776 # Permissions check 777 if not request.user.has_perm('extras.run_script'): 778 return HttpResponseForbidden() 779 780 script = self._get_script(name, module) 781 form = script.as_form(request.POST, request.FILES) 782 783 # Allow execution only if RQ worker process is running 784 if not Worker.count(get_connection('default')): 785 messages.error(request, "Unable to run script: RQ worker process not running.") 786 787 elif form.is_valid(): 788 commit = form.cleaned_data.pop('_commit') 789 790 script_content_type = ContentType.objects.get(app_label='extras', model='script') 791 job_result = JobResult.enqueue_job( 792 run_script, 793 script.full_name, 794 script_content_type, 795 request.user, 796 data=form.cleaned_data, 797 request=copy_safe_request(request), 798 commit=commit 799 ) 800 801 return redirect('extras:script_result', job_result_pk=job_result.pk) 802 803 return render(request, 'extras/script.html', { 804 'module': module, 805 'script': script, 806 'form': form, 807 }) 808 809 810class ScriptResultView(ContentTypePermissionRequiredMixin, GetScriptMixin, View): 811 812 def get_required_permission(self): 813 return 'extras.view_script' 814 815 def get(self, request, job_result_pk): 816 result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk) 817 script_content_type = ContentType.objects.get(app_label='extras', model='script') 818 if result.obj_type != script_content_type: 819 raise Http404 820 821 script = self._get_script(result.name) 822 823 return render(request, 'extras/script_result.html', { 824 'script': script, 825 'result': result, 826 'class_name': script.__class__.__name__ 827 }) 828