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.core.validators import MinValueValidator
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.urls.exceptions import NoReverseMatch
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@ -96,21 +95,12 @@ class Job(models.Model):
def __str__(self): def __str__(self):
return str(self.job_id) 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): def get_absolute_url(self):
try: # TODO: Employ dynamic registration
return reverse(f'extras:{self.object_type.model}_result', args=[self.pk]) if self.object_type.model == 'reportmodule':
except NoReverseMatch: return reverse(f'extras:report_result', kwargs={'job_pk': self.pk})
return None if self.object_type.model == 'scriptmodule':
return reverse(f'extras:script_result', kwargs={'job_pk': self.pk})
def get_status_color(self): def get_status_color(self):
return JobStatusChoices.colors.get(self.status) return JobStatusChoices.colors.get(self.status)
@ -130,6 +120,16 @@ class Job(models.Model):
return f"{int(minutes)} minutes, {seconds:.2f} seconds" 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): def start(self):
""" """
Record the job's start time and update its status to "running." 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): class JobTable(NetBoxTable):
id = tables.Column(
linkify=True
)
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
object_type = columns.ContentTypeColumn( object_type = columns.ContentTypeColumn(
verbose_name=_('Type') verbose_name=_('Type')
) )
object = tables.Column(
linkify=True
)
status = columns.ChoiceFieldColumn() status = columns.ChoiceFieldColumn()
created = columns.DateTimeColumn() created = columns.DateTimeColumn()
scheduled = columns.DateTimeColumn() scheduled = columns.DateTimeColumn()
@ -25,10 +31,9 @@ class JobTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Job model = Job
fields = ( fields = (
'pk', 'id', 'object_type', 'name', 'status', 'created', 'scheduled', 'interval', 'started', 'completed', 'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'scheduled', 'interval', 'started',
'user', 'job_id', 'completed', 'user', 'job_id',
) )
default_columns = ( default_columns = (
'pk', 'id', 'object_type', 'name', 'status', 'created', 'scheduled', 'interval', 'started', 'completed', 'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user',
'user',
) )

View File

@ -98,6 +98,7 @@ urlpatterns = [
path('reports/results/<int:job_pk>/', views.ReportResultView.as_view(), name='report_result'), 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/<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>/', views.ReportView.as_view(), name='report'),
path('reports/<path:module>.<str:name>/jobs/', views.ReportJobsView.as_view(), name='report_jobs'),
# Scripts # Scripts
path('scripts/', views.ScriptListView.as_view(), name='script_list'), 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/results/<int:job_pk>/', views.ScriptResultView.as_view(), name='script_result'),
path('scripts/<int:pk>/', include(get_model_urls('extras', 'scriptmodule'))), 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>/', 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 # Markdown
path('render/markdown/', views.RenderMarkdownView.as_view(), name="render_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.choices import JobStatusChoices, ManagedFileRootPathChoices
from core.forms import ManagedFileForm from core.forms import ManagedFileForm
from core.models import Job from core.models import Job
from core.tables import JobTable
from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
from extras.dashboard.utils import get_widget_class from extras.dashboard.utils import get_widget_class
from netbox.views import generic 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): class ReportResultView(ContentTypePermissionRequiredMixin, View):
""" """
Display a Job pertaining to the execution of a Report. 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): class ScriptResultView(ContentTypePermissionRequiredMixin, View):
def get_required_permission(self): def get_required_permission(self):

View File

@ -1,36 +1,7 @@
{% extends 'generic/object.html' %} {% extends 'extras/report/base.html' %}
{% load helpers %} {% load helpers %}
{% load form_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 %} {% block content %}
<div role="tabpanel" class="tab-pane active" id="report"> <div role="tabpanel" class="tab-pane active" id="report">
{% if perms.extras.run_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,40 +1,9 @@
{% extends 'generic/object.html' %} {% extends 'extras/script/base.html' %}
{% load helpers %} {% load helpers %}
{% load form_helpers %} {% load form_helpers %}
{% load log_levels %} {% 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 %} {% block content %}
<div role="tabpanel" class="tab-pane active" id="run">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
{% if not perms.extras.run_script %} {% if not perms.extras.run_script %}
@ -83,9 +52,4 @@
</form> </form>
</div> </div>
</div> </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 %} {% 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 %}