mirror of
https://github.com/netbox-community/netbox.git
synced 2026-03-21 17:10:10 +01:00
Because scripts are already linked through the Job Instance field, the name is displayed twice. Removing this reduces redundancy and opens up the possibility of simplifying the BackgroundJob framework in future commits.
1356 lines
42 KiB
Python
1356 lines
42 KiB
Python
from django.contrib import messages
|
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.core.paginator import EmptyPage
|
|
from django.db.models import Count, Q
|
|
from django.http import HttpResponseBadRequest, HttpResponseForbidden, HttpResponse
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.urls import reverse
|
|
from django.utils.module_loading import import_string
|
|
from django.utils import timezone
|
|
from django.utils.translation import gettext as _
|
|
from django.views.generic import View
|
|
|
|
from core.choices import ManagedFileRootPathChoices
|
|
from core.forms import ManagedFileForm
|
|
from core.models import Job
|
|
from core.tables import JobTable
|
|
from dcim.models import Device, DeviceRole, Platform
|
|
from extras.choices import LogLevelChoices
|
|
from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
|
|
from extras.dashboard.utils import get_widget_class
|
|
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
|
from netbox.views import generic
|
|
from netbox.views.generic.mixins import TableMixin
|
|
from utilities.forms import ConfirmationForm, get_field_value
|
|
from utilities.htmx import htmx_partial
|
|
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
|
from utilities.query import count_related
|
|
from utilities.querydict import normalize_querydict
|
|
from utilities.request import copy_safe_request
|
|
from utilities.rqworker import get_workers_for_queue
|
|
from utilities.templatetags.builtins.filters import render_markdown
|
|
from utilities.views import ContentTypePermissionRequiredMixin, get_viewname, register_model_view
|
|
from virtualization.models import VirtualMachine
|
|
from . import filtersets, forms, tables
|
|
from .constants import LOG_LEVEL_RANK
|
|
from .models import *
|
|
from .tables import ReportResultsTable, ScriptResultsTable
|
|
|
|
|
|
#
|
|
# Custom fields
|
|
#
|
|
|
|
class CustomFieldListView(generic.ObjectListView):
|
|
queryset = CustomField.objects.select_related('choice_set')
|
|
filterset = filtersets.CustomFieldFilterSet
|
|
filterset_form = forms.CustomFieldFilterForm
|
|
table = tables.CustomFieldTable
|
|
|
|
|
|
@register_model_view(CustomField)
|
|
class CustomFieldView(generic.ObjectView):
|
|
queryset = CustomField.objects.select_related('choice_set')
|
|
|
|
def get_extra_context(self, request, instance):
|
|
related_models = ()
|
|
|
|
for object_type in instance.object_types.all():
|
|
related_models += (
|
|
object_type.model_class().objects.restrict(request.user, 'view').exclude(
|
|
Q(**{f'custom_field_data__{instance.name}': ''}) |
|
|
Q(**{f'custom_field_data__{instance.name}': None})
|
|
),
|
|
)
|
|
|
|
return {
|
|
'related_models': related_models
|
|
}
|
|
|
|
|
|
@register_model_view(CustomField, 'edit')
|
|
class CustomFieldEditView(generic.ObjectEditView):
|
|
queryset = CustomField.objects.select_related('choice_set')
|
|
form = forms.CustomFieldForm
|
|
|
|
|
|
@register_model_view(CustomField, 'delete')
|
|
class CustomFieldDeleteView(generic.ObjectDeleteView):
|
|
queryset = CustomField.objects.select_related('choice_set')
|
|
|
|
|
|
class CustomFieldBulkImportView(generic.BulkImportView):
|
|
queryset = CustomField.objects.select_related('choice_set')
|
|
model_form = forms.CustomFieldImportForm
|
|
|
|
|
|
class CustomFieldBulkEditView(generic.BulkEditView):
|
|
queryset = CustomField.objects.select_related('choice_set')
|
|
filterset = filtersets.CustomFieldFilterSet
|
|
table = tables.CustomFieldTable
|
|
form = forms.CustomFieldBulkEditForm
|
|
|
|
|
|
class CustomFieldBulkDeleteView(generic.BulkDeleteView):
|
|
queryset = CustomField.objects.select_related('choice_set')
|
|
filterset = filtersets.CustomFieldFilterSet
|
|
table = tables.CustomFieldTable
|
|
|
|
|
|
#
|
|
# Custom field choices
|
|
#
|
|
|
|
class CustomFieldChoiceSetListView(generic.ObjectListView):
|
|
queryset = CustomFieldChoiceSet.objects.all()
|
|
filterset = filtersets.CustomFieldChoiceSetFilterSet
|
|
filterset_form = forms.CustomFieldChoiceSetFilterForm
|
|
table = tables.CustomFieldChoiceSetTable
|
|
|
|
|
|
@register_model_view(CustomFieldChoiceSet)
|
|
class CustomFieldChoiceSetView(generic.ObjectView):
|
|
queryset = CustomFieldChoiceSet.objects.all()
|
|
|
|
def get_extra_context(self, request, instance):
|
|
|
|
# Paginate choices list
|
|
per_page = get_paginate_count(request)
|
|
try:
|
|
page_number = request.GET.get('page', 1)
|
|
except ValueError:
|
|
page_number = 1
|
|
paginator = EnhancedPaginator(instance.choices, per_page)
|
|
try:
|
|
choices = paginator.page(page_number)
|
|
except EmptyPage:
|
|
choices = paginator.page(paginator.num_pages)
|
|
|
|
return {
|
|
'paginator': paginator,
|
|
'choices': choices,
|
|
}
|
|
|
|
|
|
@register_model_view(CustomFieldChoiceSet, 'edit')
|
|
class CustomFieldChoiceSetEditView(generic.ObjectEditView):
|
|
queryset = CustomFieldChoiceSet.objects.all()
|
|
form = forms.CustomFieldChoiceSetForm
|
|
|
|
|
|
@register_model_view(CustomFieldChoiceSet, 'delete')
|
|
class CustomFieldChoiceSetDeleteView(generic.ObjectDeleteView):
|
|
queryset = CustomFieldChoiceSet.objects.all()
|
|
|
|
|
|
class CustomFieldChoiceSetBulkImportView(generic.BulkImportView):
|
|
queryset = CustomFieldChoiceSet.objects.all()
|
|
model_form = forms.CustomFieldChoiceSetImportForm
|
|
|
|
|
|
class CustomFieldChoiceSetBulkEditView(generic.BulkEditView):
|
|
queryset = CustomFieldChoiceSet.objects.all()
|
|
filterset = filtersets.CustomFieldChoiceSetFilterSet
|
|
table = tables.CustomFieldChoiceSetTable
|
|
form = forms.CustomFieldChoiceSetBulkEditForm
|
|
|
|
|
|
class CustomFieldChoiceSetBulkDeleteView(generic.BulkDeleteView):
|
|
queryset = CustomFieldChoiceSet.objects.all()
|
|
filterset = filtersets.CustomFieldChoiceSetFilterSet
|
|
table = tables.CustomFieldChoiceSetTable
|
|
|
|
|
|
#
|
|
# Custom links
|
|
#
|
|
|
|
class CustomLinkListView(generic.ObjectListView):
|
|
queryset = CustomLink.objects.all()
|
|
filterset = filtersets.CustomLinkFilterSet
|
|
filterset_form = forms.CustomLinkFilterForm
|
|
table = tables.CustomLinkTable
|
|
|
|
|
|
@register_model_view(CustomLink)
|
|
class CustomLinkView(generic.ObjectView):
|
|
queryset = CustomLink.objects.all()
|
|
|
|
|
|
@register_model_view(CustomLink, 'edit')
|
|
class CustomLinkEditView(generic.ObjectEditView):
|
|
queryset = CustomLink.objects.all()
|
|
form = forms.CustomLinkForm
|
|
|
|
|
|
@register_model_view(CustomLink, 'delete')
|
|
class CustomLinkDeleteView(generic.ObjectDeleteView):
|
|
queryset = CustomLink.objects.all()
|
|
|
|
|
|
class CustomLinkBulkImportView(generic.BulkImportView):
|
|
queryset = CustomLink.objects.all()
|
|
model_form = forms.CustomLinkImportForm
|
|
|
|
|
|
class CustomLinkBulkEditView(generic.BulkEditView):
|
|
queryset = CustomLink.objects.all()
|
|
filterset = filtersets.CustomLinkFilterSet
|
|
table = tables.CustomLinkTable
|
|
form = forms.CustomLinkBulkEditForm
|
|
|
|
|
|
class CustomLinkBulkDeleteView(generic.BulkDeleteView):
|
|
queryset = CustomLink.objects.all()
|
|
filterset = filtersets.CustomLinkFilterSet
|
|
table = tables.CustomLinkTable
|
|
|
|
|
|
#
|
|
# Export templates
|
|
#
|
|
|
|
class ExportTemplateListView(generic.ObjectListView):
|
|
queryset = ExportTemplate.objects.all()
|
|
filterset = filtersets.ExportTemplateFilterSet
|
|
filterset_form = forms.ExportTemplateFilterForm
|
|
table = tables.ExportTemplateTable
|
|
template_name = 'extras/exporttemplate_list.html'
|
|
actions = {
|
|
**DEFAULT_ACTION_PERMISSIONS,
|
|
'bulk_sync': {'sync'},
|
|
}
|
|
|
|
|
|
@register_model_view(ExportTemplate)
|
|
class ExportTemplateView(generic.ObjectView):
|
|
queryset = ExportTemplate.objects.all()
|
|
|
|
|
|
@register_model_view(ExportTemplate, 'edit')
|
|
class ExportTemplateEditView(generic.ObjectEditView):
|
|
queryset = ExportTemplate.objects.all()
|
|
form = forms.ExportTemplateForm
|
|
|
|
|
|
@register_model_view(ExportTemplate, 'delete')
|
|
class ExportTemplateDeleteView(generic.ObjectDeleteView):
|
|
queryset = ExportTemplate.objects.all()
|
|
|
|
|
|
class ExportTemplateBulkImportView(generic.BulkImportView):
|
|
queryset = ExportTemplate.objects.all()
|
|
model_form = forms.ExportTemplateImportForm
|
|
|
|
|
|
class ExportTemplateBulkEditView(generic.BulkEditView):
|
|
queryset = ExportTemplate.objects.all()
|
|
filterset = filtersets.ExportTemplateFilterSet
|
|
table = tables.ExportTemplateTable
|
|
form = forms.ExportTemplateBulkEditForm
|
|
|
|
|
|
class ExportTemplateBulkDeleteView(generic.BulkDeleteView):
|
|
queryset = ExportTemplate.objects.all()
|
|
filterset = filtersets.ExportTemplateFilterSet
|
|
table = tables.ExportTemplateTable
|
|
|
|
|
|
class ExportTemplateBulkSyncDataView(generic.BulkSyncDataView):
|
|
queryset = ExportTemplate.objects.all()
|
|
|
|
|
|
#
|
|
# Saved filters
|
|
#
|
|
|
|
class SavedFilterMixin:
|
|
|
|
def get_queryset(self, request):
|
|
"""
|
|
Return only shared SavedFilters, or those owned by the current user, unless
|
|
this is a superuser.
|
|
"""
|
|
queryset = SavedFilter.objects.all()
|
|
user = request.user
|
|
if user.is_superuser:
|
|
return queryset
|
|
if user.is_anonymous:
|
|
return queryset.filter(shared=True)
|
|
return queryset.filter(
|
|
Q(shared=True) | Q(user=user)
|
|
)
|
|
|
|
|
|
class SavedFilterListView(SavedFilterMixin, generic.ObjectListView):
|
|
filterset = filtersets.SavedFilterFilterSet
|
|
filterset_form = forms.SavedFilterFilterForm
|
|
table = tables.SavedFilterTable
|
|
|
|
|
|
@register_model_view(SavedFilter)
|
|
class SavedFilterView(SavedFilterMixin, generic.ObjectView):
|
|
queryset = SavedFilter.objects.all()
|
|
|
|
|
|
@register_model_view(SavedFilter, 'edit')
|
|
class SavedFilterEditView(SavedFilterMixin, generic.ObjectEditView):
|
|
queryset = SavedFilter.objects.all()
|
|
form = forms.SavedFilterForm
|
|
|
|
def alter_object(self, obj, request, url_args, url_kwargs):
|
|
if not obj.pk:
|
|
obj.user = request.user
|
|
return obj
|
|
|
|
|
|
@register_model_view(SavedFilter, 'delete')
|
|
class SavedFilterDeleteView(SavedFilterMixin, generic.ObjectDeleteView):
|
|
queryset = SavedFilter.objects.all()
|
|
|
|
|
|
class SavedFilterBulkImportView(SavedFilterMixin, generic.BulkImportView):
|
|
queryset = SavedFilter.objects.all()
|
|
model_form = forms.SavedFilterImportForm
|
|
|
|
|
|
class SavedFilterBulkEditView(SavedFilterMixin, generic.BulkEditView):
|
|
queryset = SavedFilter.objects.all()
|
|
filterset = filtersets.SavedFilterFilterSet
|
|
table = tables.SavedFilterTable
|
|
form = forms.SavedFilterBulkEditForm
|
|
|
|
|
|
class SavedFilterBulkDeleteView(SavedFilterMixin, generic.BulkDeleteView):
|
|
queryset = SavedFilter.objects.all()
|
|
filterset = filtersets.SavedFilterFilterSet
|
|
table = tables.SavedFilterTable
|
|
|
|
|
|
#
|
|
# Bookmarks
|
|
#
|
|
|
|
class BookmarkCreateView(generic.ObjectEditView):
|
|
form = forms.BookmarkForm
|
|
|
|
def get_queryset(self, request):
|
|
return Bookmark.objects.filter(user=request.user)
|
|
|
|
def alter_object(self, obj, request, url_args, url_kwargs):
|
|
obj.user = request.user
|
|
return obj
|
|
|
|
|
|
@register_model_view(Bookmark, 'delete')
|
|
class BookmarkDeleteView(generic.ObjectDeleteView):
|
|
|
|
def get_queryset(self, request):
|
|
return Bookmark.objects.filter(user=request.user)
|
|
|
|
|
|
class BookmarkBulkDeleteView(generic.BulkDeleteView):
|
|
table = tables.BookmarkTable
|
|
|
|
def get_queryset(self, request):
|
|
return Bookmark.objects.filter(user=request.user)
|
|
|
|
|
|
#
|
|
# Notification groups
|
|
#
|
|
|
|
class NotificationGroupListView(generic.ObjectListView):
|
|
queryset = NotificationGroup.objects.all()
|
|
filterset = filtersets.NotificationGroupFilterSet
|
|
filterset_form = forms.NotificationGroupFilterForm
|
|
table = tables.NotificationGroupTable
|
|
|
|
|
|
@register_model_view(NotificationGroup)
|
|
class NotificationGroupView(generic.ObjectView):
|
|
queryset = NotificationGroup.objects.all()
|
|
|
|
|
|
@register_model_view(NotificationGroup, 'edit')
|
|
class NotificationGroupEditView(generic.ObjectEditView):
|
|
queryset = NotificationGroup.objects.all()
|
|
form = forms.NotificationGroupForm
|
|
|
|
|
|
@register_model_view(NotificationGroup, 'delete')
|
|
class NotificationGroupDeleteView(generic.ObjectDeleteView):
|
|
queryset = NotificationGroup.objects.all()
|
|
|
|
|
|
class NotificationGroupBulkImportView(generic.BulkImportView):
|
|
queryset = NotificationGroup.objects.all()
|
|
model_form = forms.NotificationGroupImportForm
|
|
|
|
|
|
class NotificationGroupBulkEditView(generic.BulkEditView):
|
|
queryset = NotificationGroup.objects.all()
|
|
filterset = filtersets.NotificationGroupFilterSet
|
|
table = tables.NotificationGroupTable
|
|
form = forms.NotificationGroupBulkEditForm
|
|
|
|
|
|
class NotificationGroupBulkDeleteView(generic.BulkDeleteView):
|
|
queryset = NotificationGroup.objects.all()
|
|
filterset = filtersets.NotificationGroupFilterSet
|
|
table = tables.NotificationGroupTable
|
|
|
|
|
|
#
|
|
# Notifications
|
|
#
|
|
|
|
class NotificationsView(LoginRequiredMixin, View):
|
|
"""
|
|
HTMX-only user-specific notifications list.
|
|
"""
|
|
def get(self, request):
|
|
return render(request, 'htmx/notifications.html', {
|
|
'notifications': request.user.notifications.unread(),
|
|
'total_count': request.user.notifications.count(),
|
|
})
|
|
|
|
|
|
@register_model_view(Notification, 'read')
|
|
class NotificationReadView(LoginRequiredMixin, View):
|
|
"""
|
|
Mark the Notification read and redirect the user to its attached object.
|
|
"""
|
|
def get(self, request, pk):
|
|
notification = get_object_or_404(request.user.notifications, pk=pk)
|
|
notification.read = timezone.now()
|
|
notification.save()
|
|
|
|
return redirect(notification.object.get_absolute_url())
|
|
|
|
|
|
@register_model_view(Notification, 'dismiss')
|
|
class NotificationDismissView(LoginRequiredMixin, View):
|
|
"""
|
|
A convenience view which allows deleting notifications with one click.
|
|
"""
|
|
def get(self, request, pk):
|
|
notification = get_object_or_404(request.user.notifications, pk=pk)
|
|
notification.delete()
|
|
|
|
if htmx_partial(request):
|
|
return render(request, 'htmx/notifications.html', {
|
|
'notifications': request.user.notifications.unread()[:10],
|
|
})
|
|
|
|
return redirect('account:notifications')
|
|
|
|
|
|
@register_model_view(Notification, 'delete')
|
|
class NotificationDeleteView(generic.ObjectDeleteView):
|
|
|
|
def get_queryset(self, request):
|
|
return Notification.objects.filter(user=request.user)
|
|
|
|
|
|
class NotificationBulkDeleteView(generic.BulkDeleteView):
|
|
table = tables.NotificationTable
|
|
|
|
def get_queryset(self, request):
|
|
return Notification.objects.filter(user=request.user)
|
|
|
|
|
|
#
|
|
# Subscriptions
|
|
#
|
|
|
|
class SubscriptionCreateView(generic.ObjectEditView):
|
|
form = forms.SubscriptionForm
|
|
|
|
def get_queryset(self, request):
|
|
return Subscription.objects.filter(user=request.user)
|
|
|
|
def alter_object(self, obj, request, url_args, url_kwargs):
|
|
obj.user = request.user
|
|
return obj
|
|
|
|
|
|
@register_model_view(Subscription, 'delete')
|
|
class SubscriptionDeleteView(generic.ObjectDeleteView):
|
|
|
|
def get_queryset(self, request):
|
|
return Subscription.objects.filter(user=request.user)
|
|
|
|
|
|
class SubscriptionBulkDeleteView(generic.BulkDeleteView):
|
|
table = tables.SubscriptionTable
|
|
|
|
def get_queryset(self, request):
|
|
return Subscription.objects.filter(user=request.user)
|
|
|
|
|
|
#
|
|
# Webhooks
|
|
#
|
|
|
|
class WebhookListView(generic.ObjectListView):
|
|
queryset = Webhook.objects.all()
|
|
filterset = filtersets.WebhookFilterSet
|
|
filterset_form = forms.WebhookFilterForm
|
|
table = tables.WebhookTable
|
|
|
|
|
|
@register_model_view(Webhook)
|
|
class WebhookView(generic.ObjectView):
|
|
queryset = Webhook.objects.all()
|
|
|
|
|
|
@register_model_view(Webhook, 'edit')
|
|
class WebhookEditView(generic.ObjectEditView):
|
|
queryset = Webhook.objects.all()
|
|
form = forms.WebhookForm
|
|
|
|
|
|
@register_model_view(Webhook, 'delete')
|
|
class WebhookDeleteView(generic.ObjectDeleteView):
|
|
queryset = Webhook.objects.all()
|
|
|
|
|
|
class WebhookBulkImportView(generic.BulkImportView):
|
|
queryset = Webhook.objects.all()
|
|
model_form = forms.WebhookImportForm
|
|
|
|
|
|
class WebhookBulkEditView(generic.BulkEditView):
|
|
queryset = Webhook.objects.all()
|
|
filterset = filtersets.WebhookFilterSet
|
|
table = tables.WebhookTable
|
|
form = forms.WebhookBulkEditForm
|
|
|
|
|
|
class WebhookBulkDeleteView(generic.BulkDeleteView):
|
|
queryset = Webhook.objects.all()
|
|
filterset = filtersets.WebhookFilterSet
|
|
table = tables.WebhookTable
|
|
|
|
|
|
#
|
|
# Event Rules
|
|
#
|
|
|
|
class EventRuleListView(generic.ObjectListView):
|
|
queryset = EventRule.objects.all()
|
|
filterset = filtersets.EventRuleFilterSet
|
|
filterset_form = forms.EventRuleFilterForm
|
|
table = tables.EventRuleTable
|
|
|
|
|
|
@register_model_view(EventRule)
|
|
class EventRuleView(generic.ObjectView):
|
|
queryset = EventRule.objects.all()
|
|
|
|
|
|
@register_model_view(EventRule, 'edit')
|
|
class EventRuleEditView(generic.ObjectEditView):
|
|
queryset = EventRule.objects.all()
|
|
form = forms.EventRuleForm
|
|
|
|
|
|
@register_model_view(EventRule, 'delete')
|
|
class EventRuleDeleteView(generic.ObjectDeleteView):
|
|
queryset = EventRule.objects.all()
|
|
|
|
|
|
class EventRuleBulkImportView(generic.BulkImportView):
|
|
queryset = EventRule.objects.all()
|
|
model_form = forms.EventRuleImportForm
|
|
|
|
|
|
class EventRuleBulkEditView(generic.BulkEditView):
|
|
queryset = EventRule.objects.all()
|
|
filterset = filtersets.EventRuleFilterSet
|
|
table = tables.EventRuleTable
|
|
form = forms.EventRuleBulkEditForm
|
|
|
|
|
|
class EventRuleBulkDeleteView(generic.BulkDeleteView):
|
|
queryset = EventRule.objects.all()
|
|
filterset = filtersets.EventRuleFilterSet
|
|
table = tables.EventRuleTable
|
|
|
|
|
|
#
|
|
# Tags
|
|
#
|
|
|
|
class TagListView(generic.ObjectListView):
|
|
queryset = Tag.objects.annotate(
|
|
items=count_related(TaggedItem, 'tag')
|
|
)
|
|
filterset = filtersets.TagFilterSet
|
|
filterset_form = forms.TagFilterForm
|
|
table = tables.TagTable
|
|
|
|
|
|
@register_model_view(Tag)
|
|
class TagView(generic.ObjectView):
|
|
queryset = Tag.objects.all()
|
|
|
|
def get_extra_context(self, request, instance):
|
|
tagged_items = TaggedItem.objects.filter(tag=instance)
|
|
taggeditem_table = tables.TaggedItemTable(
|
|
data=tagged_items,
|
|
orderable=False
|
|
)
|
|
taggeditem_table.configure(request)
|
|
|
|
object_types = [
|
|
{
|
|
'content_type': ContentType.objects.get(pk=ti['content_type']),
|
|
'item_count': ti['item_count']
|
|
} for ti in tagged_items.values('content_type').annotate(item_count=Count('pk'))
|
|
]
|
|
|
|
return {
|
|
'taggeditem_table': taggeditem_table,
|
|
'tagged_item_count': tagged_items.count(),
|
|
'object_types': object_types,
|
|
}
|
|
|
|
|
|
@register_model_view(Tag, 'edit')
|
|
class TagEditView(generic.ObjectEditView):
|
|
queryset = Tag.objects.all()
|
|
form = forms.TagForm
|
|
|
|
|
|
@register_model_view(Tag, 'delete')
|
|
class TagDeleteView(generic.ObjectDeleteView):
|
|
queryset = Tag.objects.all()
|
|
|
|
|
|
class TagBulkImportView(generic.BulkImportView):
|
|
queryset = Tag.objects.all()
|
|
model_form = forms.TagImportForm
|
|
|
|
|
|
class TagBulkEditView(generic.BulkEditView):
|
|
queryset = Tag.objects.annotate(
|
|
items=count_related(TaggedItem, 'tag')
|
|
)
|
|
table = tables.TagTable
|
|
form = forms.TagBulkEditForm
|
|
|
|
|
|
class TagBulkDeleteView(generic.BulkDeleteView):
|
|
queryset = Tag.objects.annotate(
|
|
items=count_related(TaggedItem, 'tag')
|
|
)
|
|
table = tables.TagTable
|
|
|
|
|
|
#
|
|
# Config contexts
|
|
#
|
|
|
|
class ConfigContextListView(generic.ObjectListView):
|
|
queryset = ConfigContext.objects.all()
|
|
filterset = filtersets.ConfigContextFilterSet
|
|
filterset_form = forms.ConfigContextFilterForm
|
|
table = tables.ConfigContextTable
|
|
template_name = 'extras/configcontext_list.html'
|
|
actions = {
|
|
'add': {'add'},
|
|
'bulk_edit': {'change'},
|
|
'bulk_delete': {'delete'},
|
|
'bulk_sync': {'sync'},
|
|
}
|
|
|
|
|
|
@register_model_view(ConfigContext)
|
|
class ConfigContextView(generic.ObjectView):
|
|
queryset = ConfigContext.objects.all()
|
|
|
|
def get_extra_context(self, request, instance):
|
|
# Gather assigned objects for parsing in the template
|
|
assigned_objects = (
|
|
('Regions', instance.regions.all),
|
|
('Site Groups', instance.site_groups.all),
|
|
('Sites', instance.sites.all),
|
|
('Locations', instance.locations.all),
|
|
('Device Types', instance.device_types.all),
|
|
('Roles', instance.roles.all),
|
|
('Platforms', instance.platforms.all),
|
|
('Cluster Types', instance.cluster_types.all),
|
|
('Cluster Groups', instance.cluster_groups.all),
|
|
('Clusters', instance.clusters.all),
|
|
('Tenant Groups', instance.tenant_groups.all),
|
|
('Tenants', instance.tenants.all),
|
|
('Tags', instance.tags.all),
|
|
)
|
|
|
|
# Determine user's preferred output format
|
|
if request.GET.get('format') in ['json', 'yaml']:
|
|
format = request.GET.get('format')
|
|
if request.user.is_authenticated:
|
|
request.user.config.set('data_format', format, commit=True)
|
|
elif request.user.is_authenticated:
|
|
format = request.user.config.get('data_format', 'json')
|
|
else:
|
|
format = 'json'
|
|
|
|
return {
|
|
'assigned_objects': assigned_objects,
|
|
'format': format,
|
|
}
|
|
|
|
|
|
@register_model_view(ConfigContext, 'edit')
|
|
class ConfigContextEditView(generic.ObjectEditView):
|
|
queryset = ConfigContext.objects.all()
|
|
form = forms.ConfigContextForm
|
|
|
|
|
|
class ConfigContextBulkEditView(generic.BulkEditView):
|
|
queryset = ConfigContext.objects.all()
|
|
filterset = filtersets.ConfigContextFilterSet
|
|
table = tables.ConfigContextTable
|
|
form = forms.ConfigContextBulkEditForm
|
|
|
|
|
|
@register_model_view(ConfigContext, 'delete')
|
|
class ConfigContextDeleteView(generic.ObjectDeleteView):
|
|
queryset = ConfigContext.objects.all()
|
|
|
|
|
|
class ConfigContextBulkDeleteView(generic.BulkDeleteView):
|
|
queryset = ConfigContext.objects.all()
|
|
filterset = filtersets.ConfigContextFilterSet
|
|
table = tables.ConfigContextTable
|
|
|
|
|
|
class ConfigContextBulkSyncDataView(generic.BulkSyncDataView):
|
|
queryset = ConfigContext.objects.all()
|
|
|
|
|
|
class ObjectConfigContextView(generic.ObjectView):
|
|
base_template = None
|
|
template_name = 'extras/object_configcontext.html'
|
|
|
|
def get_extra_context(self, request, instance):
|
|
source_contexts = ConfigContext.objects.restrict(request.user, 'view').get_for_object(instance)
|
|
|
|
# Determine user's preferred output format
|
|
if request.GET.get('format') in ['json', 'yaml']:
|
|
format = request.GET.get('format')
|
|
if request.user.is_authenticated:
|
|
request.user.config.set('data_format', format, commit=True)
|
|
elif request.user.is_authenticated:
|
|
format = request.user.config.get('data_format', 'json')
|
|
else:
|
|
format = 'json'
|
|
|
|
return {
|
|
'rendered_context': instance.get_config_context(),
|
|
'source_contexts': source_contexts,
|
|
'format': format,
|
|
'base_template': self.base_template,
|
|
}
|
|
|
|
|
|
#
|
|
# Config templates
|
|
#
|
|
|
|
class ConfigTemplateListView(generic.ObjectListView):
|
|
queryset = ConfigTemplate.objects.annotate(
|
|
device_count=count_related(Device, 'config_template'),
|
|
vm_count=count_related(VirtualMachine, 'config_template'),
|
|
role_count=count_related(DeviceRole, 'config_template'),
|
|
platform_count=count_related(Platform, 'config_template'),
|
|
)
|
|
filterset = filtersets.ConfigTemplateFilterSet
|
|
filterset_form = forms.ConfigTemplateFilterForm
|
|
table = tables.ConfigTemplateTable
|
|
template_name = 'extras/configtemplate_list.html'
|
|
actions = {
|
|
**DEFAULT_ACTION_PERMISSIONS,
|
|
'bulk_sync': {'sync'},
|
|
}
|
|
|
|
|
|
@register_model_view(ConfigTemplate)
|
|
class ConfigTemplateView(generic.ObjectView):
|
|
queryset = ConfigTemplate.objects.all()
|
|
|
|
|
|
@register_model_view(ConfigTemplate, 'edit')
|
|
class ConfigTemplateEditView(generic.ObjectEditView):
|
|
queryset = ConfigTemplate.objects.all()
|
|
form = forms.ConfigTemplateForm
|
|
|
|
|
|
@register_model_view(ConfigTemplate, 'delete')
|
|
class ConfigTemplateDeleteView(generic.ObjectDeleteView):
|
|
queryset = ConfigTemplate.objects.all()
|
|
|
|
|
|
class ConfigTemplateBulkImportView(generic.BulkImportView):
|
|
queryset = ConfigTemplate.objects.all()
|
|
model_form = forms.ConfigTemplateImportForm
|
|
|
|
|
|
class ConfigTemplateBulkEditView(generic.BulkEditView):
|
|
queryset = ConfigTemplate.objects.all()
|
|
filterset = filtersets.ConfigTemplateFilterSet
|
|
table = tables.ConfigTemplateTable
|
|
form = forms.ConfigTemplateBulkEditForm
|
|
|
|
|
|
class ConfigTemplateBulkDeleteView(generic.BulkDeleteView):
|
|
queryset = ConfigTemplate.objects.all()
|
|
filterset = filtersets.ConfigTemplateFilterSet
|
|
table = tables.ConfigTemplateTable
|
|
|
|
|
|
class ConfigTemplateBulkSyncDataView(generic.BulkSyncDataView):
|
|
queryset = ConfigTemplate.objects.all()
|
|
|
|
|
|
#
|
|
# Image attachments
|
|
#
|
|
|
|
class ImageAttachmentListView(generic.ObjectListView):
|
|
queryset = ImageAttachment.objects.all()
|
|
filterset = filtersets.ImageAttachmentFilterSet
|
|
filterset_form = forms.ImageAttachmentFilterForm
|
|
table = tables.ImageAttachmentTable
|
|
actions = {
|
|
'export': {'view'},
|
|
}
|
|
|
|
|
|
@register_model_view(ImageAttachment, 'edit')
|
|
class ImageAttachmentEditView(generic.ObjectEditView):
|
|
queryset = ImageAttachment.objects.all()
|
|
form = forms.ImageAttachmentForm
|
|
|
|
def alter_object(self, instance, request, args, kwargs):
|
|
if not instance.pk:
|
|
# Assign the parent object based on URL kwargs
|
|
object_type = get_object_or_404(ContentType, pk=request.GET.get('object_type'))
|
|
instance.parent = get_object_or_404(object_type.model_class(), pk=request.GET.get('object_id'))
|
|
return instance
|
|
|
|
def get_return_url(self, request, obj=None):
|
|
return obj.parent.get_absolute_url() if obj else super().get_return_url(request)
|
|
|
|
def get_extra_addanother_params(self, request):
|
|
return {
|
|
'object_type': request.GET.get('object_type'),
|
|
'object_id': request.GET.get('object_id'),
|
|
}
|
|
|
|
|
|
@register_model_view(ImageAttachment, 'delete')
|
|
class ImageAttachmentDeleteView(generic.ObjectDeleteView):
|
|
queryset = ImageAttachment.objects.all()
|
|
|
|
def get_return_url(self, request, obj=None):
|
|
return obj.parent.get_absolute_url() if obj else super().get_return_url(request)
|
|
|
|
|
|
#
|
|
# Journal entries
|
|
#
|
|
|
|
class JournalEntryListView(generic.ObjectListView):
|
|
queryset = JournalEntry.objects.all()
|
|
filterset = filtersets.JournalEntryFilterSet
|
|
filterset_form = forms.JournalEntryFilterForm
|
|
table = tables.JournalEntryTable
|
|
actions = {
|
|
'import': {'add'},
|
|
'export': {'view'},
|
|
'bulk_edit': {'change'},
|
|
'bulk_delete': {'delete'},
|
|
}
|
|
|
|
|
|
@register_model_view(JournalEntry)
|
|
class JournalEntryView(generic.ObjectView):
|
|
queryset = JournalEntry.objects.all()
|
|
|
|
|
|
@register_model_view(JournalEntry, 'edit')
|
|
class JournalEntryEditView(generic.ObjectEditView):
|
|
queryset = JournalEntry.objects.all()
|
|
form = forms.JournalEntryForm
|
|
|
|
def alter_object(self, obj, request, args, kwargs):
|
|
if not obj.pk:
|
|
obj.created_by = request.user
|
|
return obj
|
|
|
|
def get_return_url(self, request, instance):
|
|
if not instance.assigned_object:
|
|
return reverse('extras:journalentry_list')
|
|
obj = instance.assigned_object
|
|
viewname = get_viewname(obj, 'journal')
|
|
return reverse(viewname, kwargs={'pk': obj.pk})
|
|
|
|
|
|
@register_model_view(JournalEntry, 'delete')
|
|
class JournalEntryDeleteView(generic.ObjectDeleteView):
|
|
queryset = JournalEntry.objects.all()
|
|
|
|
def get_return_url(self, request, instance):
|
|
obj = instance.assigned_object
|
|
viewname = get_viewname(obj, 'journal')
|
|
return reverse(viewname, kwargs={'pk': obj.pk})
|
|
|
|
|
|
class JournalEntryBulkEditView(generic.BulkEditView):
|
|
queryset = JournalEntry.objects.all()
|
|
filterset = filtersets.JournalEntryFilterSet
|
|
table = tables.JournalEntryTable
|
|
form = forms.JournalEntryBulkEditForm
|
|
|
|
|
|
class JournalEntryBulkDeleteView(generic.BulkDeleteView):
|
|
queryset = JournalEntry.objects.all()
|
|
filterset = filtersets.JournalEntryFilterSet
|
|
table = tables.JournalEntryTable
|
|
|
|
|
|
class JournalEntryBulkImportView(generic.BulkImportView):
|
|
queryset = JournalEntry.objects.all()
|
|
model_form = forms.JournalEntryImportForm
|
|
|
|
|
|
#
|
|
# Dashboard & widgets
|
|
#
|
|
|
|
class DashboardResetView(LoginRequiredMixin, View):
|
|
template_name = 'extras/dashboard/reset.html'
|
|
|
|
def get(self, request):
|
|
get_object_or_404(Dashboard.objects.all(), user=request.user)
|
|
form = ConfirmationForm()
|
|
|
|
return render(request, self.template_name, {
|
|
'form': form,
|
|
'return_url': reverse('home'),
|
|
})
|
|
|
|
def post(self, request):
|
|
dashboard = get_object_or_404(Dashboard.objects.all(), user=request.user)
|
|
form = ConfirmationForm(request.POST)
|
|
|
|
if form.is_valid():
|
|
dashboard.delete()
|
|
messages.success(request, _("Your dashboard has been reset."))
|
|
return redirect(reverse('home'))
|
|
|
|
return render(request, self.template_name, {
|
|
'form': form,
|
|
'return_url': reverse('home'),
|
|
})
|
|
|
|
|
|
class DashboardWidgetAddView(LoginRequiredMixin, View):
|
|
template_name = 'extras/dashboard/widget_add.html'
|
|
|
|
def get(self, request):
|
|
if not request.htmx:
|
|
return redirect('home')
|
|
|
|
initial = request.GET or {
|
|
'widget_class': 'extras.NoteWidget',
|
|
}
|
|
widget_form = DashboardWidgetAddForm(initial=initial)
|
|
widget_name = get_field_value(widget_form, 'widget_class')
|
|
widget_class = get_widget_class(widget_name)
|
|
config_form = widget_class.ConfigForm(initial=widget_class.default_config, prefix='config')
|
|
|
|
return render(request, self.template_name, {
|
|
'widget_class': widget_class,
|
|
'widget_form': widget_form,
|
|
'config_form': config_form,
|
|
})
|
|
|
|
def post(self, request):
|
|
widget_form = DashboardWidgetAddForm(request.POST)
|
|
config_form = None
|
|
widget_class = None
|
|
|
|
if widget_form.is_valid():
|
|
widget_class = get_widget_class(widget_form.cleaned_data['widget_class'])
|
|
config_form = widget_class.ConfigForm(request.POST, prefix='config')
|
|
|
|
if config_form.is_valid():
|
|
data = widget_form.cleaned_data
|
|
data.pop('widget_class')
|
|
data['config'] = config_form.cleaned_data
|
|
widget = widget_class(**data)
|
|
request.user.dashboard.add_widget(widget)
|
|
request.user.dashboard.save()
|
|
messages.success(request, _('Added widget: ') + str(widget.id))
|
|
|
|
return HttpResponse(headers={
|
|
'HX-Redirect': reverse('home'),
|
|
})
|
|
|
|
return render(request, self.template_name, {
|
|
'widget_class': widget_class,
|
|
'widget_form': widget_form,
|
|
'config_form': config_form,
|
|
})
|
|
|
|
|
|
class DashboardWidgetConfigView(LoginRequiredMixin, View):
|
|
template_name = 'extras/dashboard/widget_config.html'
|
|
|
|
def get(self, request, id):
|
|
if not request.htmx:
|
|
return redirect('home')
|
|
|
|
widget = request.user.dashboard.get_widget(id)
|
|
widget_form = DashboardWidgetForm(initial=widget.form_data)
|
|
config_form = widget.ConfigForm(initial=widget.form_data.get('config'), prefix='config')
|
|
|
|
return render(request, self.template_name, {
|
|
'widget_class': widget.__class__,
|
|
'widget_form': widget_form,
|
|
'config_form': config_form,
|
|
'form_url': reverse('extras:dashboardwidget_config', kwargs={'id': id})
|
|
})
|
|
|
|
def post(self, request, id):
|
|
widget = request.user.dashboard.get_widget(id)
|
|
widget_form = DashboardWidgetForm(request.POST)
|
|
config_form = widget.ConfigForm(request.POST, prefix='config')
|
|
|
|
if widget_form.is_valid() and config_form.is_valid():
|
|
data = widget_form.cleaned_data
|
|
data['config'] = config_form.cleaned_data
|
|
request.user.dashboard.config[str(id)].update(data)
|
|
request.user.dashboard.save()
|
|
messages.success(request, _('Updated widget: ') + str(widget.id))
|
|
|
|
return HttpResponse(headers={
|
|
'HX-Redirect': reverse('home'),
|
|
})
|
|
|
|
return render(request, self.template_name, {
|
|
'widget_form': widget_form,
|
|
'config_form': config_form,
|
|
'form_url': reverse('extras:dashboardwidget_config', kwargs={'id': id})
|
|
})
|
|
|
|
|
|
class DashboardWidgetDeleteView(LoginRequiredMixin, View):
|
|
template_name = 'generic/object_delete.html'
|
|
|
|
def get(self, request, id):
|
|
if not request.htmx:
|
|
return redirect('home')
|
|
|
|
widget = request.user.dashboard.get_widget(id)
|
|
form = ConfirmationForm(initial=request.GET)
|
|
|
|
return render(request, 'htmx/delete_form.html', {
|
|
'object_type': widget.__class__.__name__,
|
|
'object': widget,
|
|
'form': form,
|
|
'form_url': reverse('extras:dashboardwidget_delete', kwargs={'id': id})
|
|
})
|
|
|
|
def post(self, request, id):
|
|
form = ConfirmationForm(request.POST)
|
|
|
|
if form.is_valid():
|
|
request.user.dashboard.delete_widget(id)
|
|
request.user.dashboard.save()
|
|
messages.success(request, _('Deleted widget: ') + str(id))
|
|
else:
|
|
messages.error(request, _('Error deleting widget: ') + str(form.errors[0]))
|
|
|
|
return redirect(reverse('home'))
|
|
|
|
|
|
#
|
|
# Scripts
|
|
#
|
|
|
|
@register_model_view(ScriptModule, 'edit')
|
|
class ScriptModuleCreateView(generic.ObjectEditView):
|
|
queryset = ScriptModule.objects.all()
|
|
form = ManagedFileForm
|
|
|
|
def alter_object(self, obj, *args, **kwargs):
|
|
obj.file_root = ManagedFileRootPathChoices.SCRIPTS
|
|
return obj
|
|
|
|
|
|
@register_model_view(ScriptModule, 'delete')
|
|
class ScriptModuleDeleteView(generic.ObjectDeleteView):
|
|
queryset = ScriptModule.objects.all()
|
|
default_return_url = 'extras:script_list'
|
|
|
|
|
|
class ScriptListView(ContentTypePermissionRequiredMixin, View):
|
|
|
|
def get_required_permission(self):
|
|
return 'extras.view_script'
|
|
|
|
def get(self, request):
|
|
script_modules = ScriptModule.objects.restrict(request.user).prefetch_related(
|
|
'data_source', 'data_file', 'jobs'
|
|
)
|
|
|
|
return render(request, 'extras/script_list.html', {
|
|
'model': ScriptModule,
|
|
'script_modules': script_modules,
|
|
})
|
|
|
|
|
|
class BaseScriptView(generic.ObjectView):
|
|
queryset = Script.objects.all()
|
|
|
|
def _get_script_class(self, script):
|
|
"""
|
|
Return an instance of the Script's Python class
|
|
"""
|
|
if script_class := script.python_class:
|
|
return script_class()
|
|
|
|
|
|
class ScriptView(BaseScriptView):
|
|
|
|
def get(self, request, **kwargs):
|
|
script = self.get_object(**kwargs)
|
|
script_class = self._get_script_class(script)
|
|
if not script_class:
|
|
return render(request, 'extras/script.html', {
|
|
'script': script,
|
|
})
|
|
|
|
form = script_class.as_form(initial=normalize_querydict(request.GET))
|
|
|
|
return render(request, 'extras/script.html', {
|
|
'script': script,
|
|
'script_class': script_class,
|
|
'form': form,
|
|
'job_count': script.jobs.count(),
|
|
})
|
|
|
|
def post(self, request, **kwargs):
|
|
script = self.get_object(**kwargs)
|
|
|
|
if not request.user.has_perm('extras.run_script', obj=script):
|
|
return HttpResponseForbidden()
|
|
|
|
script_class = self._get_script_class(script)
|
|
if not script_class:
|
|
return render(request, 'extras/script.html', {
|
|
'script': script,
|
|
})
|
|
|
|
form = script_class.as_form(request.POST, request.FILES)
|
|
|
|
# Allow execution only if RQ worker process is running
|
|
if not get_workers_for_queue('default'):
|
|
messages.error(request, _("Unable to run script: RQ worker process not running."))
|
|
elif form.is_valid():
|
|
ScriptJob = import_string("extras.jobs.ScriptJob")
|
|
job = ScriptJob.enqueue(
|
|
instance=script,
|
|
user=request.user,
|
|
schedule_at=form.cleaned_data.pop('_schedule_at'),
|
|
interval=form.cleaned_data.pop('_interval'),
|
|
data=form.cleaned_data,
|
|
request=copy_safe_request(request),
|
|
job_timeout=script.python_class.job_timeout,
|
|
commit=form.cleaned_data.pop('_commit')
|
|
)
|
|
|
|
return redirect('extras:script_result', job_pk=job.pk)
|
|
|
|
return render(request, 'extras/script.html', {
|
|
'script': script,
|
|
'script_class': script.python_class(),
|
|
'form': form,
|
|
'job_count': script.jobs.count(),
|
|
})
|
|
|
|
|
|
class ScriptSourceView(BaseScriptView):
|
|
queryset = Script.objects.all()
|
|
|
|
def get(self, request, **kwargs):
|
|
script = self.get_object(**kwargs)
|
|
script_class = self._get_script_class(script)
|
|
|
|
return render(request, 'extras/script/source.html', {
|
|
'script': script,
|
|
'script_class': script_class,
|
|
'job_count': script.jobs.count(),
|
|
'tab': 'source',
|
|
})
|
|
|
|
|
|
class ScriptJobsView(BaseScriptView):
|
|
queryset = Script.objects.all()
|
|
|
|
def get(self, request, **kwargs):
|
|
script = self.get_object(**kwargs)
|
|
|
|
jobs_table = JobTable(
|
|
data=script.jobs.all(),
|
|
orderable=False,
|
|
user=request.user
|
|
)
|
|
jobs_table.configure(request)
|
|
|
|
return render(request, 'extras/script/jobs.html', {
|
|
'script': script,
|
|
'table': jobs_table,
|
|
'job_count': script.jobs.count(),
|
|
'tab': 'jobs',
|
|
})
|
|
|
|
|
|
class LegacyScriptRedirectView(ContentTypePermissionRequiredMixin, View):
|
|
"""
|
|
Redirect legacy (pre-v4.0) script URLs. Examples:
|
|
/extras/scripts/<module>/<name>/ --> /extras/scripts/<id>/
|
|
/extras/scripts/<module>/<name>/source/ --> /extras/scripts/<id>/source/
|
|
/extras/scripts/<module>/<name>/jobs/ --> /extras/scripts/<id>/jobs/
|
|
"""
|
|
def get_required_permission(self):
|
|
return 'extras.view_script'
|
|
|
|
def get(self, request, module, name, path=''):
|
|
module = get_object_or_404(ScriptModule.objects.restrict(request.user), file_path__regex=f"^{module}\\.")
|
|
script = get_object_or_404(Script.objects.all(), module=module, name=name)
|
|
|
|
url = reverse('extras:script', kwargs={'pk': script.pk})
|
|
|
|
return redirect(f'{url}{path}')
|
|
|
|
|
|
class ScriptResultView(TableMixin, generic.ObjectView):
|
|
queryset = Job.objects.all()
|
|
|
|
def get_required_permission(self):
|
|
return 'extras.view_script'
|
|
|
|
def get_table(self, job, request, bulk_actions=True):
|
|
data = []
|
|
tests = None
|
|
table = None
|
|
index = 0
|
|
|
|
log_threshold = LOG_LEVEL_RANK.get(request.GET.get('log_threshold', LogLevelChoices.LOG_DEFAULT))
|
|
if job.data:
|
|
|
|
if 'log' in job.data:
|
|
if 'tests' in job.data:
|
|
tests = job.data['tests']
|
|
|
|
for log in job.data['log']:
|
|
log_level = LOG_LEVEL_RANK.get(log.get('status'), LogLevelChoices.LOG_DEFAULT)
|
|
if log_level >= log_threshold:
|
|
index += 1
|
|
result = {
|
|
'index': index,
|
|
'time': log.get('time'),
|
|
'status': log.get('status'),
|
|
'message': log.get('message'),
|
|
'object': log.get('obj'),
|
|
'url': log.get('url'),
|
|
}
|
|
data.append(result)
|
|
|
|
table = ScriptResultsTable(data, user=request.user)
|
|
table.configure(request)
|
|
else:
|
|
# for legacy reports
|
|
tests = job.data
|
|
|
|
if tests:
|
|
for method, test_data in tests.items():
|
|
if 'log' in test_data:
|
|
for time, status, obj, url, message in test_data['log']:
|
|
log_level = LOG_LEVEL_RANK.get(status, LogLevelChoices.LOG_DEFAULT)
|
|
if log_level >= log_threshold:
|
|
index += 1
|
|
result = {
|
|
'index': index,
|
|
'method': method,
|
|
'time': time,
|
|
'status': status,
|
|
'object': obj,
|
|
'url': url,
|
|
'message': message,
|
|
}
|
|
data.append(result)
|
|
|
|
table = ReportResultsTable(data, user=request.user)
|
|
table.configure(request)
|
|
|
|
return table
|
|
|
|
def get(self, request, **kwargs):
|
|
table = None
|
|
job = get_object_or_404(Job.objects.all(), pk=kwargs.get('job_pk'))
|
|
|
|
if job.completed:
|
|
table = self.get_table(job, request, bulk_actions=False)
|
|
|
|
context = {
|
|
'script': job.object,
|
|
'job': job,
|
|
'table': table,
|
|
'log_levels': dict(LogLevelChoices),
|
|
'log_threshold': request.GET.get('log_threshold', LogLevelChoices.LOG_DEFAULT)
|
|
}
|
|
|
|
if job.data and 'log' in job.data:
|
|
# Script
|
|
context['tests'] = job.data.get('tests', {})
|
|
elif job.data:
|
|
# Legacy Report
|
|
context['tests'] = {
|
|
name: data for name, data in job.data.items()
|
|
if name.startswith('test_')
|
|
}
|
|
|
|
# If this is an HTMX request, return only the result HTML
|
|
if htmx_partial(request):
|
|
response = render(request, 'extras/htmx/script_result.html', context)
|
|
if job.completed or not job.started:
|
|
response.status_code = 286
|
|
return response
|
|
|
|
return render(request, 'extras/script_result.html', context)
|
|
|
|
|
|
#
|
|
# Markdown
|
|
#
|
|
|
|
class RenderMarkdownView(LoginRequiredMixin, View):
|
|
|
|
def post(self, request):
|
|
form = forms.RenderMarkdownForm(request.POST)
|
|
if not form.is_valid():
|
|
HttpResponseBadRequest()
|
|
rendered = render_markdown(form.cleaned_data['text'])
|
|
|
|
return HttpResponse(rendered)
|