Closes #11890: Sync/upload reports & scripts (#12059)

* Initial work on #11890

* Consolidate get_scripts() and get_reports() functions

* Introduce proxy models for script & report modules

* Add add/delete views for reports & scripts

* Add deletion links for modules

* Enable resolving scripts/reports from module class

* Remove get_modules() utility function

* Show results in report/script lists

* Misc cleanup

* Fix file uploads

* Support automatic migration for submodules

* Fix module child ordering

* Template cleanup

* Remove ManagedFile views

* Move is_script(), is_report() into extras.utils

* Fix URLs for nested reports & scripts

* Misc cleanup
This commit is contained in:
Jeremy Stretch
2023-03-24 21:00:36 -04:00
committed by GitHub
parent 9c5f4163af
commit f7a2eb8aef
23 changed files with 659 additions and 316 deletions
+13 -15
View File
@@ -16,8 +16,8 @@ from extras import filtersets
from extras.choices import JobResultStatusChoices
from extras.models import *
from extras.models import CustomField
from extras.reports import get_report, get_reports, run_report
from extras.scripts import get_script, get_scripts, run_script
from extras.reports import get_report, run_report
from extras.scripts import get_script, run_script
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.features import SyncedDataMixin
from netbox.api.metadata import ContentTypeMetadata
@@ -27,7 +27,6 @@ from utilities.exceptions import RQWorkerNotRunningException
from utilities.utils import copy_safe_request, count_related
from . import serializers
from .mixins import ConfigTemplateRenderMixin
from .nested_serializers import NestedConfigTemplateSerializer
class ExtrasRootView(APIRootView):
@@ -189,7 +188,6 @@ class ReportViewSet(ViewSet):
"""
Compile all reports and their related results (if any). Result data is deferred in the list view.
"""
report_list = []
report_content_type = ContentType.objects.get(app_label='extras', model='report')
results = {
r.name: r
@@ -199,13 +197,13 @@ class ReportViewSet(ViewSet):
).order_by('name', '-created').distinct('name').defer('data')
}
# Iterate through all available Reports.
for module_name, reports in get_reports().items():
for report in reports.values():
report_list = []
for report_module in ReportModule.objects.restrict(request.user):
report_list.extend([report() for report in report_module.reports.values()])
# Attach the relevant JobResult (if any) to each Report.
report.result = results.get(report.full_name, None)
report_list.append(report)
# Attach JobResult objects to each report (if any)
for report in report_list:
report.result = results.get(report.full_name, None)
serializer = serializers.ReportSerializer(report_list, many=True, context={
'request': request,
@@ -296,15 +294,15 @@ class ScriptViewSet(ViewSet):
).order_by('name', '-created').distinct('name').defer('data')
}
flat_list = []
for script_list in get_scripts().values():
flat_list.extend(script_list.values())
script_list = []
for script_module in ScriptModule.objects.restrict(request.user):
script_list.extend(script_module.scripts.values())
# Attach JobResult objects to each script (if any)
for script in flat_list:
for script in script_list:
script.result = results.get(script.full_name, None)
serializer = serializers.ScriptSerializer(flat_list, many=True, context={'request': request})
serializer = serializers.ScriptSerializer(script_list, many=True, context={'request': request})
return Response(serializer.data)
@@ -5,8 +5,8 @@ from django.core.management.base import BaseCommand
from django.utils import timezone
from extras.choices import JobResultStatusChoices
from extras.models import JobResult
from extras.reports import get_reports, run_report
from extras.models import JobResult, ReportModule
from extras.reports import run_report
class Command(BaseCommand):
@@ -17,13 +17,9 @@ class Command(BaseCommand):
def handle(self, *args, **options):
# Gather all available reports
reports = get_reports()
# Run reports
for module_name, report_list in reports.items():
for report in report_list.values():
if module_name in options['reports'] or report.full_name in options['reports']:
for module in ReportModule.objects.all():
for report in module.reports.values():
if module.name in options['reports'] or report.full_name in options['reports']:
# Run the report and create a new JobResult
self.stdout.write(
@@ -0,0 +1,86 @@
import os
import pkgutil
from django.conf import settings
from django.db import migrations, models
import extras.models.models
def create_files(cls, root_name, root_path):
path_tree = [
path for path, _, _ in os.walk(root_path)
if os.path.basename(path)[0] not in ('_', '.')
]
modules = list(pkgutil.iter_modules(path_tree))
filenames = []
for importer, module_name, is_pkg in modules:
if is_pkg:
continue
try:
module = importer.find_module(module_name).load_module(module_name)
rel_path = os.path.relpath(module.__file__, root_path)
filenames.append(rel_path)
except ImportError:
pass
managed_files = [
cls(file_root=root_name, file_path=filename)
for filename in filenames
]
cls.objects.bulk_create(managed_files)
def replicate_scripts(apps, schema_editor):
ScriptModule = apps.get_model('extras', 'ScriptModule')
create_files(ScriptModule, 'scripts', settings.SCRIPTS_ROOT)
def replicate_reports(apps, schema_editor):
ReportModule = apps.get_model('extras', 'ReportModule')
create_files(ReportModule, 'reports', settings.REPORTS_ROOT)
class Migration(migrations.Migration):
dependencies = [
('core', '0002_managedfile'),
('extras', '0090_objectchange_index_request_id'),
]
operations = [
# Create proxy models
migrations.CreateModel(
name='ReportModule',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=(extras.models.models.PythonModuleMixin, 'core.managedfile', models.Model),
),
migrations.CreateModel(
name='ScriptModule',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=(extras.models.models.PythonModuleMixin, 'core.managedfile', models.Model),
),
# Instantiate ManagedFiles to represent scripts & reports
migrations.RunPython(
code=replicate_scripts,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=replicate_reports,
reverse_code=migrations.RunPython.noop
),
]
+2
View File
@@ -23,8 +23,10 @@ __all__ = (
'JournalEntry',
'ObjectChange',
'Report',
'ReportModule',
'SavedFilter',
'Script',
'ScriptModule',
'StagedChange',
'Tag',
'TaggedItem',
+116 -3
View File
@@ -1,6 +1,11 @@
import inspect
import json
import os
import uuid
from functools import cached_property
from pkgutil import ModuleInfo, get_importer
import django_rq
from django.conf import settings
from django.contrib import admin
from django.contrib.auth.models import User
@@ -16,12 +21,13 @@ from django.utils import timezone
from django.utils.formats import date_format
from django.utils.translation import gettext as _
from rest_framework.utils.encoders import JSONEncoder
import django_rq
from core.choices import ManagedFileRootPathChoices
from core.models import ManagedFile
from extras.choices import *
from extras.constants import *
from extras.conditions import ConditionSet
from extras.utils import FeatureQuery, image_upload
from extras.constants import *
from extras.utils import FeatureQuery, image_upload, is_report, is_script
from netbox.config import get_config
from netbox.constants import RQ_QUEUE_DEFAULT
from netbox.models import ChangeLoggedModel
@@ -41,8 +47,10 @@ __all__ = (
'JobResult',
'JournalEntry',
'Report',
'ReportModule',
'SavedFilter',
'Script',
'ScriptModule',
'Webhook',
)
@@ -814,6 +822,27 @@ class ConfigRevision(models.Model):
# Custom scripts & reports
#
class PythonModuleMixin:
@property
def path(self):
return os.path.splitext(self.file_path)[0]
def get_module_info(self):
path = os.path.dirname(self.full_path)
module_name = os.path.basename(self.path)
return ModuleInfo(
module_finder=get_importer(path),
name=module_name,
ispkg=False
)
def get_module(self):
importer, module_name, _ = self.get_module_info()
module = importer.find_module(module_name).load_module(module_name)
return module
class Script(JobResultsMixin, WebhooksMixin, models.Model):
"""
Dummy model used to generate permissions for custom scripts. Does not exist in the database.
@@ -822,6 +851,48 @@ class Script(JobResultsMixin, WebhooksMixin, models.Model):
managed = False
class ScriptModuleManager(models.Manager.from_queryset(RestrictedQuerySet)):
def get_queryset(self):
return super().get_queryset().filter(file_root=ManagedFileRootPathChoices.SCRIPTS)
class ScriptModule(PythonModuleMixin, ManagedFile):
"""
Proxy model for script module files.
"""
objects = ScriptModuleManager()
class Meta:
proxy = True
def get_absolute_url(self):
return reverse('extras:script_list')
@cached_property
def scripts(self):
def _get_name(cls):
# For child objects in submodules use the full import path w/o the root module as the name
return cls.full_name.split(".", maxsplit=1)[1]
module = self.get_module()
scripts = {}
ordered = getattr(module, 'script_order', [])
for cls in ordered:
scripts[_get_name(cls)] = cls
for name, cls in inspect.getmembers(module, is_script):
if cls not in ordered:
scripts[_get_name(cls)] = cls
return scripts
def save(self, *args, **kwargs):
self.file_root = ManagedFileRootPathChoices.SCRIPTS
return super().save(*args, **kwargs)
#
# Reports
#
@@ -832,3 +903,45 @@ class Report(JobResultsMixin, WebhooksMixin, models.Model):
"""
class Meta:
managed = False
class ReportModuleManager(models.Manager.from_queryset(RestrictedQuerySet)):
def get_queryset(self):
return super().get_queryset().filter(file_root=ManagedFileRootPathChoices.REPORTS)
class ReportModule(PythonModuleMixin, ManagedFile):
"""
Proxy model for report module files.
"""
objects = ReportModuleManager()
class Meta:
proxy = True
def get_absolute_url(self):
return reverse('extras:report_list')
@cached_property
def reports(self):
def _get_name(cls):
# For child objects in submodules use the full import path w/o the root module as the name
return cls.full_name.split(".", maxsplit=1)[1]
module = self.get_module()
reports = {}
ordered = getattr(module, 'report_order', [])
for cls in ordered:
reports[_get_name(cls)] = cls
for name, cls in inspect.getmembers(module, is_report):
if cls not in ordered:
reports[_get_name(cls)] = cls
return reports
def save(self, *args, **kwargs):
self.file_root = ManagedFileRootPathChoices.REPORTS
return super().save(*args, **kwargs)
+20 -64
View File
@@ -1,75 +1,23 @@
import inspect
import logging
import pkgutil
import traceback
from datetime import timedelta
from django.conf import settings
from django.utils import timezone
from django.utils.functional import classproperty
from django_rq import job
from .choices import JobResultStatusChoices, LogLevelChoices
from .models import JobResult
from .models import JobResult, ReportModule
logger = logging.getLogger(__name__)
def is_report(obj):
"""
Returns True if the given object is a Report.
"""
return obj in Report.__subclasses__()
def get_report(module_name, report_name):
"""
Return a specific report from within a module.
"""
reports = get_reports()
module = reports.get(module_name)
if module is None:
return None
report = module.get(report_name)
if report is None:
return None
return report
def get_reports():
"""
Compile a list of all reports available across all modules in the reports path. Returns a list of tuples:
[
(module_name, (report, report, report, ...)),
(module_name, (report, report, report, ...)),
...
]
"""
module_list = {}
# Iterate through all modules within the reports path. These are the user-created files in which reports are
# defined.
for importer, module_name, _ in pkgutil.iter_modules([settings.REPORTS_ROOT]):
module = importer.find_module(module_name).load_module(module_name)
report_order = getattr(module, "report_order", ())
ordered_reports = [cls() for cls in report_order if is_report(cls)]
unordered_reports = [cls() for _, cls in inspect.getmembers(module, is_report) if cls not in report_order]
module_reports = {}
for cls in [*ordered_reports, *unordered_reports]:
# For reports in submodules use the full import path w/o the root module as the name
report_name = cls.full_name.split(".", maxsplit=1)[1]
module_reports[report_name] = cls
if module_reports:
module_list[module_name] = module_reports
return module_list
module = ReportModule.objects.get(file_path=f'{module_name}.py')
return module.reports.get(report_name)
@job('default')
@@ -79,7 +27,7 @@ def run_report(job_result, *args, **kwargs):
method for queueing into the background processor.
"""
module_name, report_name = job_result.name.split('.', 1)
report = get_report(module_name, report_name)
report = get_report(module_name, report_name)()
try:
job_result.start()
@@ -136,7 +84,7 @@ class Report(object):
self.active_test = None
self.failed = False
self.logger = logging.getLogger(f"netbox.reports.{self.full_name}")
self.logger = logging.getLogger(f"netbox.reports.{self.__module__}.{self.__class__.__name__}")
# Compile test methods and initialize results skeleton
test_methods = []
@@ -154,13 +102,17 @@ class Report(object):
raise Exception("A report must contain at least one test method.")
self.test_methods = test_methods
@property
@classproperty
def module(self):
return self.__module__
@property
@classproperty
def class_name(self):
return self.__class__.__name__
return self.__name__
@classproperty
def full_name(self):
return f'{self.module}.{self.class_name}'
@property
def name(self):
@@ -169,9 +121,9 @@ class Report(object):
"""
return self.class_name
@property
def full_name(self):
return f'{self.module}.{self.class_name}'
#
# Logging methods
#
def _log(self, obj, message, level=LogLevelChoices.LOG_DEFAULT):
"""
@@ -228,6 +180,10 @@ class Report(object):
self.logger.info(f"Failure | {obj}: {message}")
self.failed = True
#
# Run methods
#
def run(self, job_result):
"""
Run the report and save its results. Each test method will be executed in order.
+19 -76
View File
@@ -2,9 +2,6 @@ import inspect
import json
import logging
import os
import pkgutil
import sys
import threading
import traceback
from datetime import timedelta
@@ -17,7 +14,7 @@ from django.utils.functional import classproperty
from extras.api.serializers import ScriptOutputSerializer
from extras.choices import JobResultStatusChoices, LogLevelChoices
from extras.models import JobResult
from extras.models import JobResult, ScriptModule
from extras.signals import clear_webhooks
from ipam.formfields import IPAddressFormField, IPNetworkFormField
from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
@@ -43,8 +40,6 @@ __all__ = [
'TextVar',
]
lock = threading.Lock()
#
# Script variables
@@ -272,7 +267,7 @@ class BaseScript:
def __init__(self):
# Initiate the log
self.logger = logging.getLogger(f"netbox.scripts.{self.module()}.{self.__class__.__name__}")
self.logger = logging.getLogger(f"netbox.scripts.{self.__module__}.{self.__class__.__name__}")
self.log = []
# Declare the placeholder for the current request
@@ -285,22 +280,26 @@ class BaseScript:
def __str__(self):
return self.name
@classproperty
def module(self):
return self.__module__
@classproperty
def class_name(self):
return self.__name__
@classproperty
def full_name(self):
return f'{self.module}.{self.class_name}'
@classproperty
def name(self):
return getattr(self.Meta, 'name', self.__name__)
@classproperty
def full_name(self):
return '.'.join([self.__module__, self.__name__])
@classproperty
def description(self):
return getattr(self.Meta, 'description', '')
@classmethod
def module(cls):
return cls.__module__
@classmethod
def root_module(cls):
return cls.__module__.split(".")[0]
@@ -427,15 +426,6 @@ class Script(BaseScript):
# Functions
#
def is_script(obj):
"""
Returns True if the object is a Script.
"""
try:
return issubclass(obj, Script) and obj != Script
except TypeError:
return False
def is_variable(obj):
"""
@@ -452,10 +442,10 @@ def run_script(data, request, commit=True, *args, **kwargs):
job_result = kwargs.pop('job_result')
job_result.start()
module, script_name = job_result.name.split('.', 1)
script = get_script(module, script_name)()
module_name, script_name = job_result.name.split('.', 1)
script = get_script(module_name, script_name)()
logger = logging.getLogger(f"netbox.scripts.{module}.{script_name}")
logger = logging.getLogger(f"netbox.scripts.{module_name}.{script_name}")
logger.info(f"Running script (commit={commit})")
# Add files to form data
@@ -522,56 +512,9 @@ def run_script(data, request, commit=True, *args, **kwargs):
)
def get_scripts(use_names=False):
"""
Return a dict of dicts mapping all scripts to their modules. Set use_names to True to use each module's human-
defined name in place of the actual module name.
"""
scripts = {}
# Get all modules within the scripts path. These are the user-created files in which scripts are
# defined.
modules = list(pkgutil.iter_modules([settings.SCRIPTS_ROOT]))
modules_bases = set([name.split(".")[0] for _, name, _ in modules])
# Deleting from sys.modules needs to done behind a lock to prevent race conditions where a module is
# removed from sys.modules while another thread is importing
with lock:
for module_name in list(sys.modules.keys()):
# Everything sharing a base module path with a module in the script folder is removed.
# We also remove all modules with a base module called "scripts". This allows modifying imported
# non-script modules without having to reload the RQ worker.
module_base = module_name.split(".")[0]
if module_base == "scripts" or module_base in modules_bases:
del sys.modules[module_name]
for importer, module_name, _ in modules:
module = importer.find_module(module_name).load_module(module_name)
if use_names and hasattr(module, 'name'):
module_name = module.name
module_scripts = {}
script_order = getattr(module, "script_order", ())
ordered_scripts = [cls for cls in script_order if is_script(cls)]
unordered_scripts = [cls for _, cls in inspect.getmembers(module, is_script) if cls not in script_order]
for cls in [*ordered_scripts, *unordered_scripts]:
# For scripts in submodules use the full import path w/o the root module as the name
script_name = cls.full_name.split(".", maxsplit=1)[1]
module_scripts[script_name] = cls
if module_scripts:
scripts[module_name] = module_scripts
return scripts
def get_script(module_name, script_name):
"""
Retrieve a script class by module and name. Returns None if the script does not exist.
"""
scripts = get_scripts()
module = scripts.get(module_name)
if module:
return module.get(script_name)
module = ScriptModule.objects.get(file_path=f'{module_name}.py')
return module.scripts.get(script_name)
+10 -6
View File
@@ -94,19 +94,23 @@ urlpatterns = [
# Reports
path('reports/', views.ReportListView.as_view(), name='report_list'),
path('reports/add/', views.ReportModuleCreateView.as_view(), name='reportmodule_add'),
path('reports/results/<int:job_result_pk>/', views.ReportResultView.as_view(), name='report_result'),
re_path(r'^reports/(?P<module>.([^.]+)).(?P<name>.(.+))/', views.ReportView.as_view(), name='report'),
path('reports/<int:pk>/', include(get_model_urls('extras', 'reportmodule'))),
path('reports/<path:module>.<str:name>/', views.ReportView.as_view(), name='report'),
# Scripts
path('scripts/', views.ScriptListView.as_view(), name='script_list'),
path('scripts/add/', views.ScriptModuleCreateView.as_view(), name='scriptmodule_add'),
path('scripts/results/<int:job_result_pk>/', views.ScriptResultView.as_view(), name='script_result'),
path('scripts/<int:pk>/', include(get_model_urls('extras', 'scriptmodule'))),
path('scripts/<path:module>.<str:name>/', views.ScriptView.as_view(), name='script'),
# Job results
path('job-results/', views.JobResultListView.as_view(), name='jobresult_list'),
path('job-results/delete/', views.JobResultBulkDeleteView.as_view(), name='jobresult_bulk_delete'),
path('job-results/<int:pk>/delete/', views.JobResultDeleteView.as_view(), name='jobresult_delete'),
# Scripts
path('scripts/', views.ScriptListView.as_view(), name='script_list'),
path('scripts/results/<int:job_result_pk>/', views.ScriptResultView.as_view(), name='script_result'),
re_path(r'^scripts/(?P<module>.([^.]+)).(?P<name>.(.+))/', views.ScriptView.as_view(), name='script'),
# Markdown
path('render/markdown/', views.RenderMarkdownView.as_view(), name="render_markdown")
]
+22
View File
@@ -66,3 +66,25 @@ def register_features(model, features):
raise KeyError(
f"{feature} is not a valid model feature! Valid keys are: {registry['model_features'].keys()}"
)
def is_script(obj):
"""
Returns True if the object is a Script.
"""
from .scripts import Script
try:
return issubclass(obj, Script) and obj != Script
except TypeError:
return False
def is_report(obj):
"""
Returns True if the given object is a Report.
"""
from .reports import Report
try:
return issubclass(obj, Report) and obj != Report
except TypeError:
return False
+60 -48
View File
@@ -7,6 +7,8 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.views.generic import View
from core.choices import ManagedFileRootPathChoices
from core.forms import ManagedFileForm
from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
from extras.dashboard.utils import get_widget_class
from netbox.views import generic
@@ -20,8 +22,8 @@ from . import filtersets, forms, tables
from .choices import JobResultStatusChoices
from .forms.reports import ReportForm
from .models import *
from .reports import get_report, get_reports, run_report
from .scripts import get_scripts, run_script
from .reports import get_report, run_report
from .scripts import run_script
#
@@ -790,18 +792,34 @@ class DashboardWidgetDeleteView(LoginRequiredMixin, View):
# Reports
#
@register_model_view(ReportModule, 'edit')
class ReportModuleCreateView(generic.ObjectEditView):
queryset = ReportModule.objects.all()
form = ManagedFileForm
def alter_object(self, obj, *args, **kwargs):
obj.file_root = ManagedFileRootPathChoices.REPORTS
return obj
@register_model_view(ReportModule, 'delete')
class ReportModuleDeleteView(generic.ObjectDeleteView):
queryset = ReportModule.objects.all()
default_return_url = 'extras:report_list'
class ReportListView(ContentTypePermissionRequiredMixin, View):
"""
Retrieve all of the available reports from disk and the recorded JobResult (if any) for each.
Retrieve all the available reports from disk and the recorded JobResult (if any) for each.
"""
def get_required_permission(self):
return 'extras.view_report'
def get(self, request):
report_modules = ReportModule.objects.restrict(request.user)
reports = get_reports()
report_content_type = ContentType.objects.get(app_label='extras', model='report')
results = {
job_results = {
r.name: r
for r in JobResult.objects.filter(
obj_type=report_content_type,
@@ -809,17 +827,10 @@ class ReportListView(ContentTypePermissionRequiredMixin, View):
).order_by('name', '-created').distinct('name').defer('data')
}
ret = []
for module, report_list in reports.items():
module_reports = []
for report in report_list.values():
report.result = results.get(report.full_name, None)
module_reports.append(report)
ret.append((module, module_reports))
return render(request, 'extras/report_list.html', {
'reports': ret,
'model': ReportModule,
'report_modules': report_modules,
'job_results': job_results,
})
@@ -831,10 +842,8 @@ class ReportView(ContentTypePermissionRequiredMixin, View):
return 'extras.view_report'
def get(self, request, module, name):
report = get_report(module, name)
if report is None:
raise Http404
module = get_object_or_404(ReportModule.objects.restrict(request.user), file_path=f'{module}.py')
report = module.reports[name]()
report_content_type = ContentType.objects.get(app_label='extras', model='report')
report.result = JobResult.objects.filter(
@@ -844,20 +853,17 @@ class ReportView(ContentTypePermissionRequiredMixin, View):
).first()
return render(request, 'extras/report.html', {
'module': module,
'report': report,
'form': ReportForm(),
})
def post(self, request, module, name):
# Permissions check
if not request.user.has_perm('extras.run_report'):
return HttpResponseForbidden()
report = get_report(module, name)
if report is None:
raise Http404
module = get_object_or_404(ReportModule.objects.restrict(request.user), file_path=f'{module}.py')
report = module.reports[name]()
form = ReportForm(request.POST)
if form.is_valid():
@@ -883,6 +889,7 @@ class ReportView(ContentTypePermissionRequiredMixin, View):
return redirect('extras:report_result', job_result_pk=job_result.pk)
return render(request, 'extras/report.html', {
'module': module,
'report': report,
'form': form,
})
@@ -924,15 +931,20 @@ class ReportResultView(ContentTypePermissionRequiredMixin, View):
# Scripts
#
class GetScriptMixin:
def _get_script(self, name, module=None):
if module is None:
module, name = name.split('.', 1)
scripts = get_scripts()
try:
return scripts[module][name]()
except KeyError:
raise Http404
@register_model_view(ScriptModule, 'edit')
class ScriptModuleCreateView(generic.ObjectEditView):
queryset = ScriptModule.objects.all()
form = ManagedFileForm
def alter_object(self, obj, *args, **kwargs):
obj.file_root = ManagedFileRootPathChoices.SCRIPTS
return obj
@register_model_view(ScriptModule, 'delete')
class ScriptModuleDeleteView(generic.ObjectDeleteView):
queryset = ScriptModule.objects.all()
default_return_url = 'extras:script_list'
class ScriptListView(ContentTypePermissionRequiredMixin, View):
@@ -941,10 +953,10 @@ class ScriptListView(ContentTypePermissionRequiredMixin, View):
return 'extras.view_script'
def get(self, request):
script_modules = ScriptModule.objects.restrict(request.user)
scripts = get_scripts(use_names=True)
script_content_type = ContentType.objects.get(app_label='extras', model='script')
results = {
job_results = {
r.name: r
for r in JobResult.objects.filter(
obj_type=script_content_type,
@@ -952,22 +964,21 @@ class ScriptListView(ContentTypePermissionRequiredMixin, View):
).order_by('name', '-created').distinct('name').defer('data')
}
for _scripts in scripts.values():
for script in _scripts.values():
script.result = results.get(script.full_name)
return render(request, 'extras/script_list.html', {
'scripts': scripts,
'model': ScriptModule,
'script_modules': script_modules,
'job_results': job_results,
})
class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
class ScriptView(ContentTypePermissionRequiredMixin, View):
def get_required_permission(self):
return 'extras.view_script'
def get(self, request, module, name):
script = self._get_script(name, module)
module = get_object_or_404(ScriptModule.objects.restrict(request.user), file_path=f'{module}.py')
script = module.scripts[name]()
form = script.as_form(initial=normalize_querydict(request.GET))
# Look for a pending JobResult (use the latest one by creation timestamp)
@@ -985,12 +996,11 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
})
def post(self, request, module, name):
# Permissions check
if not request.user.has_perm('extras.run_script'):
return HttpResponseForbidden()
script = self._get_script(name, module)
module = get_object_or_404(ScriptModule.objects.restrict(request.user), file_path=f'{module}.py')
script = module.scripts[name]()
form = script.as_form(request.POST, request.FILES)
# Allow execution only if RQ worker process is running
@@ -1020,7 +1030,7 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
})
class ScriptResultView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
class ScriptResultView(ContentTypePermissionRequiredMixin, View):
def get_required_permission(self):
return 'extras.view_script'
@@ -1031,7 +1041,9 @@ class ScriptResultView(ContentTypePermissionRequiredMixin, GetScriptMixin, View)
if result.obj_type != script_content_type:
raise Http404
script = self._get_script(result.name)
module_name, script_name = result.name.split('.', 1)
module = get_object_or_404(ScriptModule.objects.restrict(request.user), file_path=f'{module_name}.py')
script = module.scripts[script_name]()
# If this is an HTMX request, return only the result HTML
if is_htmx(request):