mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 17:26:10 -06:00
Add tabbed views for report & script jobs
This commit is contained in:
parent
431b21b5fe
commit
e3cd40e95f
@ -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."
|
||||
|
@ -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',
|
||||
)
|
||||
|
@ -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")
|
||||
|
@ -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):
|
||||
|
@ -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 %}
|
||||
|
35
netbox/templates/extras/report/base.html
Normal file
35
netbox/templates/extras/report/base.html
Normal 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 %}
|
15
netbox/templates/extras/report/jobs.html
Normal file
15
netbox/templates/extras/report/jobs.html
Normal 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 %}
|
@ -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 %}
|
||||
|
37
netbox/templates/extras/script/base.html
Normal file
37
netbox/templates/extras/script/base.html
Normal 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 %}
|
15
netbox/templates/extras/script/jobs.html
Normal file
15
netbox/templates/extras/script/jobs.html
Normal 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 %}
|
6
netbox/templates/extras/script/source.html
Normal file
6
netbox/templates/extras/script/source.html
Normal 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 %}
|
Loading…
Reference in New Issue
Block a user