Add tabbed views for report & script jobs

This commit is contained in:
jeremystretch 2023-03-28 11:38:37 -04:00
parent 431b21b5fe
commit e3cd40e95f
11 changed files with 258 additions and 126 deletions

View File

@ -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."

View File

@ -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',
)

View File

@ -98,6 +98,7 @@ urlpatterns = [
path('reports/results/<int:job_pk>/', views.ReportResultView.as_view(), name='report_result'),
path('reports/<int:pk>/', include(get_model_urls('extras', 'reportmodule'))),
path('reports/<path:module>.<str:name>/', views.ReportView.as_view(), name='report'),
path('reports/<path:module>.<str:name>/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/<int:job_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'),
path('scripts/<path:module>.<str:name>/source/', views.ScriptSourceView.as_view(), name='script_source'),
path('scripts/<path:module>.<str:name>/jobs/', views.ScriptJobsView.as_view(), name='script_jobs'),
# Markdown
path('render/markdown/', views.RenderMarkdownView.as_view(), name="render_markdown")

View File

@ -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):

View File

@ -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 %}
<li class="breadcrumb-item"><a href="{% url 'extras:report_list' %}">Reports</a></li>
<li class="breadcrumb-item"><a href="{% url 'extras:report_list' %}#module{{ module.pk }}">{{ report.module|bettertitle }}</a></li>
{% endblock breadcrumbs %}
{% block subtitle %}
{% if report.description %}
<div class="object-subtitle">
<div class="text-muted">{{ report.description|markdown }}</div>
</div>
{% endif %}
{% endblock subtitle %}
{% block controls %}{% endblock %}
{% block tabs %}
<ul class="nav nav-tabs px-3">
<li class="nav-item" role="presentation">
<a href="#report" role="tab" data-bs-toggle="tab" class="nav-link active">Report</a>
</li>
</ul>
{% endblock tabs %}
{% block content %}
<div role="tabpanel" class="tab-pane active" id="report">
{% if perms.extras.run_report %}

View File

@ -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 %}
<li class="breadcrumb-item"><a href="{% url 'extras:report_list' %}">Reports</a></li>
<li class="breadcrumb-item"><a href="{% url 'extras:report_list' %}#module{{ module.pk }}">{{ report.module|bettertitle }}</a></li>
{% endblock breadcrumbs %}
{% block subtitle %}
{% if report.description %}
<div class="object-subtitle">
<div class="text-muted">{{ report.description|markdown }}</div>
</div>
{% endif %}
{% endblock subtitle %}
{% block controls %}{% endblock %}
{% block tabs %}
<ul class="nav nav-tabs px-3">
<li class="nav-item" role="presentation">
<a class="nav-link{% if not tab %} active{% endif %}" href="{% url 'extras:report' module=report.module name=report.class_name %}">Report</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link{% if tab == 'jobs' %} active{% endif %}" href="{% url 'extras:report_jobs' module=report.module name=report.class_name %}">Jobs</a>
</li>
</ul>
{% endblock tabs %}

View File

