PR review updates

This commit is contained in:
John Anderson 2020-07-03 11:55:04 -04:00
parent f98fa364c0
commit f092c107b5
17 changed files with 178 additions and 123 deletions

View File

@ -258,11 +258,7 @@ class JobResultSerializer(serializers.ModelSerializer):
# #
class ReportSerializer(serializers.Serializer): class ReportSerializer(serializers.Serializer):
url = serializers.HyperlinkedIdentityField( id = serializers.CharField(read_only=True, source="full_name")
view_name='extras-api:report-detail',
lookup_field='full_name',
lookup_url_kwarg='pk'
)
module = serializers.CharField(max_length=255) module = serializers.CharField(max_length=255)
name = serializers.CharField(max_length=255) name = serializers.CharField(max_length=255)
description = serializers.CharField(max_length=255, required=False) description = serializers.CharField(max_length=255, required=False)
@ -279,12 +275,8 @@ class ReportDetailSerializer(ReportSerializer):
# #
class ScriptSerializer(serializers.Serializer): class ScriptSerializer(serializers.Serializer):
url = serializers.HyperlinkedIdentityField(
view_name='extras-api:script-detail',
lookup_field='full_name',
lookup_url_kwarg='pk'
)
id = serializers.CharField(read_only=True, source="full_name") id = serializers.CharField(read_only=True, source="full_name")
module = serializers.CharField(max_length=255)
name = serializers.CharField(read_only=True) name = serializers.CharField(read_only=True)
description = serializers.CharField(read_only=True) description = serializers.CharField(read_only=True)
vars = serializers.SerializerMethodField(read_only=True) vars = serializers.SerializerMethodField(read_only=True)

View File

@ -14,7 +14,7 @@ from extras.choices import JobResultStatusChoices
from extras.models import ( from extras.models import (
ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, JobResult, Tag, ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, JobResult, Tag,
) )
from extras.reports import get_report, get_reports from extras.reports import get_report, get_reports, run_report
from extras.scripts import get_script, get_scripts, run_script from extras.scripts import get_script, get_scripts, run_script
from utilities.api import IsAuthenticatedOrLoginNotRequired, ModelViewSet from utilities.api import IsAuthenticatedOrLoginNotRequired, ModelViewSet
from utilities.utils import copy_safe_request from utilities.utils import copy_safe_request

View File

@ -129,25 +129,23 @@ class JobResultStatusChoices(ChoiceSet):
STATUS_PENDING = 'pending' STATUS_PENDING = 'pending'
STATUS_RUNNING = 'running' STATUS_RUNNING = 'running'
STATUS_COMPLETED = 'completed' STATUS_COMPLETED = 'completed'
STATUS_ERRORED = 'errored'
STATUS_FAILED = 'failed' STATUS_FAILED = 'failed'
CHOICES = ( CHOICES = (
(STATUS_PENDING, 'Pending'), (STATUS_PENDING, 'Pending'),
(STATUS_RUNNING, 'Running'), (STATUS_RUNNING, 'Running'),
(STATUS_COMPLETED, 'Completed'), (STATUS_COMPLETED, 'Completed'),
(STATUS_ERRORED, 'Errored'),
(STATUS_FAILED, 'Failed'), (STATUS_FAILED, 'Failed'),
) )
TERMINAL_STATE_CHOICES = ( TERMINAL_STATE_CHOICES = (
STATUS_COMPLETED, STATUS_COMPLETED,
STATUS_ERRORED,
STATUS_FAILED, STATUS_FAILED,
) )
NON_TERMINAL_STATE_CHOICES = (
STATUS_PENDING,
STATUS_RUNNING,
)
# #
# Webhooks # Webhooks

View File

@ -0,0 +1,22 @@
# Generated by Django 3.0.7 on 2020-06-23 02:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('extras', '0042_customfield_manager'),
]
operations = [
migrations.CreateModel(
name='Report',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
],
options={
'managed': False,
},
)
]

View File

