From e3cd40e95f45864f320a095e774cef5307a5589a Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 28 Mar 2023 11:38:37 -0400 Subject: [PATCH] Add tabbed views for report & script jobs --- netbox/core/models/jobs.py | 30 +++--- netbox/core/tables/jobs.py | 13 ++- netbox/extras/urls.py | 3 + netbox/extras/views.py | 81 ++++++++++++++ netbox/templates/extras/report.html | 31 +----- netbox/templates/extras/report/base.html | 35 ++++++ netbox/templates/extras/report/jobs.html | 15 +++ netbox/templates/extras/script.html | 118 +++++++-------------- netbox/templates/extras/script/base.html | 37 +++++++ netbox/templates/extras/script/jobs.html | 15 +++ netbox/templates/extras/script/source.html | 6 ++ 11 files changed, 258 insertions(+), 126 deletions(-) create mode 100644 netbox/templates/extras/report/base.html create mode 100644 netbox/templates/extras/report/jobs.html create mode 100644 netbox/templates/extras/script/base.html create mode 100644 netbox/templates/extras/script/jobs.html create mode 100644 netbox/templates/extras/script/source.html diff --git a/netbox/core/models/jobs.py b/netbox/core/models/jobs.py index 1d5e693ee..928138370 100644 --- a/netbox/core/models/jobs.py +++ b/netbox/core/models/jobs.py @@ -7,7 +7,6 @@ from django.contrib.contenttypes.models import ContentType from django.core.validators import MinValueValidator from django.db import models from django.urls import reverse -from django.urls.exceptions import NoReverseMatch from django.utils import timezone from django.utils.translation import gettext as _ @@ -96,21 +95,12 @@ class Job(models.Model): def __str__(self): return str(self.job_id) - def delete(self, *args, **kwargs): - super().delete(*args, **kwargs) - - rq_queue_name = get_config().QUEUE_MAPPINGS.get(self.object_type.model, RQ_QUEUE_DEFAULT) - queue = django_rq.get_queue(rq_queue_name) - job = queue.fetch_job(str(self.job_id)) - - if job: - job.cancel() - def get_absolute_url(self): - try: - return reverse(f'extras:{self.object_type.model}_result', args=[self.pk]) - except NoReverseMatch: - return None + # TODO: Employ dynamic registration + if self.object_type.model == 'reportmodule': + return reverse(f'extras:report_result', kwargs={'job_pk': self.pk}) + if self.object_type.model == 'scriptmodule': + return reverse(f'extras:script_result', kwargs={'job_pk': self.pk}) def get_status_color(self): return JobStatusChoices.colors.get(self.status) @@ -130,6 +120,16 @@ class Job(models.Model): return f"{int(minutes)} minutes, {seconds:.2f} seconds" + def delete(self, *args, **kwargs): + super().delete(*args, **kwargs) + + rq_queue_name = get_config().QUEUE_MAPPINGS.get(self.object_type.model, RQ_QUEUE_DEFAULT) + queue = django_rq.get_queue(rq_queue_name) + job = queue.fetch_job(str(self.job_id)) + + if job: + job.cancel() + def start(self): """ Record the job's start time and update its status to "running." diff --git a/netbox/core/tables/jobs.py b/netbox/core/tables/jobs.py index 662119dbc..540d252b2 100644 --- a/netbox/core/tables/jobs.py +++ b/netbox/core/tables/jobs.py @@ -6,12 +6,18 @@ from ..models import Job class JobTable(NetBoxTable): + id = tables.Column( + linkify=True + ) name = tables.Column( linkify=True ) object_type = columns.ContentTypeColumn( verbose_name=_('Type') ) + object = tables.Column( + linkify=True + ) status = columns.ChoiceFieldColumn() created = columns.DateTimeColumn() scheduled = columns.DateTimeColumn() @@ -25,10 +31,9 @@ class JobTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = Job fields = ( - 'pk', 'id', 'object_type', 'name', 'status', 'created', 'scheduled', 'interval', 'started', 'completed', - 'user', 'job_id', + 'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'scheduled', 'interval', 'started', + 'completed', 'user', 'job_id', ) default_columns = ( - 'pk', 'id', 'object_type', 'name', 'status', 'created', 'scheduled', 'interval', 'started', 'completed', - 'user', + 'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user', ) diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index 2bc6e709c..400596c40 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -98,6 +98,7 @@ urlpatterns = [ path('reports/results//', views.ReportResultView.as_view(), name='report_result'), path('reports//', include(get_model_urls('extras', 'reportmodule'))), path('reports/./', views.ReportView.as_view(), name='report'), + path('reports/./jobs/', views.ReportJobsView.as_view(), name='report_jobs'), # Scripts path('scripts/', views.ScriptListView.as_view(), name='script_list'), @@ -105,6 +106,8 @@ urlpatterns = [ path('scripts/results//', views.ScriptResultView.as_view(), name='script_result'), path('scripts//', include(get_model_urls('extras', 'scriptmodule'))), path('scripts/./', views.ScriptView.as_view(), name='script'), + path('scripts/./source/', views.ScriptSourceView.as_view(), name='script_source'), + path('scripts/./jobs/', views.ScriptJobsView.as_view(), name='script_jobs'), # Markdown path('render/markdown/', views.RenderMarkdownView.as_view(), name="render_markdown") diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 7aa908803..080c041d4 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -10,6 +10,7 @@ from django.views.generic import View from core.choices import JobStatusChoices, ManagedFileRootPathChoices from core.forms import ManagedFileForm from core.models import Job +from core.tables import JobTable from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm from extras.dashboard.utils import get_widget_class from netbox.views import generic @@ -896,6 +897,38 @@ class ReportView(ContentTypePermissionRequiredMixin, View): }) +class ReportJobsView(ContentTypePermissionRequiredMixin, View): + + def get_required_permission(self): + return 'extras.view_report' + + def get(self, request, module, name): + module = get_object_or_404(ReportModule.objects.restrict(request.user), file_path=f'{module}.py') + report = module.reports[name]() + + object_type = ContentType.objects.get(app_label='extras', model='reportmodule') + jobs = Job.objects.filter( + object_type=object_type, + object_id=module.pk, + name=report.name, + status__in=JobStatusChoices.TERMINAL_STATE_CHOICES + ) + + jobs_table = JobTable( + data=jobs, + orderable=False, + user=request.user + ) + jobs_table.configure(request) + + return render(request, 'extras/report/jobs.html', { + 'module': module, + 'report': report, + 'table': jobs_table, + 'tab': 'jobs', + }) + + class ReportResultView(ContentTypePermissionRequiredMixin, View): """ Display a Job pertaining to the execution of a Report. @@ -1031,6 +1064,54 @@ class ScriptView(ContentTypePermissionRequiredMixin, View): }) +class ScriptSourceView(ContentTypePermissionRequiredMixin, View): + + def get_required_permission(self): + return 'extras.view_script' + + def get(self, request, module, name): + module = get_object_or_404(ScriptModule.objects.restrict(request.user), file_path=f'{module}.py') + script = module.scripts[name]() + + return render(request, 'extras/script/source.html', { + 'module': module, + 'script': script, + 'tab': 'source', + }) + + +class ScriptJobsView(ContentTypePermissionRequiredMixin, View): + + def get_required_permission(self): + return 'extras.view_script' + + def get(self, request, module, name): + module = get_object_or_404(ScriptModule.objects.restrict(request.user), file_path=f'{module}.py') + script = module.scripts[name]() + + object_type = ContentType.objects.get(app_label='extras', model='scriptmodule') + jobs = Job.objects.filter( + object_type=object_type, + object_id=module.pk, + name=script.class_name, + status__in=JobStatusChoices.TERMINAL_STATE_CHOICES + ) + + jobs_table = JobTable( + data=jobs, + orderable=False, + user=request.user + ) + jobs_table.configure(request) + + return render(request, 'extras/script/jobs.html', { + 'module': module, + 'script': script, + 'table': jobs_table, + 'tab': 'jobs', + }) + + class ScriptResultView(ContentTypePermissionRequiredMixin, View): def get_required_permission(self): diff --git a/netbox/templates/extras/report.html b/netbox/templates/extras/report.html index 6cc52d914..fa6b9660f 100644 --- a/netbox/templates/extras/report.html +++ b/netbox/templates/extras/report.html @@ -1,36 +1,7 @@ -{% extends 'generic/object.html' %} +{% extends 'extras/report/base.html' %} {% load helpers %} {% load form_helpers %} -{% block title %}{{ report.name }}{% endblock %} - -{% block object_identifier %} - {{ report.full_name }} -{% endblock %} - -{% block breadcrumbs %} - - -{% endblock breadcrumbs %} - -{% block subtitle %} - {% if report.description %} -
-
{{ report.description|markdown }}
-
- {% endif %} -{% endblock subtitle %} - -{% block controls %}{% endblock %} - -{% block tabs %} - -{% endblock tabs %} - {% block content %}
{% if perms.extras.run_report %} diff --git a/netbox/templates/extras/report/base.html b/netbox/templates/extras/report/base.html new file mode 100644 index 000000000..218384f55 --- /dev/null +++ b/netbox/templates/extras/report/base.html @@ -0,0 +1,35 @@ +{% extends 'generic/object.html' %} +{% load helpers %} +{% load form_helpers %} + +{% block title %}{{ report.name }}{% endblock %} + +{% block object_identifier %} + {{ report.full_name }} +{% endblock %} + +{% block breadcrumbs %} + + +{% endblock breadcrumbs %} + +{% block subtitle %} + {% if report.description %} +
+
{{ report.description|markdown }}
+
+ {% endif %} +{% endblock subtitle %} + +{% block controls %}{% endblock %} + +{% block tabs %} + +{% endblock tabs %} diff --git a/netbox/templates/extras/report/jobs.html b/netbox/templates/extras/report/jobs.html new file mode 100644 index 000000000..a42e290cc --- /dev/null +++ b/netbox/templates/extras/report/jobs.html @@ -0,0 +1,15 @@ +{% extends 'extras/report/base.html' %} +{% load render_table from django_tables2 %} + +{% block content %} +
+
+
+
+ {% render_table table 'inc/table.html' %} + {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} +
+
+
+
+{% endblock %} diff --git a/netbox/templates/extras/script.html b/netbox/templates/extras/script.html index cc12d5a7c..0f74d6091 100644 --- a/netbox/templates/extras/script.html +++ b/netbox/templates/extras/script.html @@ -1,91 +1,55 @@ -{% extends 'generic/object.html' %} +{% extends 'extras/script/base.html' %} {% load helpers %} {% load form_helpers %} {% load log_levels %} -{% block title %}{{ script }}{% endblock %} - -{% block object_identifier %} - {{ script.full_name }} -{% endblock object_identifier %} - -{% block breadcrumbs %} - - -{% endblock breadcrumbs %} - -{% block subtitle %} -
-
{{ script.Meta.description|markdown }}
-
-{% endblock subtitle %} - -{% block controls %}{% endblock %} - -{% block tabs %} - -{% endblock tabs %} - {% block content %} -
-
-
- {% if not perms.extras.run_script %} -
- - You do not have permission to run scripts. -
- {% endif %} -
- {% csrf_token %} -
- {% if form.requires_input %} - {% if script.Meta.fieldsets %} - {# Render grouped fields according to declared fieldsets #} - {% for group, fields in script.Meta.fieldsets %} -
-
-
{{ group }}
-
- {% for name in fields %} - {% with field=form|getfield:name %} - {% render_field field %} - {% endwith %} - {% endfor %} +
+
+ {% if not perms.extras.run_script %} +
+ + You do not have permission to run scripts. +
+ {% endif %} + + {% csrf_token %} +
+ {% if form.requires_input %} + {% if script.Meta.fieldsets %} + {# Render grouped fields according to declared fieldsets #} + {% for group, fields in script.Meta.fieldsets %} +
+
+
{{ group }}
- {% endfor %} - {% else %} - {# Render all fields as a single group #} -
-
Script Data
+ {% for name in fields %} + {% with field=form|getfield:name %} + {% render_field field %} + {% endwith %} + {% endfor %}
- {% render_form form %} - {% endif %} + {% endfor %} {% else %} -
- - This script does not require any input to run. + {# Render all fields as a single group #} +
+
Script Data
{% render_form form %} {% endif %} -
-
- Cancel - -
- -
+ {% else %} +
+ + This script does not require any input to run. +
+ {% render_form form %} + {% endif %} +
+
+ Cancel + +
+
-
- {{ script.filename }} -
{{ script.source }}
-
{% endblock content %} diff --git a/netbox/templates/extras/script/base.html b/netbox/templates/extras/script/base.html new file mode 100644 index 000000000..b788bc270 --- /dev/null +++ b/netbox/templates/extras/script/base.html @@ -0,0 +1,37 @@ +{% extends 'generic/object.html' %} +{% load helpers %} +{% load form_helpers %} +{% load log_levels %} + +{% block title %}{{ script }}{% endblock %} + +{% block object_identifier %} + {{ script.full_name }} +{% endblock object_identifier %} + +{% block breadcrumbs %} + + +{% endblock breadcrumbs %} + +{% block subtitle %} +
+
{{ script.Meta.description|markdown }}
+
+{% endblock subtitle %} + +{% block controls %}{% endblock %} + +{% block tabs %} + +{% endblock tabs %} diff --git a/netbox/templates/extras/script/jobs.html b/netbox/templates/extras/script/jobs.html new file mode 100644 index 000000000..c550e5de7 --- /dev/null +++ b/netbox/templates/extras/script/jobs.html @@ -0,0 +1,15 @@ +{% extends 'extras/script/base.html' %} +{% load render_table from django_tables2 %} + +{% block content %} +
+
+
+
+ {% render_table table 'inc/table.html' %} + {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} +
+
+
+
+{% endblock %} diff --git a/netbox/templates/extras/script/source.html b/netbox/templates/extras/script/source.html new file mode 100644 index 000000000..733f6a1f3 --- /dev/null +++ b/netbox/templates/extras/script/source.html @@ -0,0 +1,6 @@ +{% extends 'extras/script/base.html' %} + +{% block content %} + {{ script.filename }} +
{{ script.source }}
+{% endblock %}