Compare commits

...

8 Commits

Author SHA1 Message Date
Jeremy Stretch
7c7f5a6bd8 Fixes #20934: Fix flicker when navigating in dark mode 2026-03-11 17:24:34 -04:00
Martin Hauser
cac3c1221c Closes #21631: Remove duplicate 'created' field in RackReservation table (#21632) 2026-03-11 11:49:01 -05:00
Jeremy Stretch
3a9d00a537 Update the lock-threads workflow 2026-03-11 08:56:39 -04:00
github-actions
4040e4f266 Update source translation strings 2026-03-11 05:19:17 +00:00
Jeremy Stretch
f938309ed9 Second attempt to fix @claude for PRs from forks (#21633) 2026-03-10 10:35:28 -07:00
Jeremy Stretch
98d898aba9 Fix the Claude action for external PRs (#21629) 2026-03-10 08:26:36 -07:00
Arthur Hanson
07bb6aa365 #20923: Migrate Users object to declarative layouts (#21568)
This continues the migration of object views in the user app to NetBox v4.5’s declarative layouts.
Replace legacy object view templates with declarative layouts for:
   - Users
   - Groups
   - API Tokens
   - Permissions
   - Owner Groups
   - Owners
2026-03-10 16:04:24 +01:00
pobradovic08
f3c34b30ec Fixes #21402: Prefetch device_type and manufacturer for brief mode API responses (#21616)
* Fixes #21402: Prefetch device_type and manufacturer for brief mode API responses

Add select_related for device_type__manufacturer on the DeviceViewSet
queryset to prevent N+1 queries when rendering unnamed devices in brief
mode.

* Use prefetch_related instead of select_related for device_type__manufacturer
2026-03-10 10:38:17 -04:00
24 changed files with 285 additions and 440 deletions

View File

@@ -30,9 +30,39 @@ jobs:
with:
fetch-depth: 1
# Workaround for claude-code-action bug with fork PRs: The action fetches by branch name
# (git fetch origin --depth=N <branch>), but fork PR branches don't exist on origin.
# Fix: redirect origin to the fork's URL so the action can fetch the branch directly.
- name: Configure git remote for fork PRs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Determine PR number based on event type
if [ "${{ github.event_name }}" = "issue_comment" ]; then
PR_NUMBER="${{ github.event.issue.number }}"
elif [ "${{ github.event_name }}" = "pull_request_review_comment" ] || [ "${{ github.event_name }}" = "pull_request_review" ]; then
PR_NUMBER="${{ github.event.pull_request.number }}"
else
exit 0 # issues event — no PR branch to worry about
fi
# Fetch fork info in one API call; silently skip if this is not a PR
PR_INFO=$(gh pr view "${PR_NUMBER}" --json isCrossRepository,headRepositoryOwner,headRepository 2>/dev/null || echo "")
if [ -z "$PR_INFO" ]; then
exit 0
fi
IS_FORK=$(echo "$PR_INFO" | jq -r '.isCrossRepository')
if [ "$IS_FORK" = "true" ]; then
FORK_OWNER=$(echo "$PR_INFO" | jq -r '.headRepositoryOwner.login')
FORK_REPO=$(echo "$PR_INFO" | jq -r '.headRepository.name')
echo "Fork PR detected from ${FORK_OWNER}/${FORK_REPO}: updating origin to fork URL"
git remote set-url origin "https://github.com/${FORK_OWNER}/${FORK_REPO}.git"
fi
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
uses: anthropics/claude-code-action@e763fe78de2db7389e04818a00b5ff8ba13d1360 # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}

View File

@@ -11,14 +11,14 @@ permissions:
pull-requests: write
discussions: write
concurrency:
group: lock-threads
jobs:
lock:
if: github.repository == 'netbox-community/netbox'
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
- uses: dessant/lock-threads@v6.0.0
with:
issue-inactive-days: 90
pr-inactive-days: 30
discussion-inactive-days: 180
issue-lock-reason: 'resolved'

View File

@@ -405,6 +405,7 @@ class DeviceViewSet(
NetBoxModelViewSet
):
queryset = Device.objects.prefetch_related(
'device_type__manufacturer', # Referenced by Device.__str__() for unnamed devices
'parent_bay', # Referenced by DeviceSerializer.get_parent_device()
)
filterset_class = filtersets.DeviceFilterSet

View File

@@ -218,7 +218,7 @@ class RackReservationTable(TenancyColumnsMixin, PrimaryModelTable):
class Meta(PrimaryModelTable.Meta):
model = RackReservation
fields = (
'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'status', 'user', 'created', 'tenant',
'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'status', 'user', 'tenant',
'tenant_group', 'description', 'comments', 'tags', 'actions', 'created', 'last_updated',
)
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'status', 'user', 'description')

View File

@@ -10,6 +10,7 @@ __all__ = (
'BooleanAttr',
'ChoiceAttr',
'ColorAttr',
'DateTimeAttr',
'GPSCoordinatesAttr',
'GenericForeignKeyAttr',
'ImageAttr',
@@ -367,6 +368,26 @@ class GPSCoordinatesAttr(ObjectAttribute):
})
class DateTimeAttr(ObjectAttribute):
"""
A date or datetime attribute.
Parameters:
spec (str): Controls the rendering format. Use 'date' for date-only rendering,
or 'seconds'/'minutes' for datetime rendering with the given precision.
"""
template_name = 'ui/attrs/datetime.html'
def __init__(self, *args, spec='seconds', **kwargs):
super().__init__(*args, **kwargs)
self.spec = spec
def get_context(self, obj, context):
return {
'spec': self.spec,
}
class TimezoneAttr(ObjectAttribute):
"""
A timezone value. Includes the numeric offset from UTC.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -20,12 +20,7 @@ function storeColorMode(mode: ColorMode): void {
}
function updateElements(targetMode: ColorMode): void {
const body = document.querySelector('body');
if (body && targetMode == 'dark') {
body.setAttribute('data-bs-theme', 'dark');
} else if (body) {
body.setAttribute('data-bs-theme', 'light');
}
document.documentElement.setAttribute('data-bs-theme', targetMode);
for (const elevation of getElements<HTMLObjectElement>('.rack_elevation')) {
const svg = elevation.firstElementChild ?? null;

View File

@@ -112,7 +112,7 @@ img.plugin-icon {
}
body[data-bs-theme=dark] {
html[data-bs-theme=dark] {
// Assuming icon is black/white line art, invert it and tone down brightness
img.plugin-icon {
filter: grayscale(100%) invert(100%) brightness(80%);

View File

@@ -93,7 +93,7 @@ pre {
}
// Dark mode overrides
body[data-bs-theme=dark] {
html[data-bs-theme=dark] {
// Override background color alpha value
::selection {
background-color: rgba(var(--tblr-primary-rgb),.48);
@@ -174,16 +174,11 @@ pre code {
}
// Theme-based visibility utilities
// Tabler's .hide-theme-* utilities expect data-bs-theme on :root, but NetBox applies
// it to body. These overrides use higher specificity selectors to ensure theme-based
// visibility works correctly. The :root:not(.dummy) pattern provides the additional
// specificity needed to override Tabler's :root:not() rules.
:root:not(.dummy) body[data-bs-theme='light'] .hide-theme-light,
:root:not(.dummy) body[data-bs-theme='dark'] .hide-theme-dark {
:root:not(.dummy)[data-bs-theme='light'] .hide-theme-light,
:root:not(.dummy)[data-bs-theme='dark'] .hide-theme-dark {
display: none !important;
}
:root:not(.dummy) body[data-bs-theme='dark'] .hide-theme-light,
:root:not(.dummy) body[data-bs-theme='light'] .hide-theme-dark {
:root:not(.dummy)[data-bs-theme='dark'] .hide-theme-light,
:root:not(.dummy)[data-bs-theme='light'] .hide-theme-dark {
display: inline-flex !important;
}

View File

@@ -77,13 +77,13 @@
}
// Light theme styling
body[data-bs-theme=light] .navbar-vertical.navbar-expand-lg {
html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg {
// Background Gradient
background: linear-gradient(180deg, rgba(0, 133, 125, 0.00) 0%, rgba(0, 133, 125, 0.10) 100%), #FFF;
}
// Dark theme styling
body[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg {
html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg {
// Background Gradient
background: linear-gradient(180deg, rgba(0, 242, 212, 0.00) 0%, rgba(0, 242, 212, 0.10) 100%), #001423;

View File

@@ -59,7 +59,7 @@ table th.orderable a {
color: var(--#{$prefix}body-color);
}
body[data-bs-theme=dark] {
html[data-bs-theme=dark] {
// Adjust table header background color
.table thead th, .markdown>table thead th {
background: $rich-black !important;

View File

@@ -0,0 +1 @@
{% load helpers %}{% if spec == 'date' %}{{ value|isodate }}{% else %}{{ value|isodatetime:spec }}{% endif %}

View File

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

View File

@@ -1,60 +1,3 @@
{% 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,93 +1,5 @@
{% 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,50 +11,3 @@
{% 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,46 +1,3 @@
{% 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

@@ -0,0 +1,11 @@
{% 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,85 +1,3 @@
{% 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

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-10 05:18+0000\n"
"POT-Creation-Date: 2026-03-11 05:18+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -62,7 +62,7 @@ msgstr ""
#: netbox/ipam/choices.py:31 netbox/ipam/choices.py:49
#: netbox/ipam/choices.py:69 netbox/ipam/choices.py:154
#: netbox/templates/extras/configcontext.html:29
#: netbox/templates/users/user.html:35 netbox/users/forms/bulk_edit.py:41
#: netbox/users/forms/bulk_edit.py:41 netbox/users/ui/panels.py:38
#: netbox/virtualization/choices.py:22 netbox/virtualization/choices.py:45
#: netbox/vpn/choices.py:19 netbox/vpn/choices.py:280
#: netbox/wireless/choices.py:25
@@ -471,7 +471,7 @@ msgstr ""
#: netbox/dcim/tables/devicetypes.py:214 netbox/dcim/tables/devicetypes.py:255
#: netbox/dcim/tables/devicetypes.py:274 netbox/dcim/tables/racks.py:30
#: netbox/extras/forms/bulk_edit.py:306 netbox/extras/tables/tables.py:552
#: netbox/netbox/ui/attrs.py:193 netbox/templates/circuits/circuittype.html:30
#: netbox/netbox/ui/attrs.py:194 netbox/templates/circuits/circuittype.html:30
#: netbox/templates/circuits/virtualcircuittype.html:30
#: netbox/templates/dcim/cable.html:44 netbox/templates/dcim/frontport.html:40
#: netbox/templates/dcim/inventoryitemrole.html:26
@@ -890,10 +890,6 @@ msgstr ""
#: netbox/templates/tenancy/contactrole.html:22
#: netbox/templates/tenancy/tenant.html:24
#: netbox/templates/tenancy/tenantgroup.html:33
#: netbox/templates/users/group.html:21
#: netbox/templates/users/objectpermission.html:21
#: netbox/templates/users/owner.html:30
#: netbox/templates/users/ownergroup.html:27
#: netbox/templates/vpn/ikepolicy.html:17
#: netbox/templates/vpn/ikeproposal.html:17
#: netbox/templates/vpn/ipsecpolicy.html:17
@@ -1344,9 +1340,6 @@ msgstr ""
#: netbox/templates/ipam/inc/panels/fhrp_groups.html:23
#: netbox/templates/ipam/panels/fhrp_groups.html:9
#: netbox/templates/ipam/vlan.html:27 netbox/templates/tenancy/tenant.html:20
#: netbox/templates/users/group.html:6 netbox/templates/users/group.html:14
#: netbox/templates/users/owner.html:26
#: netbox/templates/users/ownergroup.html:20
#: netbox/templates/vpn/tunnel.html:29
#: netbox/templates/wireless/wirelesslan.html:18
#: netbox/tenancy/forms/bulk_edit.py:44 netbox/tenancy/forms/bulk_import.py:46
@@ -1743,10 +1736,6 @@ msgstr ""
#: netbox/templates/tenancy/contactgroup.html:21
#: netbox/templates/tenancy/contactrole.html:18
#: netbox/templates/tenancy/tenantgroup.html:29
#: netbox/templates/users/group.html:17
#: netbox/templates/users/objectpermission.html:17
#: netbox/templates/users/owner.html:22
#: netbox/templates/users/ownergroup.html:23
#: netbox/templates/vpn/ikepolicy.html:13
#: netbox/templates/vpn/ikeproposal.html:13
#: netbox/templates/vpn/ipsecpolicy.html:13
@@ -2117,8 +2106,7 @@ msgid "Local"
msgstr ""
#: netbox/core/data_backends.py:64 netbox/core/tables/change_logging.py:21
#: netbox/templates/account/profile.html:13 netbox/templates/users/user.html:15
#: netbox/users/tables.py:64
#: netbox/templates/account/profile.html:13 netbox/users/tables.py:64
msgid "Username"
msgstr ""
@@ -2189,7 +2177,6 @@ msgstr ""
#: netbox/templates/extras/eventrule.html:17
#: netbox/templates/extras/savedfilter.html:25
#: netbox/templates/extras/tableconfig.html:33
#: netbox/templates/users/objectpermission.html:25
#: netbox/users/forms/bulk_edit.py:87 netbox/users/forms/bulk_edit.py:105
#: netbox/users/forms/filtersets.py:67 netbox/users/forms/filtersets.py:133
#: netbox/users/tables.py:30 netbox/users/tables.py:113
@@ -2302,8 +2289,7 @@ msgstr ""
#: netbox/templates/core/objectchange.html:36
#: netbox/templates/extras/savedfilter.html:21
#: netbox/templates/extras/tableconfig.html:29
#: netbox/templates/inc/user_menu.html:31 netbox/templates/users/user.html:4
#: netbox/templates/users/user.html:12 netbox/users/filtersets.py:135
#: netbox/templates/inc/user_menu.html:31 netbox/users/filtersets.py:135
#: netbox/users/filtersets.py:217 netbox/users/forms/filtersets.py:81
#: netbox/users/forms/filtersets.py:126 netbox/users/forms/model_forms.py:181
#: netbox/users/forms/model_forms.py:221 netbox/users/tables.py:22
@@ -2747,7 +2733,7 @@ msgid "Deletion is prevented by a protection rule: {message}"
msgstr ""
#: netbox/core/tables/change_logging.py:26
#: netbox/templates/account/profile.html:17 netbox/templates/users/user.html:19
#: netbox/templates/account/profile.html:17
msgid "Full Name"
msgstr ""
@@ -5963,8 +5949,7 @@ msgstr ""
#: netbox/dcim/forms/object_create.py:312 netbox/dcim/tables/devices.py:1130
#: netbox/ipam/tables/fhrp.py:31 netbox/templates/dcim/virtualchassis.html:43
#: netbox/templates/dcim/virtualchassis_edit.html:59
#: netbox/templates/ipam/fhrpgroup.html:38
#: netbox/templates/users/ownergroup.html:35
#: netbox/templates/ipam/fhrpgroup.html:38 netbox/users/views.py:347
msgid "Members"
msgstr ""
@@ -8828,7 +8813,6 @@ msgstr ""
#: netbox/extras/forms/bulk_import.py:318
#: netbox/extras/forms/model_forms.py:414 netbox/netbox/navigation/menu.py:415
#: netbox/templates/extras/notificationgroup.html:41
#: netbox/templates/users/group.html:29 netbox/templates/users/owner.html:46
#: netbox/users/forms/filtersets.py:181 netbox/users/forms/model_forms.py:265
#: netbox/users/forms/model_forms.py:277 netbox/users/forms/model_forms.py:352
#: netbox/users/forms/model_forms.py:483 netbox/users/forms/model_forms.py:498
@@ -8845,8 +8829,7 @@ msgstr ""
#: netbox/netbox/navigation/menu.py:416
#: netbox/templates/extras/notificationgroup.html:31
#: netbox/templates/tenancy/contact.html:21
#: netbox/templates/users/owner.html:36 netbox/tenancy/forms/bulk_edit.py:121
#: netbox/tenancy/forms/filtersets.py:107
#: netbox/tenancy/forms/bulk_edit.py:121 netbox/tenancy/forms/filtersets.py:107
#: netbox/tenancy/forms/model_forms.py:93 netbox/tenancy/tables/contacts.py:57
#: netbox/tenancy/tables/contacts.py:101 netbox/users/forms/filtersets.py:176
#: netbox/users/forms/model_forms.py:210 netbox/users/forms/model_forms.py:222
@@ -10008,7 +9991,7 @@ msgstr ""
#: netbox/extras/tables/tables.py:517 netbox/extras/tables/tables.py:555
#: netbox/templates/extras/customfield.html:105
#: netbox/templates/extras/eventrule.html:27
#: netbox/templates/users/objectpermission.html:64 netbox/users/tables.py:110
#: netbox/templates/users/panels/object_types.html:3 netbox/users/tables.py:110
msgid "Object Types"
msgstr ""
@@ -10059,7 +10042,7 @@ msgstr ""
#: netbox/netbox/forms/mixins.py:162 netbox/netbox/forms/mixins.py:187
#: netbox/netbox/tables/tables.py:292 netbox/netbox/tables/tables.py:307
#: netbox/netbox/tables/tables.py:322 netbox/templates/generic/object.html:61
#: netbox/templates/users/owner.html:19 netbox/users/forms/model_forms.py:481
#: netbox/users/forms/model_forms.py:481
msgid "Owner"
msgstr ""
@@ -12506,7 +12489,7 @@ msgstr ""
#: netbox/templates/dcim/manufacturer.html:8
#: netbox/templates/extras/tableconfig_edit.html:29
#: netbox/templates/generic/bulk_add_component.html:22
#: netbox/templates/users/objectpermission.html:38
#: netbox/users/ui/panels.py:52
#: netbox/utilities/templates/helpers/table_config_form.html:20
#: netbox/utilities/templates/widgets/splitmultiselect.html:11
#: netbox/utilities/templatetags/buttons.py:175
@@ -12543,8 +12526,7 @@ msgstr ""
#: netbox/templates/htmx/delete_form.html:70
#: netbox/templates/ipam/inc/panels/fhrp_groups.html:48
#: netbox/templates/ipam/panels/fhrp_groups.html:34
#: netbox/templates/users/objectpermission.html:46
#: netbox/utilities/templatetags/buttons.py:146
#: netbox/users/ui/panels.py:54 netbox/utilities/templatetags/buttons.py:146
msgid "Delete"
msgstr ""
@@ -12801,13 +12783,13 @@ msgstr ""
msgid "Copy"
msgstr ""
#: netbox/netbox/ui/attrs.py:212
#: netbox/netbox/ui/attrs.py:213
#, python-brace-format
msgid ""
"Invalid decoding option: {decoding}! Must be one of {image_decoding_choices}"
msgstr ""
#: netbox/netbox/ui/attrs.py:343
#: netbox/netbox/ui/attrs.py:344
msgid "GPS coordinates"
msgstr ""
@@ -13077,26 +13059,25 @@ msgid "Account Details"
msgstr ""
#: netbox/templates/account/profile.html:27
#: netbox/templates/tenancy/contact.html:53 netbox/templates/users/user.html:23
#: netbox/templates/tenancy/contact.html:53
#: netbox/tenancy/forms/bulk_edit.py:104
msgid "Email"
msgstr ""
#: netbox/templates/account/profile.html:31 netbox/templates/users/user.html:27
#: netbox/templates/account/profile.html:31
msgid "Account Created"
msgstr ""
#: netbox/templates/account/profile.html:35 netbox/templates/users/user.html:31
#: netbox/templates/account/profile.html:35
msgid "Last Login"
msgstr ""
#: netbox/templates/account/profile.html:39 netbox/templates/users/user.html:39
#: netbox/templates/account/profile.html:39 netbox/users/ui/panels.py:39
msgid "Superuser"
msgstr ""
#: netbox/templates/account/profile.html:47
#: netbox/templates/users/objectpermission.html:82
#: netbox/templates/users/user.html:47
#: netbox/templates/account/profile.html:47 netbox/users/views.py:104
#: netbox/users/views.py:283
msgid "Assigned Groups"
msgstr ""
@@ -13126,14 +13107,7 @@ msgstr ""
#: netbox/templates/ipam/panels/fhrp_groups.html:42
#: netbox/templates/ui/panels/comments.html:9
#: netbox/templates/ui/panels/related_objects.html:22
#: netbox/templates/users/group.html:34 netbox/templates/users/group.html:44
#: netbox/templates/users/group.html:54
#: netbox/templates/users/objectpermission.html:77
#: netbox/templates/users/objectpermission.html:87
#: netbox/templates/users/owner.html:41 netbox/templates/users/owner.html:51
#: netbox/templates/users/ownergroup.html:40
#: netbox/templates/users/user.html:52 netbox/templates/users/user.html:62
#: netbox/templates/users/user.html:72
#: netbox/templates/users/panels/object_types.html:8
msgid "None"
msgstr ""
@@ -13465,8 +13439,7 @@ msgstr ""
msgid "every %(interval)s minutes"
msgstr ""
#: netbox/templates/core/objectchange.html:29
#: netbox/templates/users/objectpermission.html:42
#: netbox/templates/core/objectchange.html:29 netbox/users/ui/panels.py:53
msgid "Change"
msgstr ""
@@ -14252,8 +14225,8 @@ msgstr ""
#: netbox/templates/dcim/virtualchassis_add_member.html:27
#: netbox/templates/generic/object_edit.html:78
#: netbox/templates/users/objectpermission.html:31
#: netbox/users/forms/filtersets.py:64 netbox/users/forms/model_forms.py:373
#: netbox/users/ui/panels.py:49
msgid "Actions"
msgstr ""
@@ -15392,45 +15365,19 @@ msgstr ""
msgid "Local time"
msgstr ""
#: netbox/templates/users/group.html:39 netbox/templates/users/user.html:57
msgid "Assigned Permissions"
msgstr ""
#: netbox/templates/users/group.html:49 netbox/templates/users/user.html:67
msgid "Owner Membership"
msgstr ""
#: netbox/templates/users/inc/user_activity.html:6
#: netbox/templates/users/inc/user_activity.html:6 netbox/users/views.py:118
msgid "Recent Activity"
msgstr ""
#: netbox/templates/users/inc/user_activity.html:9
#: netbox/templates/users/inc/user_activity.html:9 netbox/users/views.py:123
msgid "View All"
msgstr ""
#: netbox/templates/users/objectpermission.html:6
#: netbox/templates/users/objectpermission.html:14
#: netbox/templates/users/objectpermission.html:4
#: netbox/users/forms/filtersets.py:63
msgid "Permission"
msgstr ""
#: netbox/templates/users/objectpermission.html:34
msgid "View"
msgstr ""
#: netbox/templates/users/objectpermission.html:52
#: netbox/users/forms/model_forms.py:363 netbox/users/forms/model_forms.py:376
msgid "Constraints"
msgstr ""
#: netbox/templates/users/objectpermission.html:72
msgid "Assigned Users"
msgstr ""
#: netbox/templates/users/ownergroup.html:11
msgid "Add Owner"
msgstr ""
#: netbox/templates/users/token.html:4 netbox/users/forms/bulk_import.py:48
#: netbox/users/forms/filtersets.py:117 netbox/users/forms/model_forms.py:127
msgid "Token"
@@ -15987,6 +15934,11 @@ msgstr ""
msgid "Actions granted in addition to those listed above"
msgstr ""
#: netbox/users/forms/model_forms.py:363 netbox/users/forms/model_forms.py:376
#: netbox/users/views.py:275
msgid "Constraints"
msgstr ""
#: netbox/users/forms/model_forms.py:365
msgid ""
"JSON expression of a queryset filter that will return only permitted "
@@ -16206,6 +16158,34 @@ msgstr ""
msgid "Example Usage"
msgstr ""
#: netbox/users/ui/panels.py:32
msgid "Full name"
msgstr ""
#: netbox/users/ui/panels.py:36
msgid "Account created"
msgstr ""
#: netbox/users/ui/panels.py:37
msgid "Last login"
msgstr ""
#: netbox/users/ui/panels.py:51
msgid "View"
msgstr ""
#: netbox/users/views.py:108 netbox/users/views.py:206
msgid "Assigned Permissions"
msgstr ""
#: netbox/users/views.py:112 netbox/users/views.py:210
msgid "Owner Membership"
msgstr ""
#: netbox/users/views.py:280
msgid "Assigned Users"
msgstr ""
#: netbox/utilities/api.py:184
#, python-brace-format
msgid "Related object not found using the provided attributes: {params}"

View File

@@ -23,3 +23,38 @@ 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.DateTimeAttr('date_joined', label=_('Account created'), spec='date')
last_login = attrs.DateTimeAttr('last_login', label=_('Last login'), spec='minutes')
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,9 +1,18 @@
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 layout
from netbox.ui import actions, layout
from netbox.ui.panels import (
ContextTablePanel,
JSONPanel,
ObjectsTablePanel,
OrganizationalObjectPanel,
RelatedObjectsPanel,
TemplatePanel,
)
from netbox.views import generic
from users.ui import panels
from utilities.query import count_related
@@ -86,7 +95,39 @@ class UserListView(generic.ObjectListView):
@register_model_view(User)
class UserView(generic.ObjectView):
queryset = User.objects.all()
template_name = 'users/user.html'
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_icon='arrow-right-thick',
permissions=['core.view_objectchange'],
),
],
),
],
)
def get_extra_context(self, request, instance):
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(user=instance)[:20]
@@ -154,7 +195,22 @@ class GroupListView(generic.ObjectListView):
@register_model_view(Group)
class GroupView(generic.ObjectView):
queryset = Group.objects.all()
template_name = 'users/group.html'
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}
),
],
)
@register_model_view(Group, 'add', detail=False)
@@ -212,7 +268,22 @@ class ObjectPermissionListView(generic.ObjectListView):
@register_model_view(ObjectPermission)
class ObjectPermissionView(generic.ObjectView):
queryset = ObjectPermission.objects.all()
template_name = 'users/objectpermission.html'
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}
),
],
)
@register_model_view(ObjectPermission, 'add', detail=False)
@@ -255,7 +326,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
@@ -263,14 +334,26 @@ class OwnerGroupListView(generic.ObjectListView):
@register_model_view(OwnerGroup)
class OwnerGroupView(GetRelatedModelsMixin, generic.ObjectView):
class OwnerGroupView(generic.ObjectView):
queryset = OwnerGroup.objects.all()
template_name = 'users/ownergroup.html'
def get_extra_context(self, request, instance):
return {
'related_models': self.get_related_models(request, instance),
}
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},
),
],
),
],
)
@register_model_view(OwnerGroup, 'add', detail=False)
@@ -326,7 +409,16 @@ class OwnerListView(generic.ObjectListView):
@register_model_view(Owner)
class OwnerView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Owner.objects.all()
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 {