@ -13,15 +13,11 @@ def convert_job_results(apps, schema_editor):
""" """
Convert ReportResult objects to JobResult objects Convert ReportResult objects to JobResult objects
""" """
from django.contrib.contenttypes.management import create_contenttypes
from extras.choices import JobResultStatusChoices from extras.choices import JobResultStatusChoices
ReportResult = apps.get_model('extras', 'ReportResult') ReportResult = apps.get_model('extras', 'ReportResult')
JobResult = apps.get_model('extras', 'JobResult') JobResult = apps.get_model('extras', 'JobResult')
ContentType = apps.get_model('contenttypes', 'ContentType') ContentType = apps.get_model('contenttypes', 'ContentType')
app_config = apps.get_app_config('extras')
app_config.models_module = app_config.models_module or True
create_contenttypes(app_config)
report_content_type = ContentType.objects.get(app_label='extras', model='report') report_content_type = ContentType.objects.get(app_label='extras', model='report')
job_results = [] job_results = []
@ -50,19 +46,10 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
('contenttypes', '0002_remove_content_type_name'), ('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('extras', '0042_customfield_manager'), ('extras', '0043_report_model'),
] ]
operations = [ operations = [
migrations.CreateModel(
name='Report',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
],
options={
'managed': False,
},
),
migrations.CreateModel( migrations.CreateModel(
name='JobResult', name='JobResult',
fields=[ fields=[

View File

@ -12,6 +12,7 @@ from django.db import models
from django.http import HttpResponse from django.http import HttpResponse
from django.template import Template, Context from django.template import Template, Context
from django.urls import reverse from django.urls import reverse
from django.utils import timezone
from rest_framework.utils.encoders import JSONEncoder from rest_framework.utils.encoders import JSONEncoder
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
@ -650,6 +651,18 @@ class JobResult(models.Model):
return f"{int(minutes)} minutes, {seconds:.2f} seconds" return f"{int(minutes)} minutes, {seconds:.2f} seconds"
def set_status(self, status):
"""
Helper method to change the status of the job result and save. If the target status is terminal, the
completion time is also set.
"""
self.status = status
if status in JobResultStatusChoices.TERMINAL_STATE_CHOICES:
self.completed = timezone.now()
self.save()
@classmethod @classmethod
def enqueue_job(cls, func, name, obj_type, user, *args, **kwargs): def enqueue_job(cls, func, name, obj_type, user, *args, **kwargs):
""" """

View File

@ -14,6 +14,9 @@ from .constants import *
from .models import JobResult from .models import JobResult
logger = logging.getLogger(__name__)
def is_report(obj): def is_report(obj):
""" """
Returns True if the given object is a Report. Returns True if the given object is a Report.
@ -71,13 +74,18 @@ def run_report(job_result, *args, **kwargs):
""" """
module_name, report_name = job_result.name.split('.', 1) module_name, report_name = job_result.name.split('.', 1)
report = get_report(module_name, report_name) report = get_report(module_name, report_name)
report.run(job_result)
try:
report.run(job_result)
except Exception:
job_result.set_status(JobResultStatusChoices.STATUS_ERRORED)
logging.error(f"Error during execution of report {job_result.name}")
# Delete any previous terminal state results # Delete any previous terminal state results
JobResult.objects.filter( JobResult.objects.filter(
obj_type=job_result.obj_type, obj_type=job_result.obj_type,
name=job_result.name, name=job_result.name,
status=JobResultStatusChoices.TERMINAL_STATE_CHOICES status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).exclude( ).exclude(
pk=job_result.pk pk=job_result.pk
).delete() ).delete()

View File

@ -399,10 +399,6 @@ def run_script(data, request, commit=True, *args, **kwargs):
A wrapper for calling Script.run(). This performs error handling and provides a hook for committing changes. It A wrapper for calling Script.run(). This performs error handling and provides a hook for committing changes. It
exists outside of the Script class to ensure it cannot be overridden by a script author. exists outside of the Script class to ensure it cannot be overridden by a script author.
""" """
output = None
start_time = None
end_time = None
job_result = kwargs.pop('job_result') job_result = kwargs.pop('job_result')
module, script_name = job_result.name.split('.', 1) module, script_name = job_result.name.split('.', 1)
@ -463,7 +459,7 @@ def run_script(data, request, commit=True, *args, **kwargs):
JobResult.objects.filter( JobResult.objects.filter(
obj_type=job_result.obj_type, obj_type=job_result.obj_type,
name=job_result.name, name=job_result.name,
status=JobResultStatusChoices.TERMINAL_STATE_CHOICES status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).exclude( ).exclude(
pk=job_result.pk pk=job_result.pk
).delete() ).delete()

View File

@ -1,8 +1,8 @@
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, ToggleColumn from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn
from .models import ConfigContext, ObjectChange, JobResult, Tag, TaggedItem from .models import ConfigContext, ObjectChange, Tag, TaggedItem
TAGGED_ITEM = """ TAGGED_ITEM = """
{% if value.get_absolute_url %} {% if value.get_absolute_url %}

View File

@ -8,9 +8,9 @@ from rest_framework import status
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, RackGroup, RackRole, Site from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, RackGroup, RackRole, Site
from extras.api.views import ScriptViewSet from extras.api.views import ScriptViewSet
from extras.models import ConfigContext, Graph, ExportTemplate, Tag from extras.models import ConfigContext, Graph, ExportTemplate, Tag
from extras.reports import Report
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
from utilities.testing import APITestCase, APIViewTestCases from utilities.testing import APITestCase, APIViewTestCases
from utilities.utils import copy_safe_request
class AppTest(APITestCase): class AppTest(APITestCase):
@ -208,6 +208,37 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
self.assertEqual(rendered_context['bar'], 456) self.assertEqual(rendered_context['bar'], 456)
class ReportTest(APITestCase):
class TestReport(Report):
pass # The report is not actually run, we are testing that the view enqueues the job
def get_test_report(self, *args):
return self.TestReport
def setUp(self):
super().setUp()
# Monkey-patch the API viewset's _get_script method to return our test script above
ReportViewSet._get_report = self.get_test_report
def test_get_report(self):
url = reverse('extras-api:report-detail', kwargs={'pk': None})
response = self.client.get(url, **self.header)
self.assertEqual(response.data['name'], self.TestReport.__name__)
def test_run_report(self):
url = reverse('extras-api:report-detail', kwargs={'pk': None})
response = self.client.post(url, {}, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(response.data['result']['status']['value'], 'pending')
class ScriptTest(APITestCase): class ScriptTest(APITestCase):
class TestScript(Script): class TestScript(Script):

View File

@ -36,12 +36,12 @@ urlpatterns = [
# Reports # Reports
path('reports/', views.ReportListView.as_view(), name='report_list'), path('reports/', views.ReportListView.as_view(), name='report_list'),
path('reports/<str:name>/', views.ReportView.as_view(), name='report'), path('reports/<str:module>.<str:name>/', views.ReportView.as_view(), name='report'),
path('reports/<str:name>/run/', views.ReportRunView.as_view(), name='report_run'), path('reports/results/<int:job_result_pk>/', views.ReportResultView.as_view(), name='report_result'),
# Scripts # Scripts
path('scripts/', views.ScriptListView.as_view(), name='script_list'), path('scripts/', views.ScriptListView.as_view(), name='script_list'),
path('scripts/<str:module>/<str:name>/', views.ScriptView.as_view(), name='script'), path('scripts/<str:module>.<str:name>/', views.ScriptView.as_view(), name='script'),
path('scripts/<str:module>/<str:name>/result/<int:job_result_pk>/', views.ScriptResultView.as_view(), name='script_result'), path('scripts/results/<int:job_result_pk>/', views.ScriptResultView.as_view(), name='script_result'),
] ]

View File

@ -350,12 +350,10 @@ class ReportListView(ContentTypePermissionRequiredMixin, View):
class GetReportMixin: class GetReportMixin:
def get_report(self, name): def _get_report(self, name, module=None):
if '.' not in name: if module is None:
raise Http404 module, name = name.split('.', 1)
# Retrieve the Report by "<module>.<report>" report = get_report(module, name)
module_name, report_name = name.split('.', 1)
report = get_report(module_name, report_name)
if report is None: if report is None:
raise Http404 raise Http404
@ -369,43 +367,29 @@ class ReportView(GetReportMixin, ContentTypePermissionRequiredMixin, View):
def get_required_permission(self): def get_required_permission(self):
return 'extras.view_reportresult' return 'extras.view_reportresult'
def get(self, request, name): def get(self, request, module, name):
report = self.get_report(name) report = self._get_report(name, module)
report_content_type = ContentType.objects.get(app_label='extras', model='report') report_content_type = ContentType.objects.get(app_label='extras', model='report')
latest_run_result = JobResult.objects.filter( report.result = JobResult.objects.filter(
obj_type=report_content_type, obj_type=report_content_type,
name=report.full_name, name=report.full_name,
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).first() ).first()
pending_run_result = JobResult.objects.filter(
obj_type=report_content_type,
name=report.full_name,
status__in=JobResultStatusChoices.NON_TERMINAL_STATE_CHOICES
).order_by(
'created'
).first()
report.result = latest_run_result
report.pending_result = pending_run_result
return render(request, 'extras/report.html', { return render(request, 'extras/report.html', {
'report': report, 'report': report,
'run_form': ConfirmationForm(), 'run_form': ConfirmationForm(),
}) })
def post(self, request, module, name):
class ReportRunView(GetReportMixin, ContentTypePermissionRequiredMixin, View): # Permissions check
""" if not request.user.has_perm('extras.run_report'):
Run a Report and record a new ReportResult. return HttpResponseForbidden()
"""
def get_required_permission(self):
return 'extras.add_reportresult'
def post(self, request, name): report = self._get_report(name, module)
report = self.get_report(name)
form = ConfirmationForm(request.POST) form = ConfirmationForm(request.POST)
if form.is_valid(): if form.is_valid():
@ -419,15 +403,42 @@ class ReportRunView(GetReportMixin, ContentTypePermissionRequiredMixin, View):
request.user request.user
) )
return redirect('extras:report', name=report.full_name) return redirect('extras:report_result', job_result_pk=job_result.pk)
return render(request, 'extras/report.html', {
'report': report,
'run_form': form,
})
class ReportResultView(ContentTypePermissionRequiredMixin, GetReportMixin, View):
def get_required_permission(self):
return 'extras.view_report'
def get(self, request, job_result_pk):
result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk)
report_content_type = ContentType.objects.get(app_label='extras', model='report')
if result.obj_type != report_content_type:
raise Http404
report = self._get_report(result.name)
return render(request, 'extras/report_result.html', {
'report': report,
'result': result,
'class_name': report.name,
'run_form': ConfirmationForm(),
})
# #
# Scripts # Scripts
# #
class GetScriptMixin: class GetScriptMixin:
def _get_script(self, module, name): def _get_script(self, name, module=None):
if module is None:
module, name = name.split('.', 1)
scripts = get_scripts() scripts = get_scripts()
try: try:
return scripts[module][name]() return scripts[module][name]()
@ -453,7 +464,7 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
return 'extras.view_script' return 'extras.view_script'
def get(self, request, module, name): def get(self, request, module, name):
script = self._get_script(module, name) script = self._get_script(name, module)
form = script.as_form(initial=request.GET) form = script.as_form(initial=request.GET)
# Look for a pending JobResult (use the latest one by creation timestamp) # Look for a pending JobResult (use the latest one by creation timestamp)
@ -461,7 +472,8 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
script.result = JobResult.objects.filter( script.result = JobResult.objects.filter(
obj_type=script_content_type, obj_type=script_content_type,
name=script.full_name, name=script.full_name,
status__in=JobResultStatusChoices.NON_TERMINAL_STATE_CHOICES ).exclude(
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).first() ).first()
return render(request, 'extras/script.html', { return render(request, 'extras/script.html', {
@ -476,10 +488,8 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
if not request.user.has_perm('extras.run_script'): if not request.user.has_perm('extras.run_script'):
return HttpResponseForbidden() return HttpResponseForbidden()
script = self._get_script(module, name) script = self._get_script(name, module)
form = script.as_form(request.POST, request.FILES) form = script.as_form(request.POST, request.FILES)
output = None
execution_time = None
if form.is_valid(): if form.is_valid():
commit = form.cleaned_data.pop('_commit') commit = form.cleaned_data.pop('_commit')
@ -495,7 +505,13 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
commit=commit commit=commit
) )
return redirect('extras:script_result', module=module, name=name, job_result_pk=job_result.pk) return redirect('extras:script_result', job_result_pk=job_result.pk)
return render(request, 'extras/script.html', {
'module': module,
'script': script,
'form': form,
})
class ScriptResultView(ContentTypePermissionRequiredMixin, GetScriptMixin, View): class ScriptResultView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
@ -503,19 +519,16 @@ class ScriptResultView(ContentTypePermissionRequiredMixin, GetScriptMixin, View)
def get_required_permission(self): def get_required_permission(self):
return 'extras.view_script' return 'extras.view_script'
def get(self, request, module, name, job_result_pk): def get(self, request, job_result_pk):
script = self._get_script(module, name)
form = script.as_form(initial=request.GET)
# Look for a pending JobResult (use the latest one by creation timestamp)
script_content_type = ContentType.objects.get(app_label='extras', model='script')
result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk) result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk)
script_content_type = ContentType.objects.get(app_label='extras', model='script')
if result.obj_type != script_content_type: if result.obj_type != script_content_type:
raise Http404 raise Http404
script = self._get_script(result.name)
return render(request, 'extras/script_result.html', { return render(request, 'extras/script_result.html', {
'module': module,
'script': script, 'script': script,
'result': result, 'result': result,
'class_name': name 'class_name': script.__class__.__name__
}) })

View File

@ -21,7 +21,7 @@
{% for report in module_reports %} {% for report in module_reports %}
<tr> <tr>
<td> <td>
<a href="{% url 'extras:report' name=report.full_name %}" name="report.{{ report.name }}"><strong>{{ report.name }}</strong></a> <a href="{% url 'extras:report_result' job_result_pk=report.result.pk %}" name="report.{{ report.name }}"><strong>{{ report.name }}</strong></a>
</td> </td>
<td> <td>
{% include 'extras/inc/job_label.html' with result=report.result %} {% include 'extras/inc/job_label.html' with result=report.result %}

View File

@ -16,36 +16,35 @@
</div> </div>
{% if perms.extras.add_reportresult %} {% if perms.extras.add_reportresult %}
<div class="pull-right noprint"> <div class="pull-right noprint">
<form action="{% url 'extras:report_run' name=report.full_name %}" method="post"> <form action="{% url 'extras:report' module=report.module name=report.name %}" method="post">
{% csrf_token %} {% csrf_token %}
{{ run_form }} {{ run_form }}
<button type="submit" name="_run" class="btn btn-primary"><i class="fa fa-play"></i> Run Report</button> <button type="submit" name="_run" class="btn btn-primary"><i class="fa fa-play"></i> Run Report</button>
</form> </form>
</div> </div>
{% endif %} {% endif %}
<h1>{{ report.name }}{% include 'extras/inc/job_label.html' with result=report.result %}</h1> <h1>{{ report.name }}</h1>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
{% if report.description %} {% if report.description %}
<p class="lead">{{ report.description }}</p> <p class="lead">{{ report.description }}</p>
{% endif %} {% endif %}
{% if report.result %} <p>
<p>Last run: <strong>{{ report.result.created }}</strong> {% if report.result.completed %} Duration: <strong>{{ report.result.duration }}</strong>{% endif %}</p> Run: <strong>{{ result.created }}</strong>
{% endif %} {% if result.completed %}
{% if report.pending_result %} Duration: <strong>{{ result.duration }}</strong>
<p> {% else %}
Pending run: <strong>{{ report.pending_result.created }}</strong>
<span id="pending-result-label">{% include 'extras/inc/job_label.html' with result=report.pending_result %}</span>
<img id="pending-result-loader" src="{% static 'img/ajax-loader.gif' %}" /> <img id="pending-result-loader" src="{% static 'img/ajax-loader.gif' %}" />
</p> {% endif %}
{% endif %} <span id="pending-result-label">{% include 'extras/inc/job_label.html' with result=result %}</span>
{% if report.result %} </p>
{% if result.completed %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Report Methods</strong> <strong>Report Methods</strong>
</div> </div>
<table class="table table-hover panel-body"> <table class="table table-hover panel-body">
{% for method, data in report.result.data.items %} {% for method, data in result.data.items %}
<tr> <tr>
<td><code><a href="#{{ method }}">{{ method }}</a></code></td> <td><code><a href="#{{ method }}">{{ method }}</a></code></td>
<td class="text-right report-stats"> <td class="text-right report-stats">
@ -72,7 +71,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for method, data in report.result.data.items %} {% for method, data in result.data.items %}
<tr> <tr>
<th colspan="4" style="font-family: monospace"> <th colspan="4" style="font-family: monospace">
<a name="{{ method }}"></a>{{ method }} <a name="{{ method }}"></a>{{ method }}
@ -99,11 +98,7 @@
</table> </table>
</div> </div>
{% else %} {% else %}
<div class="well">No results are available for this report. Please run the report first.</div> <div class="well">Pending results</div>
{% endif %}
</div>
<div class="col-md-3">
{% if report.result %}
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -111,19 +106,14 @@
{% block javascript %} {% block javascript %}
<script type="text/javascript"> <script type="text/javascript">
{% if report.pending_result %} {% if not result.completed %}
var pending_result_id = {{ report.pending_result.pk }}; var pending_result_id = {{ result.pk }};
{% else %} {% else %}
var pending_result_id = null; var pending_result_id = null;
{% endif %} {% endif %}
function jobTerminatedAction(){ function jobTerminatedAction(){
$('#pending-result-loader').hide(); refreshWindow();
var refreshButton = document.createElement('button');
refreshButton.className = 'btn btn-xs btn-primary';
refreshButton.onclick = refreshWindow;
refreshButton.innerHTML = '<i class="fa fa-refresh"></i> Refresh';
$('#pending-result-loader').parents('p').append(refreshButton)
} }
</script> </script>

View File

@ -11,7 +11,7 @@
<div class="col-md-12"> <div class="col-md-12">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'extras:script_list' %}">Scripts</a></li> <li><a href="{% url 'extras:script_list' %}">Scripts</a></li>
<li><a href="{% url 'extras:script_list' %}#module.{{ module }}">{{ module|bettertitle }}</a></li> <li><a href="{% url 'extras:script_list' %}#module.{{ script.module }}">{{ script.module|bettertitle }}</a></li>
<li><a href="{% url 'extras:script' module=script.module name=class_name %}">{{ script }}</a></li> <li><a href="{% url 'extras:script' module=script.module name=class_name %}">{{ script }}</a></li>
<li>{{ result.created }}</li> <li>{{ result.created }}</li>
</ol> </ol>
@ -36,9 +36,9 @@
{% if result.completed %} {% if result.completed %}
Duration: <strong>{{ result.duration }}</strong> Duration: <strong>{{ result.duration }}</strong>
{% else %} {% else %}
<span id="pending-result-label">{% include 'extras/inc/job_label.html' with result=result %}</span>
<img id="pending-result-loader" src="{% static 'img/ajax-loader.gif' %}" /> <img id="pending-result-loader" src="{% static 'img/ajax-loader.gif' %}" />
{% endif %} {% endif %}
<span id="pending-result-label">{% include 'extras/inc/job_label.html' with result=result %}</span>
</p> </p>
<div role="tabpanel" class="tab-pane active" id="log"> <div role="tabpanel" class="tab-pane active" id="log">
{% if result.completed %} {% if result.completed %}
@ -76,6 +76,12 @@
</div> </div>
</div> </div>
</div> </div>
{% else %}
<div class="row">
<div class="col-md-12">
<div class="well">Pending results</div>
</div>
</div>
{% endif %} {% endif %}
</div> </div>
<div role="tabpanel" class="tab-pane" id="output"> <div role="tabpanel" class="tab-pane" id="output">

View File

@ -280,7 +280,7 @@
<table class="table table-hover panel-body"> <table class="table table-hover panel-body">
{% for result in report_results %} {% for result in report_results %}
<tr> <tr>
<td><a href="{% url 'extras:report' name=result.name %}">{{ result.name }}</a></td> <td><a href="{% url 'extras:report_result' job_result_pk=result.pk %}">{{ result.name }}</a></td>
<td class="text-right"><span title="{{ result.created }}">{% include 'extras/inc/job_label.html' %}</span></td> <td class="text-right"><span title="{{ result.created }}">{% include 'extras/inc/job_label.html' %}</span></td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -270,9 +270,8 @@ class NetBoxFakeRequest:
A fake request object which is explicitly defined at the module level so it is able to be pickled. It simply A fake request object which is explicitly defined at the module level so it is able to be pickled. It simply
takes what is passed to it as kwargs on init and sets them as instance variables. takes what is passed to it as kwargs on init and sets them as instance variables.
""" """
def __init__(self, *args, **kwargs): def __init__(self, _dict):
for k, v in kwargs.items(): self.__dict__ = _dict
setattr(self, k, v)
def copy_safe_request(request): def copy_safe_request(request):
@ -285,7 +284,7 @@ def copy_safe_request(request):
for k in HTTP_REQUEST_META_SAFE_COPY for k in HTTP_REQUEST_META_SAFE_COPY
if k in request.META and isinstance(request.META[k], str) if k in request.META and isinstance(request.META[k], str)
} }
return NetBoxFakeRequest(**{ return NetBoxFakeRequest({
'META': meta, 'META': meta,
'POST': request.POST, 'POST': request.POST,
'GET': request.GET, 'GET': request.GET,