Compare commits

..

2 Commits

Author SHA1 Message Date
Jeremy Stretch
cb99199340 Initial POC for #21025 2026-03-03 08:17:55 -05:00
Jeremy Stretch
7ec656bc7c Introduce GitHub actions for Claude Code review (#21545) 2026-03-02 10:39:23 -06:00
27 changed files with 1642 additions and 222 deletions

View File

@@ -0,0 +1,44 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options

50
.github/workflows/claude.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'

View File

@@ -12,7 +12,7 @@ from dcim import filtersets
from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
from dcim.models import *
from dcim.svg import CableTraceSVG
from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
from extras.api.mixins import RenderConfigMixin
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.pagination import StripCountAnnotationsPaginator
@@ -398,12 +398,7 @@ class PlatformViewSet(MPTTLockedMixin, NetBoxModelViewSet):
# Devices/modules
#
class DeviceViewSet(
SequentialBulkCreatesMixin,
ConfigContextQuerySetMixin,
RenderConfigMixin,
NetBoxModelViewSet
):
class DeviceViewSet(SequentialBulkCreatesMixin, RenderConfigMixin, NetBoxModelViewSet):
queryset = Device.objects.prefetch_related(
'parent_bay', # Referenced by DeviceSerializer.get_parent_device()
)

View File

@@ -0,0 +1,21 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0226_modulebay_rebuild_tree'),
]
operations = [
migrations.AddField(
model_name='device',
name='config_context_data',
field=models.JSONField(blank=True, editable=False, null=True),
),
migrations.AddField(
model_name='module',
name='config_context_data',
field=models.JSONField(blank=True, editable=False, null=True),
),
]

View File