@ -0,0 +1,15 @@
{% extends 'extras/report/base.html' %}
{% load render_table from django_tables2 %}
{% block content %}
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<div class="card-body table-responsive">
{% render_table table 'inc/table.html' %}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -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 %}
<li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}">Scripts</a></li>
<li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}#module{{ module.pk }}">{{ module|bettertitle }}</a></li>
{% endblock breadcrumbs %}
{% block subtitle %}
<div class="object-subtitle">
<div class="text-muted">{{ script.Meta.description|markdown }}</div>
</div>
{% endblock subtitle %}
{% block controls %}{% endblock %}
{% block tabs %}
<ul class="nav nav-tabs px-3">
<li class="nav-item" role="presentation">
<a href="#run" role="tab" data-bs-toggle="tab" class="nav-link active">Run</a>
</li>
<li class="nav-item" role="presentation">
<a href="#source" role="tab" data-bs-toggle="tab" class="nav-link">Source</a>
</li>
</ul>
{% endblock tabs %}
{% block content %}
<div role="tabpanel" class="tab-pane active" id="run">
<div class="row">
<div class="col">
{% if not perms.extras.run_script %}
<div class="alert alert-warning">
<i class="mdi mdi-alert"></i>
You do not have permission to run scripts.
</div>
{% endif %}
<form action="" method="post" enctype="multipart/form-data" class="form form-object-edit">
{% csrf_token %}
<div class="field-group my-4">
{% if form.requires_input %}
{% if script.Meta.fieldsets %}
{# Render grouped fields according to declared fieldsets #}
{% for group, fields in script.Meta.fieldsets %}
<div class="field-group mb-5">
<div class="row mb-2">
<h5 class="offset-sm-3">{{ group }}</h5>
</div>
{% for name in fields %}
{% with field=form|getfield:name %}
{% render_field field %}
{% endwith %}
{% endfor %}
<div class="row">
<div class="col">
{% if not perms.extras.run_script %}
<div class="alert alert-warning">
<i class="mdi mdi-alert"></i>
You do not have permission to run scripts.
</div>
{% endif %}
<form action="" method="post" enctype="multipart/form-data" class="form form-object-edit">
{% csrf_token %}
<div class="field-group my-4">
{% if form.requires_input %}
{% if script.Meta.fieldsets %}
{# Render grouped fields according to declared fieldsets #}
{% for group, fields in script.Meta.fieldsets %}
<div class="field-group mb-5">
<div class="row mb-2">
<h5 class="offset-sm-3">{{ group }}</h5>
</div>
{% endfor %}
{% else %}
{# Render all fields as a single group #}
<div class="row mb-2">
<h5 class="offset-sm-3">Script Data</h5>
{% for name in fields %}
{% with field=form|getfield:name %}
{% render_field field %}
{% endwith %}
{% endfor %}
</div>
{% render_form form %}
{% endif %}
{% endfor %}
{% else %}
<div class="alert alert-info">
<i class="mdi mdi-information"></i>
This script does not require any input to run.
{# Render all fields as a single group #}
<div class="row mb-2">
<h5 class="offset-sm-3">Script Data</h5>
</div>
{% render_form form %}
{% endif %}
</div>
<div class="float-end">
<a href="{% url 'extras:script_list' %}" class="btn btn-outline-danger">Cancel</a>
<button type="submit" name="_run" class="btn btn-primary"{% if not perms.extras.run_script %} disabled="disabled"{% endif %}><i class="mdi mdi-play"></i> Run Script</button>
</div>
</form>
</div>
{% else %}
<div class="alert alert-info">
<i class="mdi mdi-information"></i>
This script does not require any input to run.
</div>
{% render_form form %}
{% endif %}
</div>
<div class="float-end">
<a href="{% url 'extras:script_list' %}" class="btn btn-outline-danger">Cancel</a>
<button type="submit" name="_run" class="btn btn-primary"{% if not perms.extras.run_script %} disabled="disabled"{% endif %}><i class="mdi mdi-play"></i> Run Script</button>
</div>
</form>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="source">
<code class="h6 my-3 d-block">{{ script.filename }}</code>
<pre class="block">{{ script.source }}</pre>
</div>
{% endblock content %}

View File

@ -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 %}
<li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}">Scripts</a></li>
<li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}#module{{ module.pk }}">{{ module|bettertitle }}</a></li>
{% endblock breadcrumbs %}
{% block subtitle %}
<div class="object-subtitle">
<div class="text-muted">{{ script.Meta.description|markdown }}</div>
</div>
{% endblock subtitle %}
{% block controls %}{% endblock %}
{% block tabs %}
<ul class="nav nav-tabs px-3">
<li class="nav-item" role="presentation">
<a class="nav-link{% if not tab %} active{% endif %}" href="{% url 'extras:script' module=script.module name=script.class_name %}">Script</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link{% if tab == 'source' %} active{% endif %}" href="{% url 'extras:script_source' module=script.module name=script.class_name %}">Source</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link{% if tab == 'jobs' %} active{% endif %}" href="{% url 'extras:script_jobs' module=script.module name=script.class_name %}">Jobs</a>
</li>
</ul>
{% endblock tabs %}

View File

@ -0,0 +1,15 @@
{% extends 'extras/script/base.html' %}
{% load render_table from django_tables2 %}
{% block content %}
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<div class="card-body table-responsive">
{% render_table table 'inc/table.html' %}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,6 @@
{% extends 'extras/script/base.html' %}
{% block content %}
<code class="h6 my-3 d-block">{{ script.filename }}</code>
<pre class="block">{{ script.source }}</pre>
{% endblock %}