#20923: Initial work on migrating the core app

This commit is contained in:
Jeremy Stretch
2026-03-25 12:57:10 -04:00
parent 981f31304d
commit bf27ff9593
25 changed files with 314 additions and 428 deletions

View File

91
netbox/core/ui/panels.py Normal file
View File

@@ -0,0 +1,91 @@
from django.utils.translation import gettext_lazy as _
from netbox.ui import attrs, panels
class DataSourcePanel(panels.ObjectAttributesPanel):
title = _('Data Source')
name = attrs.TextAttr('name')
type = attrs.ChoiceAttr('type')
enabled = attrs.BooleanAttr('enabled')
status = attrs.ChoiceAttr('status')
sync_interval = attrs.ChoiceAttr('sync_interval', label=_('Sync interval'))
last_synced = attrs.DateTimeAttr('last_synced', label=_('Last synced'))
description = attrs.TextAttr('description')
source_url = attrs.TemplatedAttr(
'source_url',
label=_('URL'),
template_name='core/datasource/attrs/source_url.html',
)
ignore_rules = attrs.TemplatedAttr(
'ignore_rules',
label=_('Ignore rules'),
template_name='core/datasource/attrs/ignore_rules.html',
)
class DataSourceBackendPanel(panels.ObjectPanel):
template_name = 'core/panels/datasource_backend.html'
title = _('Backend')
class DataFilePanel(panels.ObjectAttributesPanel):
title = _('Data File')
source = attrs.RelatedObjectAttr('source', linkify=True)
path = attrs.TextAttr('path', style='font-monospace', copy_button=True)
last_updated = attrs.DateTimeAttr('last_updated')
size = attrs.TemplatedAttr('size', template_name='core/datafile/attrs/size.html')
hash = attrs.TextAttr('hash', label=_('SHA256 hash'), style='font-monospace', copy_button=True)
class DataFileContentPanel(panels.ObjectPanel):
template_name = 'core/panels/datafile_content.html'
title = _('Content')
class JobPanel(panels.ObjectAttributesPanel):
title = _('Job')
object_type = attrs.TemplatedAttr(
'object_type',
label=_('Object type'),
template_name='core/job/attrs/object_type.html',
)
name = attrs.TextAttr('name')
status = attrs.ChoiceAttr('status')
error = attrs.TextAttr('error')
user = attrs.TextAttr('user', label=_('Created by'))
class JobSchedulingPanel(panels.ObjectAttributesPanel):
title = _('Scheduling')
created = attrs.DateTimeAttr('created')
scheduled = attrs.TemplatedAttr('scheduled', template_name='core/job/attrs/scheduled.html')
started = attrs.DateTimeAttr('started')
completed = attrs.DateTimeAttr('completed')
queue = attrs.TextAttr('queue_name', label=_('Queue'))
class ObjectChangePanel(panels.ObjectAttributesPanel):
title = _('Change')
time = attrs.DateTimeAttr('time')
user = attrs.TemplatedAttr(
'user_name',
label=_('User'),
template_name='core/objectchange/attrs/user.html',
)
action = attrs.ChoiceAttr('action')
changed_object_type = attrs.TextAttr(
'changed_object_type',
label=_('Object type'),
)
changed_object = attrs.TemplatedAttr(
'object_repr',
label=_('Object'),
template_name='core/objectchange/attrs/changed_object.html',
)
message = attrs.TextAttr('message')
request_id = attrs.TemplatedAttr(
'request_id',
label=_('Request ID'),
template_name='core/objectchange/attrs/request_id.html',
)

View File