@@ -2683,7 +2683,7 @@ class DeviceInventoryView(DeviceComponentsView):
@register_model_view(Device, 'configcontext', path='config-context')
class DeviceConfigContextView(ObjectConfigContextView):
queryset = Device.objects.annotate_config_context_data()
queryset = Device.objects.all()
base_template = 'dcim/device/base.html'
tab = ViewTab(
label=_('Config Context'),

View File

@@ -10,34 +10,11 @@ from netbox.api.renderers import TextRenderer
from .serializers import ConfigTemplateSerializer
__all__ = (
'ConfigContextQuerySetMixin',
'ConfigTemplateRenderMixin',
'RenderConfigMixin',
)
class ConfigContextQuerySetMixin:
"""
Used by views that work with config context models (device and virtual machine).
Provides a get_queryset() method which deals with adding the config context
data annotation or not.
"""
def get_queryset(self):
"""
Build the proper queryset based on the request context
If the `brief` query param equates to True or the `exclude` query param
includes `config_context` as a value, return the base queryset.
Else, return the queryset annotated with config context data
"""
queryset = super().get_queryset()
request = self.get_serializer_context()['request']
if self.brief or 'config_context' in request.query_params.get('exclude', []):
return queryset
return queryset.annotate_config_context_data()
class ConfigTemplateRenderMixin:
"""
Provides a method to return a rendered ConfigTemplate as REST API data.

View File

@@ -22,19 +22,7 @@ if TYPE_CHECKING:
@strawberry.type
class ConfigContextMixin:
@classmethod
def get_queryset(cls, queryset, info: Info, **kwargs):
queryset = super().get_queryset(queryset, info, **kwargs)
# If `config_context` is requested, call annotate_config_context_data() on the queryset
selected = {f.name for f in info.selected_fields[0].selections}
if 'config_context' in selected and hasattr(queryset, 'annotate_config_context_data'):
return queryset.annotate_config_context_data()
return queryset
# Ensure `local_context_data` is fetched when `config_context` is requested
@strawberry_django.field(only=['local_context_data'])
@strawberry_django.field(only=['config_context_data', 'local_context_data'])
def config_context(self) -> strawberry.scalars.JSON:
return self.get_config_context()

View File

@@ -0,0 +1,40 @@
from django.core.management.base import BaseCommand
from django.db import connection
class Command(BaseCommand):
help = 'Rebuild pre-rendered config context data for all devices and/or virtual machines'
def add_arguments(self, parser):
parser.add_argument(
'--devices-only',
action='store_true',
help='Only rebuild config context data for devices',
)
parser.add_argument(
'--vms-only',
action='store_true',
help='Only rebuild config context data for virtual machines',
)
def handle(self, *args, **options):
devices_only = options['devices_only']
vms_only = options['vms_only']
with connection.cursor() as cursor:
if not vms_only:
self.stdout.write('Rebuilding config context data for devices...')
cursor.execute(
'UPDATE dcim_device SET config_context_data = compute_config_context_for_device(id)'
)
self.stdout.write(self.style.SUCCESS(f' Updated {cursor.rowcount} devices'))
if not devices_only:
self.stdout.write('Rebuilding config context data for virtual machines...')
cursor.execute(
'UPDATE virtualization_virtualmachine '
'SET config_context_data = compute_config_context_for_vm(id)'
)
self.stdout.write(self.style.SUCCESS(f' Updated {cursor.rowcount} virtual machines'))
self.stdout.write(self.style.SUCCESS('Done.'))

File diff suppressed because it is too large Load Diff

View File

@@ -225,6 +225,11 @@ class ConfigContextModel(models.Model):
"Local config context data takes precedence over source contexts in the final rendered config context"
)
)
config_context_data = models.JSONField(
blank=True,
null=True,
editable=False,
)
class Meta:
abstract = True
@@ -234,19 +239,21 @@ class ConfigContextModel(models.Model):
Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs.
Return the rendered configuration context for a device or VM.
"""
data = {}
# Use pre-rendered cached field if available
if self.config_context_data is not None:
return self.config_context_data
if not hasattr(self, 'config_context_data'):
# The annotation is not available, so we fall back to manually querying for the config context objects
config_context_data = ConfigContext.objects.get_for_object(self, aggregate_data=True) or []
# Fall back to annotation if queryset was annotated
data = {}
if hasattr(self, '_annotated_config_context_data'):
config_context_data = self._annotated_config_context_data or []
else:
# The attribute may exist, but the annotated value could be None if there is no config context data
config_context_data = self.config_context_data or []
# Last resort: compute on-the-fly
config_context_data = ConfigContext.objects.get_for_object(self, aggregate_data=True) or []
for context in config_context_data:
data = deepmerge(data, context)
# If the object has local config context data defined, merge it last
if self.local_context_data:
data = deepmerge(data, self.local_context_data)

View File

@@ -90,7 +90,7 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
"""
from extras.models import ConfigContext
return self.annotate(
config_context_data=Subquery(
_annotated_config_context_data=Subquery(
ConfigContext.objects.filter(
self._get_config_context_filters()
).annotate(

View File

@@ -206,6 +206,7 @@ class ConfigContextTest(TestCase):
"b": 456,
"c": 777
}
device.refresh_from_db()
self.assertEqual(device.get_config_context(), expected_data)
def test_name_ordering_after_weight(self):
@@ -235,6 +236,7 @@ class ConfigContextTest(TestCase):
"b": 456,
"c": 789
}
device.refresh_from_db()
self.assertEqual(device.get_config_context(), expected_data)
def test_schema_validation(self):
@@ -303,6 +305,7 @@ class ConfigContextTest(TestCase):
)
ConfigContext.objects.bulk_create([context1, context2, context3, context4])
device.refresh_from_db()
annotated_queryset = Device.objects.filter(name=device.name).annotate_config_context_data()
self.assertEqual(device.get_config_context(), annotated_queryset[0].get_config_context())
@@ -666,7 +669,7 @@ class ConfigContextTest(TestCase):
self.assertFalse(queryset.query.distinct)
# Check that tag subqueries DO use DISTINCT by inspecting the annotation
config_annotation = queryset.query.annotations.get('config_context_data')
config_annotation = queryset.query.annotations.get('_annotated_config_context_data')
self.assertIsNotNone(config_annotation)
def find_tag_subqueries(where_node):

View File

@@ -1 +0,0 @@
{% load helpers %}{{ value|isodate }}

View File

@@ -1 +0,0 @@
{% load helpers %}{{ object.get_full_name|placeholder }}

View File

@@ -1 +0,0 @@
{% load helpers %}{{ value|isodatetime:"minutes"|placeholder }}

View File

@@ -1,3 +1,60 @@
{% extends 'generic/object.html' %}
{% load i18n %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% block title %}{% trans "Group" %} {{ object.name }}{% endblock %}
{% block subtitle %}{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Group" %}</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 "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
</div>
<div class="col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Users" %}</h2>
<div class="list-group list-group-flush">
{% for user in object.users.all %}
<a href="{% url 'users:user' pk=user.pk %}" class="list-group-item list-group-item-action">{{ user }}</a>
{% empty %}
<div class="list-group-item text-muted">{% trans "None" %}</div>
{% endfor %}
</div>
</div>
<div class="card">
<h2 class="card-header">{% trans "Assigned Permissions" %}</h2>
<div class="list-group list-group-flush">
{% for perm in object.object_permissions.all %}
<a href="{% url 'users:objectpermission' pk=perm.pk %}" class="list-group-item list-group-item-action">{{ perm }}</a>
{% empty %}
<div class="list-group-item text-muted">{% trans "None" %}</div>
{% endfor %}
</div>
</div>
<div class="card">
<h2 class="card-header">{% trans "Owner Membership" %}</h2>
<div class="list-group list-group-flush">
{% for owner in object.owners.all %}
<a href="{% url 'users:owner' pk=owner.pk %}" class="list-group-item list-group-item-action">{{ owner }}</a>
{% empty %}
<div class="list-group-item text-muted">{% trans "None" %}</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,3 +1,93 @@
{% extends 'generic/object.html' %}
{% load i18n %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% block title %}{% trans "Permission" %} {{ object.name }}{% endblock %}
{% block subtitle %}{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Permission" %}</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 "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Enabled" %}</th>
<td>{% checkmark object.enabled %}</td>
</tr>
</table>
</div>
<div class="card">
<h2 class="card-header">{% trans "Actions" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "View" %}</th>
<td>{% checkmark object.can_view %}</td>
</tr>
<tr>
<th scope="row">{% trans "Add" %}</th>
<td>{% checkmark object.can_add %}</td>
</tr>
<tr>
<th scope="row">{% trans "Change" %}</th>
<td>{% checkmark object.can_change %}</td>
</tr>
<tr>
<th scope="row">{% trans "Delete" %}</th>
<td>{% checkmark object.can_delete %}</td>
</tr>
</table>
</div>
<div class="card">
<h2 class="card-header">{% trans "Constraints" %}</h2>
<div class="card-body">
{% if object.constraints %}
<pre>{{ object.constraints|json }}</pre>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Object Types" %}</h2>
<ul class="list-group list-group-flush">
{% for user in object.object_types.all %}
<li class="list-group-item">{{ user }}</li>
{% endfor %}
</ul>
</div>
<div class="card">
<h2 class="card-header">{% trans "Assigned Users" %}</h2>
<div class="list-group list-group-flush">
{% for user in object.users.all %}
<a href="{% url 'users:user' pk=user.pk %}" class="list-group-item list-group-item-action">{{ user }}</a>
{% empty %}
<div class="list-group-item text-muted">{% trans "None" %}</div>
{% endfor %}
</div>
</div>
<div class="card">
<h2 class="card-header">{% trans "Assigned Groups" %}</h2>
<div class="list-group list-group-flush">
{% for group in object.groups.all %}
<a href="{% url 'users:group' pk=group.pk %}" class="list-group-item list-group-item-action">{{ group }}</a>
{% empty %}
<div class="list-group-item text-muted">{% trans "None" %}</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -11,3 +11,50 @@
{% endblock %}
{% block subtitle %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Owner" %}</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 "Group" %}</th>
<td>{{ object.group|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
<div class="card">
<h2 class="card-header">{% trans "Groups" %}</h2>
<div class="list-group list-group-flush">
{% for group in object.user_groups.all %}
<a href="{% url 'users:group' pk=group.pk %}" class="list-group-item list-group-item-action">{{ group }}</a>
{% empty %}
<div class="list-group-item text-muted">{% trans "None" %}</div>
{% endfor %}
</div>
</div>
<div class="card">
<h2 class="card-header">{% trans "Users" %}</h2>
<div class="list-group list-group-flush">
{% for user in object.users.all %}
<a href="{% url 'users:user' pk=user.pk %}" class="list-group-item list-group-item-action">{{ user }}</a>
{% empty %}
<div class="list-group-item text-muted">{% trans "None" %}</div>
{% endfor %}
</div>
</div>
</div>
<div class="col-md-6">
{% include 'inc/panels/related_objects.html' with filter_name='owner_id' %}
</div>
</div>
{% endblock %}

View File

@@ -1,3 +1,46 @@
{% extends 'generic/object.html' %}
{% load i18n %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% block subtitle %}{% endblock %}
{% block extra_controls %}
{% if perms.users.add_owner %}
<a href="{% url 'users:owner_add' %}?group={{ object.pk }}" class="btn btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Owner" %}
</a>
{% endif %}
{% endblock extra_controls %}
{% block content %}
<div class="row mb-3">
<div class="col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Group" %}</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 "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
</div>
<div class="col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Members" %}</h2>
<div class="list-group list-group-flush">
{% for owner in object.members.all %}
<a href="{% url 'users:owner' pk=owner.pk %}" class="list-group-item list-group-item-action">{{ owner }}</a>
{% empty %}
<div class="list-group-item text-muted">{% trans "None" %}</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,11 +0,0 @@
{% load i18n %}
<div class="card">
<h2 class="card-header">{% trans "Object Types" %}</h2>
<ul class="list-group list-group-flush">
{% for object_type in object.object_types.all %}
<li class="list-group-item">{{ object_type }}</li>
{% empty %}
<li class="list-group-item text-muted">{% trans "None" %}</li>
{% endfor %}
</ul>
</div>

View File

@@ -1,3 +1,4 @@
{% extends 'generic/object.html' %}
{% load i18n %}
{% block subtitle %}{% endblock %}
{% block title %}{% trans "Token" %} {{ object }}{% endblock %}

View File

@@ -1,3 +1,85 @@
{% extends 'generic/object.html' %}
{% load i18n %}
{% block title %}{% trans "User" %} {{ object.username }}{% endblock %}
{% block subtitle %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="card">
<h2 class="card-header">{% trans "User" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Username" %}</th>
<td>{{ object.username }}</td>
</tr>
<tr>
<th scope="row">{% trans "Full Name" %}</th>
<td>{{ object.get_full_name|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Email" %}</th>
<td>{{ object.email|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Account Created" %}</th>
<td>{{ object.date_joined|isodate }}</td>
</tr>
<tr>
<th scope="row">{% trans "Last Login" %}</th>
<td>{{ object.last_login|isodatetime:"minutes"|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Active" %}</th>
<td>{% checkmark object.is_active %}</td>
</tr>
<tr>
<th scope="row">{% trans "Superuser" %}</th>
<td>{% checkmark object.is_superuser %}</td>
</tr>
</table>
</div>
</div>
<div class="col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Assigned Groups" %}</h2>
<div class="list-group list-group-flush">
{% for group in object.groups.all %}
<a href="{% url 'users:group' pk=group.pk %}" class="list-group-item list-group-item-action">{{ group }}</a>
{% empty %}
<div class="list-group-item text-muted">{% trans "None" %}</div>
{% endfor %}
</div>
</div>
<div class="card">
<h2 class="card-header">{% trans "Assigned Permissions" %}</h2>
<div class="list-group list-group-flush">
{% for perm in object.object_permissions.all %}
<a href="{% url 'users:objectpermission' pk=perm.pk %}" class="list-group-item list-group-item-action">{{ perm }}</a>
{% empty %}
<div class="list-group-item text-muted">{% trans "None" %}</div>
{% endfor %}
</div>
</div>
<div class="card">
<h2 class="card-header">{% trans "Owner Membership" %}</h2>
<div class="list-group list-group-flush">
{% for owner in object.owners.all %}
<a href="{% url 'users:owner' pk=owner.pk %}" class="list-group-item list-group-item-action">{{ owner }}</a>
{% empty %}
<div class="list-group-item text-muted">{% trans "None" %}</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% if perms.core.view_objectchange %}
<div class="row">
<div class="col-md-12">
{% include 'users/inc/user_activity.html' with user=object table=changelog_table %}
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -23,46 +23,3 @@ class TokenExamplePanel(panels.Panel):
actions = [
actions.CopyContent('token-example')
]
class UserPanel(panels.ObjectAttributesPanel):
username = attrs.TextAttr('username')
full_name = attrs.TemplatedAttr(
'get_full_name',
label=_('Full name'),
template_name='users/attrs/full_name.html',
)
email = attrs.TextAttr('email')
date_joined = attrs.TemplatedAttr(
'date_joined',
label=_('Account created'),
template_name='users/attrs/date_joined.html',
)
last_login = attrs.TemplatedAttr(
'last_login',
label=_('Last login'),
template_name='users/attrs/last_login.html',
)
is_active = attrs.BooleanAttr('is_active', label=_('Active'))
is_superuser = attrs.BooleanAttr('is_superuser', label=_('Superuser'))
class ObjectPermissionPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
description = attrs.TextAttr('description')
enabled = attrs.BooleanAttr('enabled')
class ObjectPermissionActionsPanel(panels.ObjectAttributesPanel):
title = _('Actions')
can_view = attrs.BooleanAttr('can_view', label=_('View'))
can_add = attrs.BooleanAttr('can_add', label=_('Add'))
can_change = attrs.BooleanAttr('can_change', label=_('Change'))
can_delete = attrs.BooleanAttr('can_delete', label=_('Delete'))
class OwnerPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
group = attrs.RelatedObjectAttr('group', linkify=True)
description = attrs.TextAttr('description')

View File

@@ -1,18 +1,9 @@
from django.db.models import Count
from django.utils.translation import gettext_lazy as _
from core.models import ObjectChange
from core.tables import ObjectChangeTable
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename
from netbox.ui import actions, layout
from netbox.ui.panels import (
ContextTablePanel,
JSONPanel,
ObjectsTablePanel,
OrganizationalObjectPanel,
RelatedObjectsPanel,
TemplatePanel,
)
from netbox.ui import layout
from netbox.views import generic
from users.ui import panels
from utilities.query import count_related
@@ -95,40 +86,7 @@ class UserListView(generic.ObjectListView):
@register_model_view(User)
class UserView(generic.ObjectView):
queryset = User.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.UserPanel(),
],
right_panels=[
ObjectsTablePanel(
'users.Group', title=_('Assigned Groups'), filters={'user_id': lambda ctx: ctx['object'].pk}
),
ObjectsTablePanel(
'users.ObjectPermission',
title=_('Assigned Permissions'),
filters={'user_id': lambda ctx: ctx['object'].pk},
),
ObjectsTablePanel(
'users.Owner', title=_('Owner Membership'), filters={'user_id': lambda ctx: ctx['object'].pk}
),
],
bottom_panels=[
ContextTablePanel(
'changelog_table',
title=_('Recent Activity'),
actions=[
actions.LinkAction(
view_name='core:objectchange_list',
url_params={'user_id': lambda ctx: ctx['object'].pk},
label=_('View All'),
button_class='ghost-primary',
button_icon='arrow-right-thick',
permissions=['core.view_objectchange'],
),
],
),
],
)
template_name = 'users/user.html'
def get_extra_context(self, request, instance):
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(user=instance)[:20]
@@ -196,22 +154,7 @@ class GroupListView(generic.ObjectListView):
@register_model_view(Group)
class GroupView(generic.ObjectView):
queryset = Group.objects.all()
layout = layout.SimpleLayout(
left_panels=[
OrganizationalObjectPanel(),
],
right_panels=[
ObjectsTablePanel('users.User', filters={'group_id': lambda ctx: ctx['object'].pk}),
ObjectsTablePanel(
'users.ObjectPermission',
title=_('Assigned Permissions'),
filters={'group_id': lambda ctx: ctx['object'].pk},
),
ObjectsTablePanel(
'users.Owner', title=_('Owner Membership'), filters={'user_group_id': lambda ctx: ctx['object'].pk}
),
],
)
template_name = 'users/group.html'
@register_model_view(Group, 'add', detail=False)
@@ -269,22 +212,7 @@ class ObjectPermissionListView(generic.ObjectListView):
@register_model_view(ObjectPermission)
class ObjectPermissionView(generic.ObjectView):
queryset = ObjectPermission.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ObjectPermissionPanel(),
panels.ObjectPermissionActionsPanel(),
JSONPanel('constraints', title=_('Constraints')),
],
right_panels=[
TemplatePanel('users/panels/object_types.html'),
ObjectsTablePanel(
'users.User', title=_('Assigned Users'), filters={'permission_id': lambda ctx: ctx['object'].pk}
),
ObjectsTablePanel(
'users.Group', title=_('Assigned Groups'), filters={'permission_id': lambda ctx: ctx['object'].pk}
),
],
)
template_name = 'users/objectpermission.html'
@register_model_view(ObjectPermission, 'add', detail=False)
@@ -327,7 +255,7 @@ class ObjectPermissionBulkDeleteView(generic.BulkDeleteView):
@register_model_view(OwnerGroup, 'list', path='', detail=False)
class OwnerGroupListView(generic.ObjectListView):
queryset = OwnerGroup.objects.annotate(
owner_count=count_related(Owner, 'group')
owner_count=count_related(Owner, 'group')
)
filterset = filtersets.OwnerGroupFilterSet
filterset_form = forms.OwnerGroupFilterForm
@@ -335,26 +263,14 @@ class OwnerGroupListView(generic.ObjectListView):
@register_model_view(OwnerGroup)
class OwnerGroupView(generic.ObjectView):
class OwnerGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = OwnerGroup.objects.all()
layout = layout.SimpleLayout(
left_panels=[
OrganizationalObjectPanel(),
],
right_panels=[
ObjectsTablePanel(
'users.Owner',
filters={'group_id': lambda ctx: ctx['object'].pk},
title=_('Members'),
actions=[
actions.AddObject(
'users.Owner',
url_params={'group': lambda ctx: ctx['object'].pk},
),
],
),
],
)
template_name = 'users/ownergroup.html'
def get_extra_context(self, request, instance):
return {
'related_models': self.get_related_models(request, instance),
}
@register_model_view(OwnerGroup, 'add', detail=False)
@@ -410,19 +326,7 @@ class OwnerListView(generic.ObjectListView):
@register_model_view(Owner)
class OwnerView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Owner.objects.all()
# Custom template is required to override the breadcrumbs block and include
# the owner's group in the breadcrumb trail when present.
template_name = 'users/owner.html'
layout = layout.SimpleLayout(
left_panels=[
panels.OwnerPanel(),
ObjectsTablePanel('users.Group', filters={'owner_id': lambda ctx: ctx['object'].pk}),
ObjectsTablePanel('users.User', filters={'owner_id': lambda ctx: ctx['object'].pk}),
],
right_panels=[
RelatedObjectsPanel(),
],
)
def get_extra_context(self, request, instance):
return {

View File

@@ -1,7 +1,7 @@
from django.db.models import Sum
from rest_framework.routers import APIRootView
from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
from extras.api.mixins import RenderConfigMixin
from netbox.api.viewsets import NetBoxModelViewSet
from utilities.query_functions import CollateAsChar
from virtualization import filtersets
@@ -48,7 +48,7 @@ class ClusterViewSet(NetBoxModelViewSet):
# Virtual machines
#
class VirtualMachineViewSet(ConfigContextQuerySetMixin, RenderConfigMixin, NetBoxModelViewSet):
class VirtualMachineViewSet(RenderConfigMixin, NetBoxModelViewSet):
queryset = VirtualMachine.objects.all()
filterset_class = filtersets.VirtualMachineFilterSet

View File

@@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0052_gfk_indexes'),
]
operations = [
migrations.AddField(
model_name='virtualmachine',
name='config_context_data',
field=models.JSONField(blank=True, editable=False, null=True),
),
]

View File

@@ -487,7 +487,7 @@ class VirtualMachineVirtualDisksView(generic.ObjectChildrenView):
@register_model_view(VirtualMachine, 'configcontext', path='config-context')
class VirtualMachineConfigContextView(ObjectConfigContextView):
queryset = VirtualMachine.objects.annotate_config_context_data()
queryset = VirtualMachine.objects.all()
base_template = 'virtualization/virtualmachine.html'
tab = ViewTab(
label=_('Config Context'),