Compare commits

..

8 Commits

Author SHA1 Message Date
Jeremy Stretch
6600b848c0 Fixes #20077: Fix form field focus bug on Edge 2026-03-11 12:56:36 -04: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
github-actions
2281889e9d Update source translation strings 2026-03-10 05:18:47 +00:00
36 changed files with 977 additions and 835 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

@@ -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

View File

@@ -1,16 +1,16 @@
import type { RecursivePartial, TomOption, TomSettings, TomInput } from 'tom-select/dist/cjs/types';
import { addClasses } from 'tom-select/src/vanilla.ts';
import queryString from 'query-string';
import TomSelect from 'tom-select';
import type { Stringifiable } from 'query-string';
import { DynamicParamsMap } from './dynamicParamsMap';
import { NetBoxTomSelect } from './netboxTomSelect';
// Transitional
import { QueryFilter, PathFilter } from '../types';
import { getElement, replaceAll } from '../../util';
// Extends TomSelect to provide enhanced fetching of options via the REST API
export class DynamicTomSelect extends TomSelect {
// Extends NetBoxTomSelect to provide enhanced fetching of options via the REST API
export class DynamicTomSelect extends NetBoxTomSelect {
public readonly nullOption: Nullable<TomOption> = null;
// Transitional code from APISelect

View File

@@ -0,0 +1,39 @@
import TomSelect from 'tom-select';
/**
* Extends TomSelect to work around a browser autofill bug where Edge's "last used" autofill
* simultaneously focuses multiple inputs, triggering a cascading focus/open/blur loop between
* TomSelect instances.
*
* Root cause: TomSelect's open() method calls focus(), which synchronously moves browser focus
* to this instance's control input, then schedules setTimeout(onFocus, 0). When Edge autofill
* has moved focus to a *different* select before the timeout fires, the delayed onFocus() call
* re-steals browser focus back, causing the other instance to blur and close. Each instance's
* deferred callback then repeats this, creating an infinite ping-pong loop.
*
* Fix: in the setTimeout callback, only proceed with onFocus() if this instance's element is
* still the active element. If focus has already moved elsewhere, skip the call.
*
* Upstream bug: https://github.com/orchidjs/tom-select/issues/806
* NetBox issue: https://github.com/netbox-community/netbox/issues/20077
*/
export class NetBoxTomSelect extends TomSelect {
focus(): void {
if (this.isDisabled || this.isReadOnly) return;
this.ignoreFocus = true;
const focusTarget = this.control_input.offsetWidth ? this.control_input : this.focus_node;
focusTarget.focus();
setTimeout(() => {
this.ignoreFocus = false;
// Only proceed if this instance's element is still the active element. If Edge autofill
// (or anything else) has moved focus to a different element in the interim, calling
// onFocus() here would steal focus back and restart the cascade loop.
if (document.activeElement === focusTarget || this.control.contains(document.activeElement)) {
this.onFocus();
}
}, 0);
}
}

View File

@@ -1,6 +1,6 @@
import { TomOption } from 'tom-select/src/types';
import TomSelect from 'tom-select';
import { escape_html } from 'tom-select/src/utils';
import { NetBoxTomSelect } from './classes/netboxTomSelect';
import { getPlugins } from './config';
import { getElements } from '../util';
@@ -9,7 +9,7 @@ export function initStaticSelects(): void {
for (const select of getElements<HTMLSelectElement>(
'select:not(.tomselected):not(.no-ts):not([size]):not(.api-select):not(.color-select)',
)) {
new TomSelect(select, {
new NetBoxTomSelect(select, {
...getPlugins(select),
maxOptions: undefined,
});
@@ -25,7 +25,7 @@ export function initColorSelects(): void {
}
for (const select of getElements<HTMLSelectElement>('select.color-select:not(.tomselected)')) {
new TomSelect(select, {
new NetBoxTomSelect(select, {
...getPlugins(select),
maxOptions: undefined,
render: {

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

@@ -1,3 +0,0 @@
{% load i18n %}
<span id="secret" class="font-monospace" data-secret="{{ value }}">{{ value }}</span>
<button type="button" class="btn btn-primary toggle-secret float-end" data-bs-toggle="button">{% trans "Show Secret" %}</button>

View File

@@ -1 +1,63 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "IKE Policy" %}</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 "IKE Version" %}</th>
<td>{{ object.get_version_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Mode" %}</th>
<td>{{ object.get_mode_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Pre-Shared Key" %}</th>
<td>
<span id="secret" class="font-monospace" data-secret="{{ object.preshared_key }}">{{ object.preshared_key|placeholder }}</span>
{% if object.preshared_key %}
<button type="button" class="btn btn-primary toggle-secret float-end" data-bs-toggle="button">{% trans "Show Secret" %}</button>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "IPSec Profiles" %}</th>
<td>
<a href="{% url 'vpn:ipsecprofile_list' %}?ike_policy_id={{ object.pk }}">{{ object.ipsec_profiles.count }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">{% trans "Proposals" %}</h2>
{% htmx_table 'vpn:ikeproposal_list' ike_policy_id=object.pk %}
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1 +1,62 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "IKE Proposal" %}</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 "Authentication method" %}</th>
<td>{{ object.get_authentication_method_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Encryption algorithm" %}</th>
<td>{{ object.get_encryption_algorithm_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Authentication algorithm" %}</th>
<td>{{ object.get_authentication_algorithm_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "DH group" %}</th>
<td>{{ object.get_group_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "SA lifetime (seconds)" %}</th>
<td>{{ object.sa_lifetime|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "IKE Policies" %}</th>
<td>
<a href="{% url 'vpn:ikepolicy_list' %}?proposal_id={{ object.pk }}">{{ object.ike_policies.count }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1 +1,51 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "IPSec Policy" %}</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 "PFS group" %}</th>
<td>{{ object.get_pfs_group_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "IPSec Profiles" %}</th>
<td>
<a href="{% url 'vpn:ipsecprofile_list' %}?ipsec_policy_id={{ object.pk }}">{{ object.ipsec_profiles.count }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">{% trans "Proposals" %}</h2>
{% htmx_table 'vpn:ipsecproposal_list' ipsec_policy_id=object.pk %}
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1 +1,102 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "IPSec Profile" %}</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 "Mode" %}</th>
<td>{{ object.get_mode_display }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.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 "IKE Policy" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.ike_policy|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.ike_policy.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Version" %}</th>
<td>{{ object.ike_policy.get_version_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Mode" %}</th>
<td>{{ object.ike_policy.get_mode_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Proposals" %}</th>
<td>
<ul class="list-unstyled mb-0">
{% for proposal in object.ike_policy.proposals.all %}
<li>
<a href="{{ proposal.get_absolute_url }}">{{ proposal }}</a>
</li>
{% endfor %}
</ul>
</td>
</tr>
</table>
</div>
<div class="card">
<h2 class="card-header">{% trans "IPSec Policy" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.ipsec_policy|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.ipsec_policy.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Proposals" %}</th>
<td>
<ul class="list-unstyled mb-0">
{% for proposal in object.ipsec_policy.proposals.all %}
<li>
<a href="{{ proposal.get_absolute_url }}">{{ proposal }}</a>
</li>
{% endfor %}
</ul>
</td>
</tr>
<tr>
<th scope="row">{% trans "PFS Group" %}</th>
<td>{{ object.ipsec_policy.get_pfs_group_display }}</td>
</tr>
</table>
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1 +1,58 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "IPSec Proposal" %}</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 "Encryption algorithm" %}</th>
<td>{{ object.get_encryption_algorithm_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Authentication algorithm" %}</th>
<td>{{ object.get_authentication_algorithm_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "SA lifetime (seconds)" %}</th>
<td>{{ object.sa_lifetime_seconds|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "SA lifetime (KB)" %}</th>
<td>{{ object.sa_lifetime_data|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "IPSec Policies" %}</th>
<td>
<a href="{% url 'vpn:ipsecpolicy_list' %}?proposal_id={{ object.pk }}">{{ object.ipsec_policies.count }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1 +1,78 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% 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 "L2VPN Attributes" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Identifier" %}</th>
<td>{{ object.identifier|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Type" %}</th>
<td>{{ object.get_type_display }}</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 "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Tenant" %}</th>
<td>{{ object.tenant|linkify|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/tags.html' with tags=object.tags.all url='vpn:l2vpn_list' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row mb-3">
<div class="col col-12 col-md-6">
{% include 'inc/panel_table.html' with table=import_targets_table heading="Import Route Targets" %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panel_table.html' with table=export_targets_table heading="Export Route Targets" %}
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">
{% trans "Terminations" %}
{% if perms.vpn.add_l2vpntermination %}
<div class="card-actions">
<a href="{% url 'vpn:l2vpntermination_add' %}?l2vpn={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm{% if not object.can_add_termination %} disabled" aria-disabled="true{% endif %}">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Termination" %}
</a>
</div>
{% endif %}
</h2>
{% htmx_table 'vpn:l2vpntermination_list' l2vpn_id=object.pk %}
</div>
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1 +1,28 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "L2VPN Attributes" %}</h2>
<table class="table table-hover">
<tr>
<th scope="row">{% trans "L2VPN" %}</th>
<td>{{ object.l2vpn|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Assigned Object" %}</th>
<td>{{ object.assigned_object|linkify }}</td>
</tr>
</table>
</div>
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='vpn:l2vpntermination_list' %}
</div>
</div>
{% endblock %}

View File

@@ -1,34 +0,0 @@
{% load helpers %}
{% load i18n %}
<div class="card">
<h2 class="card-header">{% trans "IKE Policy" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.ike_policy|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.ike_policy.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Version" %}</th>
<td>{{ object.ike_policy.get_version_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Mode" %}</th>
<td>{{ object.ike_policy.get_mode_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Proposals" %}</th>
<td>
<ul class="list-unstyled mb-0">
{% for proposal in object.ike_policy.proposals.all %}
<li><a href="{{ proposal.get_absolute_url }}">{{ proposal }}</a></li>
{% endfor %}
</ul>
</td>
</tr>
</table>
</div>

View File

@@ -1,30 +0,0 @@
{% load helpers %}
{% load i18n %}
<div class="card">
<h2 class="card-header">{% trans "IPSec Policy" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.ipsec_policy|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.ipsec_policy.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Proposals" %}</th>
<td>
<ul class="list-unstyled mb-0">
{% for proposal in object.ipsec_policy.proposals.all %}
<li><a href="{{ proposal.get_absolute_url }}">{{ proposal }}</a></li>
{% endfor %}
</ul>
</td>
</tr>
<tr>
<th scope="row">{% trans "PFS Group" %}</th>
<td>{{ object.ipsec_policy.get_pfs_group_display }}</td>
</tr>
</table>
</div>

View File

@@ -1,4 +1,6 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block extra_controls %}
@@ -8,3 +10,77 @@
</a>
{% endif %}
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Tunnel" %}</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 "Status" %}</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</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>
<tr>
<th scope="row">{% trans "Encapsulation" %}</th>
<td>{{ object.get_encapsulation_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "IPSec profile" %}</th>
<td>{{ object.ipsec_profile|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Tunnel ID" %}</th>
<td>{{ object.tunnel_id|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Tenant" %}</th>
<td>
{% if object.tenant.group %}
{{ object.tenant.group|linkify }} /
{% endif %}
{{ object.tenant|linkify|placeholder }}
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">
{% trans "Terminations" %}
{% if perms.vpn.add_tunneltermination %}
<div class="card-actions">
<a href="{% url 'vpn:tunneltermination_add' %}?tunnel={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Termination" %}
</a>
</div>
{% endif %}
</h2>
{% htmx_table 'vpn:tunneltermination_list' tunnel_id=object.pk %}
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,13 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'vpn:tunnelgroup_list' %}">{% trans "Tunnel Groups" %}</a></li>
{% endblock %}
{% block extra_controls %}
{% if perms.vpn.add_tunnel %}
<a href="{% url 'vpn:tunnel_add' %}?group={{ object.pk }}" class="btn btn-primary">
@@ -8,3 +15,36 @@
</a>
{% endif %}
{% endblock extra_controls %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Tunnel 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>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/comments.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_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

@@ -1 +1,57 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Tunnel Termination" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Tunnel" %}</th>
<td>{{ object.tunnel|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Role" %}</th>
<td>{% badge object.get_role_display bg_color=object.get_role_color %}</td>
</tr>
<tr>
<th scope="row">
{% if object.termination.device %}
{% trans "Device" %}
{% elif object.termination.virtual_machine %}
{% trans "Virtual Machine" %}
{% endif %}
</th>
<td>{{ object.termination.parent_object|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Interface" %}</th>
<td>{{ object.termination|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Outside IP" %}</th>
<td>{{ object.outside_ip|linkify|placeholder }}</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">{% trans "Peer Terminations" %}</h2>
{% htmx_table 'vpn:tunneltermination_list' tunnel_id=object.tunnel.pk id__n=object.pk %}
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% 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-07 05:14+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
@@ -172,8 +172,8 @@ msgstr ""
#: netbox/dcim/forms/bulk_edit.py:323 netbox/dcim/forms/bulk_edit.py:673
#: netbox/dcim/forms/bulk_edit.py:860 netbox/dcim/forms/bulk_import.py:146
#: netbox/dcim/forms/bulk_import.py:247 netbox/dcim/forms/bulk_import.py:349
#: netbox/dcim/forms/bulk_import.py:640 netbox/dcim/forms/bulk_import.py:1609
#: netbox/dcim/forms/bulk_import.py:1637 netbox/dcim/forms/filtersets.py:106
#: netbox/dcim/forms/bulk_import.py:640 netbox/dcim/forms/bulk_import.py:1612
#: netbox/dcim/forms/bulk_import.py:1640 netbox/dcim/forms/filtersets.py:106
#: netbox/dcim/forms/filtersets.py:256 netbox/dcim/forms/filtersets.py:379
#: netbox/dcim/forms/filtersets.py:483 netbox/dcim/forms/filtersets.py:855
#: netbox/dcim/forms/filtersets.py:1073 netbox/dcim/forms/filtersets.py:1147
@@ -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
@@ -500,7 +500,7 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:865 netbox/dcim/forms/bulk_import.py:886
#: netbox/dcim/forms/bulk_import.py:972 netbox/dcim/forms/bulk_import.py:1101
#: netbox/dcim/forms/bulk_import.py:1120 netbox/dcim/forms/bulk_import.py:1465
#: netbox/dcim/forms/bulk_import.py:1674 netbox/dcim/forms/filtersets.py:1104
#: netbox/dcim/forms/bulk_import.py:1677 netbox/dcim/forms/filtersets.py:1104
#: netbox/dcim/forms/filtersets.py:1205 netbox/dcim/forms/filtersets.py:1333
#: netbox/dcim/forms/filtersets.py:1424 netbox/dcim/forms/filtersets.py:1444
#: netbox/dcim/forms/filtersets.py:1464 netbox/dcim/forms/filtersets.py:1484
@@ -572,7 +572,7 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:265 netbox/dcim/forms/bulk_import.py:374
#: netbox/dcim/forms/bulk_import.py:605 netbox/dcim/forms/bulk_import.py:765
#: netbox/dcim/forms/bulk_import.py:1230 netbox/dcim/forms/bulk_import.py:1453
#: netbox/dcim/forms/bulk_import.py:1669 netbox/dcim/forms/bulk_import.py:1732
#: netbox/dcim/forms/bulk_import.py:1672 netbox/dcim/forms/bulk_import.py:1735
#: netbox/dcim/forms/filtersets.py:208 netbox/dcim/forms/filtersets.py:268
#: netbox/dcim/forms/filtersets.py:396 netbox/dcim/forms/filtersets.py:504
#: netbox/dcim/forms/filtersets.py:901 netbox/dcim/forms/filtersets.py:1024
@@ -646,7 +646,7 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:122 netbox/dcim/forms/bulk_import.py:167
#: netbox/dcim/forms/bulk_import.py:258 netbox/dcim/forms/bulk_import.py:379
#: netbox/dcim/forms/bulk_import.py:579 netbox/dcim/forms/bulk_import.py:1471
#: netbox/dcim/forms/bulk_import.py:1725 netbox/dcim/forms/filtersets.py:143
#: netbox/dcim/forms/bulk_import.py:1728 netbox/dcim/forms/filtersets.py:143
#: netbox/dcim/forms/filtersets.py:202 netbox/dcim/forms/filtersets.py:235
#: netbox/dcim/forms/filtersets.py:363 netbox/dcim/forms/filtersets.py:442
#: netbox/dcim/forms/filtersets.py:463 netbox/dcim/forms/filtersets.py:823
@@ -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
@@ -1051,7 +1047,7 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:105 netbox/dcim/forms/bulk_import.py:164
#: netbox/dcim/forms/bulk_import.py:267 netbox/dcim/forms/bulk_import.py:376
#: netbox/dcim/forms/bulk_import.py:607 netbox/dcim/forms/bulk_import.py:767
#: netbox/dcim/forms/bulk_import.py:1232 netbox/dcim/forms/bulk_import.py:1671
#: netbox/dcim/forms/bulk_import.py:1232 netbox/dcim/forms/bulk_import.py:1674
#: netbox/ipam/forms/bulk_import.py:200 netbox/ipam/forms/bulk_import.py:264
#: netbox/ipam/forms/bulk_import.py:300 netbox/ipam/forms/bulk_import.py:512
#: netbox/ipam/forms/bulk_import.py:525
@@ -1067,8 +1063,8 @@ msgstr ""
#: netbox/circuits/forms/bulk_import.py:235
#: netbox/dcim/forms/bulk_import.py:126 netbox/dcim/forms/bulk_import.py:171
#: netbox/dcim/forms/bulk_import.py:383 netbox/dcim/forms/bulk_import.py:583
#: netbox/dcim/forms/bulk_import.py:1475 netbox/dcim/forms/bulk_import.py:1666
#: netbox/dcim/forms/bulk_import.py:1729 netbox/ipam/forms/bulk_import.py:49
#: netbox/dcim/forms/bulk_import.py:1475 netbox/dcim/forms/bulk_import.py:1669
#: netbox/dcim/forms/bulk_import.py:1732 netbox/ipam/forms/bulk_import.py:49
#: netbox/ipam/forms/bulk_import.py:78 netbox/ipam/forms/bulk_import.py:106
#: netbox/ipam/forms/bulk_import.py:126 netbox/ipam/forms/bulk_import.py:146
#: netbox/ipam/forms/bulk_import.py:174 netbox/ipam/forms/bulk_import.py:259
@@ -1146,8 +1142,8 @@ msgstr ""
#: netbox/dcim/forms/bulk_edit.py:439 netbox/dcim/forms/bulk_edit.py:678
#: netbox/dcim/forms/bulk_edit.py:727 netbox/dcim/forms/bulk_edit.py:869
#: netbox/dcim/forms/bulk_import.py:252 netbox/dcim/forms/bulk_import.py:355
#: netbox/dcim/forms/bulk_import.py:646 netbox/dcim/forms/bulk_import.py:1615
#: netbox/dcim/forms/bulk_import.py:1649 netbox/dcim/forms/filtersets.py:114
#: netbox/dcim/forms/bulk_import.py:646 netbox/dcim/forms/bulk_import.py:1618
#: netbox/dcim/forms/bulk_import.py:1652 netbox/dcim/forms/filtersets.py:114
#: netbox/dcim/forms/filtersets.py:358 netbox/dcim/forms/filtersets.py:393
#: netbox/dcim/forms/filtersets.py:438 netbox/dcim/forms/filtersets.py:491
#: netbox/dcim/forms/filtersets.py:820 netbox/dcim/forms/filtersets.py:864
@@ -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
@@ -1870,7 +1859,7 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:1096 netbox/dcim/forms/bulk_import.py:1115
#: netbox/dcim/forms/bulk_import.py:1134 netbox/dcim/forms/bulk_import.py:1146
#: netbox/dcim/forms/bulk_import.py:1194 netbox/dcim/forms/bulk_import.py:1316
#: netbox/dcim/forms/bulk_import.py:1719 netbox/dcim/forms/connections.py:34
#: netbox/dcim/forms/bulk_import.py:1722 netbox/dcim/forms/connections.py:34
#: netbox/dcim/forms/filtersets.py:156 netbox/dcim/forms/filtersets.py:1021
#: netbox/dcim/forms/filtersets.py:1054 netbox/dcim/forms/filtersets.py:1202
#: netbox/dcim/forms/filtersets.py:1418 netbox/dcim/forms/filtersets.py:1441
@@ -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
@@ -2475,7 +2461,7 @@ msgstr ""
#: netbox/core/models/files.py:30 netbox/core/models/jobs.py:60
#: netbox/extras/models/models.py:852 netbox/extras/models/notifications.py:39
#: netbox/extras/models/notifications.py:195
#: netbox/netbox/models/features.py:61 netbox/users/models/tokens.py:51
#: netbox/netbox/models/features.py:62 netbox/users/models/tokens.py:51
msgid "created"
msgstr ""
@@ -2593,7 +2579,7 @@ msgid ""
msgstr ""
#: netbox/core/models/data.py:290 netbox/core/models/files.py:34
#: netbox/netbox/models/features.py:67
#: netbox/netbox/models/features.py:68
msgid "last updated"
msgstr ""
@@ -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 ""
@@ -4434,8 +4420,8 @@ msgstr ""
#: netbox/dcim/forms/bulk_edit.py:438 netbox/dcim/forms/bulk_edit.py:891
#: netbox/dcim/forms/bulk_import.py:362 netbox/dcim/forms/bulk_import.py:365
#: netbox/dcim/forms/bulk_import.py:653 netbox/dcim/forms/bulk_import.py:1656
#: netbox/dcim/forms/bulk_import.py:1660 netbox/dcim/forms/filtersets.py:123
#: netbox/dcim/forms/bulk_import.py:653 netbox/dcim/forms/bulk_import.py:1659
#: netbox/dcim/forms/bulk_import.py:1663 netbox/dcim/forms/filtersets.py:123
#: netbox/dcim/forms/filtersets.py:359 netbox/dcim/forms/filtersets.py:448
#: netbox/dcim/forms/filtersets.py:462 netbox/dcim/forms/filtersets.py:501
#: netbox/dcim/forms/filtersets.py:874 netbox/dcim/forms/filtersets.py:1086
@@ -4642,17 +4628,17 @@ msgstr ""
msgid "Domain"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:886 netbox/dcim/forms/bulk_import.py:1643
#: netbox/dcim/forms/bulk_edit.py:886 netbox/dcim/forms/bulk_import.py:1646
#: netbox/dcim/forms/filtersets.py:1316 netbox/dcim/forms/model_forms.py:865
msgid "Power panel"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:908 netbox/dcim/forms/bulk_import.py:1679
#: netbox/dcim/forms/bulk_edit.py:908 netbox/dcim/forms/bulk_import.py:1682
#: netbox/dcim/forms/filtersets.py:1338 netbox/templates/dcim/powerfeed.html:83
msgid "Supply"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:914 netbox/dcim/forms/bulk_import.py:1684
#: netbox/dcim/forms/bulk_edit.py:914 netbox/dcim/forms/bulk_import.py:1687
#: netbox/dcim/forms/filtersets.py:1343 netbox/templates/dcim/powerfeed.html:95
msgid "Phase"
msgstr ""
@@ -4900,7 +4886,7 @@ msgid "available options"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:149 netbox/dcim/forms/bulk_import.py:643
#: netbox/dcim/forms/bulk_import.py:1640 netbox/ipam/forms/bulk_import.py:493
#: netbox/dcim/forms/bulk_import.py:1643 netbox/ipam/forms/bulk_import.py:493
#: netbox/virtualization/forms/bulk_import.py:64
#: netbox/virtualization/forms/bulk_import.py:102
msgid "Assigned site"
@@ -4963,7 +4949,7 @@ msgstr ""
msgid "Parent site"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:359 netbox/dcim/forms/bulk_import.py:1653
#: netbox/dcim/forms/bulk_import.py:359 netbox/dcim/forms/bulk_import.py:1656
msgid "Rack's location (if any)"
msgstr ""
@@ -5028,7 +5014,7 @@ msgstr ""
msgid "Limit platform assignments to this manufacturer"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:576 netbox/dcim/forms/bulk_import.py:1722
#: netbox/dcim/forms/bulk_import.py:576 netbox/dcim/forms/bulk_import.py:1725
#: netbox/tenancy/forms/bulk_import.py:116
msgid "Assigned role"
msgstr ""
@@ -5378,24 +5364,24 @@ msgstr ""
msgid "Color name (e.g. \"Red\") or hex code (e.g. \"f44336\")"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1539
#: netbox/dcim/forms/bulk_import.py:1542
#, python-brace-format
msgid "Side {side_upper}: {device} {termination_object} is already connected"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1545
#: netbox/dcim/forms/bulk_import.py:1548
#, python-brace-format
msgid "{side_upper} side termination not found: {device} {name}"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1566
#: netbox/dcim/forms/bulk_import.py:1569
#, python-brace-format
msgid ""
"{color} did not match any used color name and was longer than six "
"characters: invalid hex."
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1591 netbox/dcim/forms/model_forms.py:900
#: netbox/dcim/forms/bulk_import.py:1594 netbox/dcim/forms/model_forms.py:900
#: netbox/dcim/tables/devices.py:1124
#: netbox/templates/dcim/panels/virtual_chassis_members.html:10
#: netbox/templates/dcim/virtualchassis.html:17
@@ -5403,49 +5389,49 @@ msgstr ""
msgid "Master"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1595
#: netbox/dcim/forms/bulk_import.py:1598
msgid "Master device"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1612
#: netbox/dcim/forms/bulk_import.py:1615
msgid "Name of parent site"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1646
#: netbox/dcim/forms/bulk_import.py:1649
msgid "Upstream power panel"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1676
#: netbox/dcim/forms/bulk_import.py:1679
msgid "Primary or redundant"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1681
#: netbox/dcim/forms/bulk_import.py:1684
msgid "Supply type (AC/DC)"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1686
#: netbox/dcim/forms/bulk_import.py:1689
msgid "Single or three-phase"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1736 netbox/dcim/forms/model_forms.py:1875
#: netbox/dcim/forms/bulk_import.py:1739 netbox/dcim/forms/model_forms.py:1875
#: netbox/dcim/ui/panels.py:108
#: netbox/templates/dcim/virtualdevicecontext.html:30
#: netbox/virtualization/ui/panels.py:28
msgid "Primary IPv4"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1740
#: netbox/dcim/forms/bulk_import.py:1743
msgid "IPv4 address with mask, e.g. 1.2.3.4/24"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1743 netbox/dcim/forms/model_forms.py:1884
#: netbox/dcim/forms/bulk_import.py:1746 netbox/dcim/forms/model_forms.py:1884
#: netbox/dcim/ui/panels.py:113
#: netbox/templates/dcim/virtualdevicecontext.html:41
#: netbox/virtualization/ui/panels.py:33
msgid "Primary IPv6"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1747
#: netbox/dcim/forms/bulk_import.py:1750
msgid "IPv6 address with prefix length, e.g. 2001:db8::1/64"
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 ""
@@ -12080,46 +12063,46 @@ msgstr ""
msgid "Lookup"
msgstr ""
#: netbox/netbox/models/features.py:310
#: netbox/netbox/models/features.py:311
#, python-brace-format
msgid "Invalid value for custom field '{name}': {error}"
msgstr ""
#: netbox/netbox/models/features.py:319
#: netbox/netbox/models/features.py:320
#, python-brace-format
msgid "Custom field '{name}' must have a unique value."
msgstr ""
#: netbox/netbox/models/features.py:326
#: netbox/netbox/models/features.py:327
#, python-brace-format
msgid "Missing required custom field '{name}'."
msgstr ""
#: netbox/netbox/models/features.py:519
#: netbox/netbox/models/features.py:521
msgid "Remote data source"
msgstr ""
#: netbox/netbox/models/features.py:529
#: netbox/netbox/models/features.py:531
msgid "data path"
msgstr ""
#: netbox/netbox/models/features.py:533
#: netbox/netbox/models/features.py:535
msgid "Path to remote file (relative to data source root)"
msgstr ""
#: netbox/netbox/models/features.py:536
#: netbox/netbox/models/features.py:538
msgid "auto sync enabled"
msgstr ""
#: netbox/netbox/models/features.py:538
#: netbox/netbox/models/features.py:540
msgid "Enable automatic synchronization of data when the data file is updated"
msgstr ""
#: netbox/netbox/models/features.py:541
#: netbox/netbox/models/features.py:543
msgid "date synced"
msgstr ""
#: netbox/netbox/models/features.py:634
#: netbox/netbox/models/features.py:636
#, python-brace-format
msgid "{class_name} must implement a sync_data() method."
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}"
@@ -16635,7 +16615,7 @@ msgstr ""
msgid "Unknown app_label/model_name for {name}"
msgstr ""
#: netbox/utilities/request.py:92
#: netbox/utilities/request.py:95
#, python-brace-format
msgid "Invalid IP address set for {header}: {ip}"
msgstr ""

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 {

View File

@@ -1,85 +0,0 @@
from django.utils.translation import gettext_lazy as _
from netbox.ui import attrs, panels
class TunnelGroupPanel(panels.OrganizationalObjectPanel):
pass
class TunnelPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
status = attrs.ChoiceAttr('status')
group = attrs.RelatedObjectAttr('group', linkify=True)
description = attrs.TextAttr('description')
encapsulation = attrs.ChoiceAttr('encapsulation')
ipsec_profile = attrs.RelatedObjectAttr('ipsec_profile', linkify=True, label=_('IPSec profile'))
tunnel_id = attrs.TextAttr('tunnel_id', label=_('Tunnel ID'))
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
class TunnelTerminationPanel(panels.ObjectAttributesPanel):
tunnel = attrs.RelatedObjectAttr('tunnel', linkify=True)
role = attrs.ChoiceAttr('role')
parent_object = attrs.RelatedObjectAttr(
'termination.parent_object', linkify=True, label=_('Parent')
)
termination = attrs.RelatedObjectAttr('termination', linkify=True, label=_('Interface'))
outside_ip = attrs.RelatedObjectAttr('outside_ip', linkify=True, label=_('Outside IP'))
class IKEProposalPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
description = attrs.TextAttr('description')
authentication_method = attrs.ChoiceAttr('authentication_method', label=_('Authentication method'))
encryption_algorithm = attrs.ChoiceAttr('encryption_algorithm', label=_('Encryption algorithm'))
authentication_algorithm = attrs.ChoiceAttr('authentication_algorithm', label=_('Authentication algorithm'))
group = attrs.ChoiceAttr('group', label=_('DH group'))
sa_lifetime = attrs.TextAttr('sa_lifetime', label=_('SA lifetime (seconds)'))
class IKEPolicyPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
description = attrs.TextAttr('description')
version = attrs.ChoiceAttr('version', label=_('IKE version'))
mode = attrs.ChoiceAttr('mode')
preshared_key = attrs.TemplatedAttr(
'preshared_key',
label=_('Pre-shared key'),
template_name='vpn/attrs/preshared_key.html',
)
class IPSecProposalPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
description = attrs.TextAttr('description')
encryption_algorithm = attrs.ChoiceAttr('encryption_algorithm', label=_('Encryption algorithm'))
authentication_algorithm = attrs.ChoiceAttr('authentication_algorithm', label=_('Authentication algorithm'))
sa_lifetime_seconds = attrs.TextAttr('sa_lifetime_seconds', label=_('SA lifetime (seconds)'))
sa_lifetime_data = attrs.TextAttr('sa_lifetime_data', label=_('SA lifetime (KB)'))
class IPSecPolicyPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
description = attrs.TextAttr('description')
pfs_group = attrs.ChoiceAttr('pfs_group', label=_('PFS group'))
class IPSecProfilePanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
description = attrs.TextAttr('description')
mode = attrs.ChoiceAttr('mode')
class L2VPNPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
identifier = attrs.TextAttr('identifier')
type = attrs.ChoiceAttr('type')
status = attrs.ChoiceAttr('status')
description = attrs.TextAttr('description')
tenant = attrs.RelatedObjectAttr('tenant', linkify=True)
class L2VPNTerminationPanel(panels.ObjectAttributesPanel):
l2vpn = attrs.RelatedObjectAttr('l2vpn', linkify=True, label=_('L2VPN'))
assigned_object = attrs.RelatedObjectAttr('assigned_object', linkify=True, label=_('Assigned object'))

View File

@@ -1,24 +1,11 @@
from django.utils.translation import gettext_lazy as _
from extras.ui.panels import CustomFieldsPanel, TagsPanel
from ipam.tables import RouteTargetTable
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
from netbox.ui import actions, layout
from netbox.ui.panels import (
CommentsPanel,
ContextTablePanel,
ObjectsTablePanel,
PluginContentPanel,
RelatedObjectsPanel,
TemplatePanel,
)
from netbox.views import generic
from utilities.query import count_related
from utilities.views import GetRelatedModelsMixin, register_model_view
from . import filtersets, forms, tables
from .models import *
from .ui import panels
#
# Tunnel groups
@@ -38,17 +25,6 @@ class TunnelGroupListView(generic.ObjectListView):
@register_model_view(TunnelGroup)
class TunnelGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = TunnelGroup.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.TunnelGroupPanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CommentsPanel(),
CustomFieldsPanel(),
],
)
def get_extra_context(self, request, instance):
return {
@@ -116,30 +92,6 @@ class TunnelListView(generic.ObjectListView):
@register_model_view(Tunnel)
class TunnelView(generic.ObjectView):
queryset = Tunnel.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.TunnelPanel(),
],
right_panels=[
CustomFieldsPanel(),
TagsPanel(),
CommentsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
'vpn.tunneltermination',
filters={'tunnel_id': lambda ctx: ctx['object'].pk},
actions=[
actions.AddObject(
'vpn.tunneltermination',
url_params={'tunnel': lambda ctx: ctx['object'].pk},
label=_('Add a Termination'),
),
],
title=_('Terminations'),
),
],
)
@register_model_view(Tunnel, 'add', detail=False)
@@ -208,25 +160,6 @@ class TunnelTerminationListView(generic.ObjectListView):
@register_model_view(TunnelTermination)
class TunnelTerminationView(generic.ObjectView):
queryset = TunnelTermination.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.TunnelTerminationPanel(),
],
right_panels=[
CustomFieldsPanel(),
TagsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
'vpn.tunneltermination',
filters={
'tunnel_id': lambda ctx: ctx['object'].tunnel.pk,
'id__n': lambda ctx: ctx['object'].pk,
},
title=_('Peer Terminations'),
),
],
)
@register_model_view(TunnelTermination, 'add', detail=False)
@@ -277,23 +210,6 @@ class IKEProposalListView(generic.ObjectListView):
@register_model_view(IKEProposal)
class IKEProposalView(generic.ObjectView):
queryset = IKEProposal.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.IKEProposalPanel(),
],
right_panels=[
CustomFieldsPanel(),
CommentsPanel(),
TagsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
'vpn.ikepolicy',
filters={'ike_proposal_id': lambda ctx: ctx['object'].pk},
title=_('IKE Policies'),
),
],
)
@register_model_view(IKEProposal, 'add', detail=False)
@@ -348,31 +264,8 @@ class IKEPolicyListView(generic.ObjectListView):
@register_model_view(IKEPolicy)
class IKEPolicyView(GetRelatedModelsMixin, generic.ObjectView):
class IKEPolicyView(generic.ObjectView):
queryset = IKEPolicy.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.IKEPolicyPanel(),
],
right_panels=[
CustomFieldsPanel(),
CommentsPanel(),
TagsPanel(),
RelatedObjectsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
'vpn.ikeproposal',
filters={'ike_policy_id': lambda ctx: ctx['object'].pk},
title=_('Proposals'),
),
],
)
def get_extra_context(self, request, instance):
return {
'related_models': self.get_related_models(request, instance),
}
@register_model_view(IKEPolicy, 'add', detail=False)
@@ -429,23 +322,6 @@ class IPSecProposalListView(generic.ObjectListView):
@register_model_view(IPSecProposal)
class IPSecProposalView(generic.ObjectView):
queryset = IPSecProposal.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.IPSecProposalPanel(),
],
right_panels=[
CustomFieldsPanel(),
CommentsPanel(),
TagsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
'vpn.ipsecpolicy',
filters={'ipsec_proposal_id': lambda ctx: ctx['object'].pk},
title=_('IPSec Policies'),
),
],
)
@register_model_view(IPSecProposal, 'add', detail=False)
@@ -500,31 +376,8 @@ class IPSecPolicyListView(generic.ObjectListView):
@register_model_view(IPSecPolicy)
class IPSecPolicyView(GetRelatedModelsMixin, generic.ObjectView):
class IPSecPolicyView(generic.ObjectView):
queryset = IPSecPolicy.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.IPSecPolicyPanel(),
],
right_panels=[
CustomFieldsPanel(),
CommentsPanel(),
TagsPanel(),
RelatedObjectsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
'vpn.ipsecproposal',
filters={'ipsec_policy_id': lambda ctx: ctx['object'].pk},
title=_('Proposals'),
),
],
)
def get_extra_context(self, request, instance):
return {
'related_models': self.get_related_models(request, instance),
}
@register_model_view(IPSecPolicy, 'add', detail=False)
@@ -581,18 +434,6 @@ class IPSecProfileListView(generic.ObjectListView):
@register_model_view(IPSecProfile)
class IPSecProfileView(generic.ObjectView):
queryset = IPSecProfile.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.IPSecProfilePanel(),
TagsPanel(),
CustomFieldsPanel(),
CommentsPanel(),
],
right_panels=[
TemplatePanel('vpn/panels/ipsecprofile_ike_policy.html'),
TemplatePanel('vpn/panels/ipsecprofile_ipsec_policy.html'),
],
)
@register_model_view(IPSecProfile, 'add', detail=False)
@@ -649,45 +490,6 @@ class L2VPNListView(generic.ObjectListView):
@register_model_view(L2VPN)
class L2VPNView(generic.ObjectView):
queryset = L2VPN.objects.all()
layout = layout.Layout(
layout.Row(
layout.Column(
panels.L2VPNPanel(),
TagsPanel(),
PluginContentPanel('left_page'),
),
layout.Column(
CustomFieldsPanel(),
CommentsPanel(),
PluginContentPanel('right_page'),
),
),
layout.Row(
layout.Column(
ContextTablePanel('import_targets_table', title=_('Import Route Targets')),
),
layout.Column(
ContextTablePanel('export_targets_table', title=_('Export Route Targets')),
),
),
layout.Row(
layout.Column(
ObjectsTablePanel(
'vpn.l2vpntermination',
filters={'l2vpn_id': lambda ctx: ctx['object'].pk},
actions=[
actions.AddObject(
'vpn.l2vpntermination',
url_params={'l2vpn': lambda ctx: ctx['object'].pk},
label=_('Add a Termination'),
),
],
title=_('Terminations'),
),
PluginContentPanel('full_width_page'),
),
),
)
def get_extra_context(self, request, instance):
import_targets_table = RouteTargetTable(
@@ -762,15 +564,6 @@ class L2VPNTerminationListView(generic.ObjectListView):
@register_model_view(L2VPNTermination)
class L2VPNTerminationView(generic.ObjectView):
queryset = L2VPNTermination.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.L2VPNTerminationPanel(),
],
right_panels=[
CustomFieldsPanel(),
TagsPanel(),
],
)
@register_model_view(L2VPNTermination, 'add', detail=False)