@@ -23,9 +23,20 @@ from rq.worker import Worker
from rq.worker_registration import clean_worker_registry
from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs_from_status, requeue_rq_job, stop_rq_job
from extras.ui.panels import CustomFieldsPanel, TagsPanel
from netbox.config import PARAMS, get_config
from netbox.object_actions import AddObject, BulkDelete, BulkExport, DeleteObject
from netbox.plugins.utils import get_installed_plugins
from netbox.ui import layout
from netbox.ui.panels import (
CommentsPanel,
ContextTablePanel,
JSONPanel,
ObjectsTablePanel,
PluginContentPanel,
RelatedObjectsPanel,
TemplatePanel,
)
from netbox.views import generic
from netbox.views.generic.base import BaseObjectView
from netbox.views.generic.mixins import TableMixin
@@ -48,6 +59,7 @@ from .jobs import SyncDataSourceJob
from .models import *
from .plugins import get_catalog_plugins, get_local_plugins
from .tables import CatalogPluginTable, JobLogEntryTable, PluginVersionTable
from .ui import panels
#
# Data sources
@@ -67,6 +79,24 @@ class DataSourceListView(generic.ObjectListView):
@register_model_view(DataSource)
class DataSourceView(GetRelatedModelsMixin, generic.ObjectView):
queryset = DataSource.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.DataSourcePanel(),
TagsPanel(),
CommentsPanel(),
],
right_panels=[
panels.DataSourceBackendPanel(),
RelatedObjectsPanel(),
CustomFieldsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
model='core.DataFile',
filters={'source_id': lambda ctx: ctx['object'].pk},
),
],
)
def get_extra_context(self, request, instance):
return {
@@ -157,6 +187,12 @@ class DataFileListView(generic.ObjectListView):
class DataFileView(generic.ObjectView):
queryset = DataFile.objects.all()
actions = (DeleteObject,)
layout = layout.SimpleLayout(
left_panels=[
panels.DataFilePanel(),
panels.DataFileContentPanel(),
],
)
@register_model_view(DataFile, 'delete')
@@ -188,6 +224,17 @@ class JobListView(generic.ObjectListView):
class JobView(generic.ObjectView):
queryset = Job.objects.all()
actions = (DeleteObject,)
layout = layout.SimpleLayout(
left_panels=[
panels.JobPanel(),
],
right_panels=[
panels.JobSchedulingPanel(),
],
bottom_panels=[
JSONPanel('data', title=_('Data')),
],
)
@register_model_view(Job, 'log')
@@ -200,6 +247,13 @@ class JobLogView(generic.ObjectView):
badge=lambda obj: len(obj.log_entries),
weight=500,
)
layout = layout.Layout(
layout.Row(
layout.Column(
ContextTablePanel('table', title=_('Log Entries')),
),
),
)
def get_extra_context(self, request, instance):
table = JobLogEntryTable(instance.log_entries)
@@ -241,6 +295,26 @@ class ObjectChangeListView(generic.ObjectListView):
@register_model_view(ObjectChange)
class ObjectChangeView(generic.ObjectView):
queryset = None
layout = layout.Layout(
layout.Row(
layout.Column(panels.ObjectChangePanel()),
layout.Column(TemplatePanel('core/panels/objectchange_difference.html')),
),
layout.Row(
layout.Column(TemplatePanel('core/panels/objectchange_prechange.html')),
layout.Column(TemplatePanel('core/panels/objectchange_postchange.html')),
),
layout.Row(
layout.Column(PluginContentPanel('left_page')),
layout.Column(PluginContentPanel('right_page')),
),
layout.Row(
layout.Column(
TemplatePanel('core/panels/objectchange_related.html'),
PluginContentPanel('full_width_page'),
),
),
)
def get_queryset(self, request):
return ObjectChange.objects.valid_models()
@@ -312,6 +386,14 @@ class ConfigRevisionListView(generic.ObjectListView):
@register_model_view(ConfigRevision)
class ConfigRevisionView(generic.ObjectView):
queryset = ConfigRevision.objects.all()
layout = layout.Layout(
layout.Row(
layout.Column(
TemplatePanel('core/panels/configrevision_data.html'),
TemplatePanel('core/panels/configrevision_comment.html'),
),
),
)
def get_extra_context(self, request, instance):
"""

View File

@@ -1,10 +1,7 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
{% load perms %}
{% load plugins %}
{% load static %}
{% load i18n %}
{% block breadcrumbs %}
@@ -27,22 +24,3 @@
</div>
{% endif %}
{% endblock subtitle %}
{% block content %}
<div class="row">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">{% trans "Configuration Data" %}</h2>
{% include 'core/inc/config_data.html' %}
</div>
<div class="card">
<h2 class="card-header">{% trans "Comment" %}</h2>
<div class="card-body">
{{ object.comment|placeholder }}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,62 +1,7 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
{% load perms %}
{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
{{ block.super }}
<li class="breadcrumb-item"><a href="{% url 'core:datafile_list' %}?source_id={{ object.source.pk }}">{{ object.source }}</a></li>
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col">
<div class="card">
<h2 class="card-header">{% trans "Data File" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Source" %}</th>
<td>{{ object.source|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Path" %}</th>
<td>
<span class="font-monospace" id="datafile_path">{{ object.path }}</span>
{% copy_content "datafile_path" %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Last Updated" %}</th>
<td>{{ object.last_updated }}</td>
</tr>
<tr>
<th scope="row">{% trans "Size" %}</th>
<td>{{ object.size }} {% trans "bytes" %}</td>
</tr>
<tr>
<th scope="row">{% trans "SHA256 Hash" %}</th>
<td>
<span class="font-monospace" id="datafile_hash">{{ object.hash }}</span>
{% copy_content "datafile_hash" %}
</td>
</tr>
</table>
</div>
<div class="card">
<h2 class="card-header">{% trans "Content" %}</h2>
<div class="card-body">
<pre>{{ object.data_as_string }}</pre>
</div>
</div>
{% plugin_left_page object %}
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1 @@
{% load i18n %}{{ value }} {% trans "bytes" %}

View File

@@ -1,8 +1,4 @@
{% extends 'generic/object.html' %}
{% load static %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block extra_controls %}
@@ -23,102 +19,3 @@
{% endif %}
{% endif %}
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Data Source" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Type" %}</th>
<td>{{ object.get_type_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Enabled" %}</th>
<td>{% checkmark object.enabled %}</td>
</tr>
<tr>
<th scope="row">{% trans "Status" %}</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr>
<tr>
<th scope="row">{% trans "Sync interval" %}</th>
<td>{{ object.get_sync_interval_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Last synced" %}</th>
<td>{{ object.last_synced|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "URL" %}</th>
<td>
{% if not object.type.is_local %}
<a href="{{ object.source_url }}">{{ object.source_url }}</a>
{% else %}
{{ object.source_url }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Ignore rules" %}</th>
<td>
{% if object.ignore_rules %}
<pre>{{ object.ignore_rules }}</pre>
{% else %}
{{ ''|placeholder }}
{% endif %}</td>
</tr>
</table>
</div>
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Backend" %}</h2>
{% with backend=object.backend_class %}
<table class="table table-hover attr-table">
{% for name, field in backend.parameters.items %}
<tr>
<th scope="row">{{ field.label }}</th>
{% if name in backend.sensitive_parameters %}
<td>********</td>
{% else %}
<td>{{ object.parameters|get_key:name|placeholder }}</td>
{% endif %}
</tr>
{% empty %}
<tr>
<td colspan="2" class="text-muted">
{% trans "No parameters defined" %}
</td>
</tr>
{% endfor %}
</table>
{% endwith %}
</div>
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">{% trans "Files" %}</h2>
{% htmx_table 'core:datafile_list' source_id=object.pk %}
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1 @@
<pre>{{ value }}</pre>

View File

@@ -0,0 +1 @@
{% if not object.type.is_local %}<a href="{{ value }}">{{ value }}</a>{% else %}{{ value }}{% endif %}

View File

@@ -1,78 +1 @@
{% extends 'core/job/base.html' %}
{% load i18n %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Job" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Object Type" %}</th>
<td>
<a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ object.object_type }}</a>
</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Status" %}</th>
<td>{% badge object.get_status_display object.get_status_color %}</td>
</tr>
{% if object.error %}
<tr>
<th scope="row">{% trans "Error" %}</th>
<td>{{ object.error }}</td>
</tr>
{% endif %}
<tr>
<th scope="row">{% trans "Created By" %}</th>
<td>{{ object.user|placeholder }}</td>
</tr>
</table>
</div>
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Scheduling" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Created" %}</th>
<td>{{ object.created|isodatetime }}</td>
</tr>
<tr>
<th scope="row">{% trans "Scheduled" %}</th>
<td>
{{ object.scheduled|isodatetime|placeholder }}
{% if object.interval %}
({% blocktrans with interval=object.interval %}every {{ interval }} minutes{% endblocktrans %})
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Started" %}</th>
<td>{{ object.started|isodatetime|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Completed" %}</th>
<td>{{ object.completed|isodatetime|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Queue" %}</th>
<td>{{ object.queue_name|placeholder }}</td>
</tr>
</table>
</div>
</div>
</div>
<div class="row">
<div class="col col-12">
<div class="card">
<h2 class="card-header">{% trans "Data" %}</h2>
<pre class="card-body m-0">{{ object.data|json }}</pre>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1 @@
<a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ value }}</a>

View File

@@ -0,0 +1,3 @@
{% load helpers %}
{% load i18n %}
{{ value|isodatetime }}{% if object.interval %} ({% blocktrans with interval=object.interval %}every {{ interval }} minutes{% endblocktrans %}){% endif %}

View File

@@ -1,12 +1 @@
{% extends 'core/job/base.html' %}
{% load render_table from django_tables2 %}
{% block content %}
<div class="row mb-3">
<div class="col">
<div class="card">
{% render_table table %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block title %}{{ object }}{% endblock %}
@@ -21,161 +19,3 @@
{# ObjectChange does not support the default add/edit/delete controls #}
{% block control-buttons %}{% endblock %}
{% block subtitle %}{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-5">
<div class="card">
<h2 class="card-header">{% trans "Change" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Time" %}</th>
<td>{{ object.time|isodatetime }}</td>
</tr>
<tr>
<th scope="row">{% trans "User" %}</th>
<td>
{% if object.user.get_full_name %}
{{ object.user.get_full_name }} ({{ object.user_name }})
{% else %}
{{ object.user_name }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Action" %}</th>
<td>
{{ object.get_action_display }}
</td>
</tr>
<tr>
<th scope="row">{% trans "Object Type" %}</th>
<td>
{{ object.changed_object_type }}
</td>
</tr>
<tr>
<th scope="row">{% trans "Object" %}</th>
<td>
{% if object.changed_object and object.changed_object.get_absolute_url %}
{{ object.changed_object|linkify }}
{% else %}
{{ object.object_repr }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Message" %}</th>
<td>
{{ object.message|placeholder }}
</td>
</tr>
<tr>
<th scope="row">{% trans "Request ID" %}</th>
<td>
<a href="{% url 'core:objectchange_list' %}?request_id={{ object.request_id }}">{{ object.request_id }}</a>
</td>
</tr>
</table>
</div>
</div>
<div class="col col-12 col-md-7">
<div class="card">
<h2 class="card-header d-flex justify-content-between">
{% trans "Difference" %}
<div class="btn-group btn-group-sm d-print-none">
<a {% if prev_change %}href="{% url 'core:objectchange' pk=prev_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
<i class="mdi mdi-chevron-left" aria-hidden="true"></i> {% trans "Previous" %}
</a>
<a {% if next_change %}href="{% url 'core:objectchange' pk=next_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
{% trans "Next" %} <i class="mdi mdi-chevron-right" aria-hidden="true"></i>
</a>
</div>
</h2>
<div class="card-body">
{% if diff_added == diff_removed %}
<span class="text-muted" style="margin-left: 10px;">
{% if object.action == 'create' %}
{% trans "Object Created" %}
{% elif object.action == 'delete' %}
{% trans "Object Deleted" %}
{% else %}
{% trans "No Changes" %}
{% endif %}
</span>
{% else %}
<pre class="change-diff change-removed">{{ diff_removed|json }}</pre>
<pre class="change-diff change-added">{{ diff_added|json }}</pre>
{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Pre-Change Data" %}</h2>
<div class="card-body">
{% if object.prechange_data %}
{% spaceless %}
<pre class="change-data">
{% for k, v in object.prechange_data_clean.items %}
<span{% if k in diff_removed %} class="removed"{% endif %}>{{ k }}: {{ v|json }}</span>
{% endfor %}
</pre>
{% endspaceless %}
{% elif non_atomic_change %}
{% trans "Warning: Comparing non-atomic change to previous change record" %} (<a href="{% url 'core:objectchange' pk=prev_change.pk %}">{{ prev_change.pk }}</a>)
{% else %}
<span class="text-muted">{% trans "None" %}</span>
{% endif %}
</div>
</div>
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Post-Change Data" %}</h2>
<div class="card-body">
{% if object.postchange_data %}
{% spaceless %}
<pre class="change-data">
{% for k, v in object.postchange_data_clean.items %}
<span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|json }}</span>
{% endfor %}
</pre>
{% endspaceless %}
{% else %}
<span class="text-muted">{% trans "None" %}</span>
{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col col-12 col-md-6">
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% include 'inc/panel_table.html' with table=related_changes_table heading='Related Changes' panel_class='default' %}
{% if related_changes_count > related_changes_table.rows|length %}
<div class="float-end">
<a href="{% url 'core:objectchange_list' %}?request_id={{ object.request_id }}" class="btn btn-primary">
{% blocktrans trimmed with count=related_changes_count|add:"1" %}
See All {{ count }} Changes
{% endblocktrans %}
</a>
</div>
{% endif %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,2 @@
{% load helpers %}
{% if object.changed_object and object.changed_object.get_absolute_url %}{{ object.changed_object|linkify }}{% else %}{{ value }}{% endif %}

View File

@@ -0,0 +1 @@
<a href="{% url 'core:objectchange_list' %}?request_id={{ value }}">{{ value }}</a>

View File

@@ -0,0 +1 @@
{% if object.user and object.user.get_full_name %}{{ object.user.get_full_name }} ({{ value }}){% else %}{{ value }}{% endif %}

View File

@@ -0,0 +1,11 @@
{% load i18n %}
<div class="card">
<h2 class="card-header">{% trans "Comment" %}</h2>
<div class="card-body">
{% if object.comment %}
{{ object.comment }}
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,5 @@
{% load i18n %}
<div class="card">
<h2 class="card-header">{% trans "Configuration Data" %}</h2>
{% include 'core/inc/config_data.html' %}
</div>

View File

@@ -0,0 +1,8 @@
{% extends "ui/panels/_base.html" %}
{% load i18n %}
{% block panel_content %}
<div class="card-body">
<pre>{{ object.data_as_string }}</pre>
</div>
{% endblock panel_content %}

View File

@@ -0,0 +1,26 @@
{% extends "ui/panels/_base.html" %}
{% load helpers %}
{% load i18n %}
{% block panel_content %}
{% with backend=object.backend_class %}
<table class="table table-hover attr-table">
{% for name, field in backend.parameters.items %}
<tr>
<th scope="row">{{ field.label }}</th>
{% if name in backend.sensitive_parameters %}
<td>********</td>
{% else %}
<td>{{ object.parameters|get_key:name|placeholder }}</td>
{% endif %}
</tr>
{% empty %}
<tr>
<td colspan="2" class="text-muted">
{% trans "No parameters defined" %}
</td>
</tr>
{% endfor %}
</table>
{% endwith %}
{% endblock panel_content %}

View File

@@ -0,0 +1,31 @@
{% load helpers %}
{% load i18n %}
<div class="card">
<h2 class="card-header d-flex justify-content-between">
{% trans "Difference" %}
<div class="btn-group btn-group-sm d-print-none">
<a {% if prev_change %}href="{% url 'core:objectchange' pk=prev_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
<i class="mdi mdi-chevron-left" aria-hidden="true"></i> {% trans "Previous" %}
</a>
<a {% if next_change %}href="{% url 'core:objectchange' pk=next_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
{% trans "Next" %} <i class="mdi mdi-chevron-right" aria-hidden="true"></i>
</a>
</div>
</h2>
<div class="card-body">
{% if diff_added == diff_removed %}
<span class="text-muted" style="margin-left: 10px;">
{% if object.action == 'create' %}
{% trans "Object Created" %}
{% elif object.action == 'delete' %}
{% trans "Object Deleted" %}
{% else %}
{% trans "No Changes" %}
{% endif %}
</span>
{% else %}
<pre class="change-diff change-removed">{{ diff_removed|json }}</pre>
<pre class="change-diff change-added">{{ diff_added|json }}</pre>
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,18 @@
{% load helpers %}
{% load i18n %}
<div class="card">
<h2 class="card-header">{% trans "Post-Change Data" %}</h2>
<div class="card-body">
{% if object.postchange_data %}
{% spaceless %}
<pre class="change-data">
{% for k, v in object.postchange_data_clean.items %}
<span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|json }}</span>
{% endfor %}
</pre>
{% endspaceless %}
{% else %}
<span class="text-muted">{% trans "None" %}</span>
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,20 @@
{% load helpers %}
{% load i18n %}
<div class="card">
<h2 class="card-header">{% trans "Pre-Change Data" %}</h2>
<div class="card-body">
{% if object.prechange_data %}
{% spaceless %}
<pre class="change-data">
{% for k, v in object.prechange_data_clean.items %}
<span{% if k in diff_removed %} class="removed"{% endif %}>{{ k }}: {{ v|json }}</span>
{% endfor %}
</pre>
{% endspaceless %}
{% elif non_atomic_change %}
{% trans "Warning: Comparing non-atomic change to previous change record" %} (<a href="{% url 'core:objectchange' pk=prev_change.pk %}">{{ prev_change.pk }}</a>)
{% else %}
<span class="text-muted">{% trans "None" %}</span>
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,11 @@
{% load i18n %}
{% include 'inc/panel_table.html' with table=related_changes_table heading='Related Changes' panel_class='default' %}
{% if related_changes_count > related_changes_table.rows|length %}
<div class="float-end">
<a href="{% url 'core:objectchange_list' %}?request_id={{ object.request_id }}" class="btn btn-primary">
{% blocktrans trimmed with count=related_changes_count|add:"1" %}
See All {{ count }} Changes
{% endblocktrans %}
</a>
</div>
{% endif %}