Merge branch 'netbox-community:develop' into script_rq_queue_name

This commit is contained in:
jchambers2012 2024-06-20 23:52:18 -04:00 committed by GitHub
commit e2f2799f73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 262 additions and 121 deletions

1
.gitignore vendored
View File

@ -28,3 +28,4 @@ netbox.pid
.idea .idea
.coverage .coverage
.vscode .vscode
.python-version

View File

@ -24,7 +24,7 @@ If you believe you've uncovered a security vulnerability and wish to report it c
Please note that we **DO NOT** accept reports generated by automated tooling which merely suggest that a file or file(s) _may_ be vulnerable under certain conditions, as these are most often innocuous. Please note that we **DO NOT** accept reports generated by automated tooling which merely suggest that a file or file(s) _may_ be vulnerable under certain conditions, as these are most often innocuous.
If you believe that you've found a vulnerability which meets all of these conditions, please [submit a draft security advisory](https://github.com/netbox-community/netbox/security/advisories/new) on GitHub, or email a brief description of the suspected bug and instructions for reproduction to **security@netbox.dev**. For any security concerns regarding NetBox deployed via Docker, please see the [netbox-docker](https://github.com/netbox-community/netbox-docker) project. If you believe that you've found a vulnerability which meets all of these conditions, please [submit a draft security advisory](https://github.com/netbox-community/netbox/security/advisories/new) on GitHub. For any security concerns regarding NetBox deployed via Docker, please see the [netbox-docker](https://github.com/netbox-community/netbox-docker) project.
### Bug Bounties ### Bug Bounties

View File

@ -2,6 +2,26 @@
## v4.0.6 (FUTURE) ## v4.0.6 (FUTURE)
### Enhancements
* [#15348](https://github.com/netbox-community/netbox/issues/15348) - Show saved filters alongside quick search on object list views
* [#15794](https://github.com/netbox-community/netbox/issues/15794) - Dynamically populate related objects in UI views
* [#16256](https://github.com/netbox-community/netbox/issues/16256) - Enable alphabetical ordering of bookmarks on dashboard
### Bug Fixes
* [#13925](https://github.com/netbox-community/netbox/issues/13925) - Fix support for "zulu" (UTC) timestamps for custom fields
* [#14829](https://github.com/netbox-community/netbox/issues/14829) - Fix support for simple conditions (without AND/OR) in event rules
* [#16143](https://github.com/netbox-community/netbox/issues/16143) - Display timestamps in tables in the configured timezone
* [#16416](https://github.com/netbox-community/netbox/issues/16416) - Retain dark/light mode toggle on mobile view
* [#16444](https://github.com/netbox-community/netbox/issues/16444) - Disable ordering circuits list by A/Z termination
* [#16450](https://github.com/netbox-community/netbox/issues/16450) - Searching for rack unit in form dropdown should be case-insensitive
* [#16452](https://github.com/netbox-community/netbox/issues/16452) - Fix sizing of buttons within object attribute panels
* [#16454](https://github.com/netbox-community/netbox/issues/16454) - Address DNS lookup bug in `django-debug-toolbar
* [#16460](https://github.com/netbox-community/netbox/issues/16460) - Omit spaces from telephone number URLs
* [#16512](https://github.com/netbox-community/netbox/issues/16512) - Restore a user's preferred language (if any) on login
* [#16542](https://github.com/netbox-community/netbox/issues/16542) - Fix bulk form operations when HTMX is enabled
--- ---
## v4.0.5 (2024-06-06) ## v4.0.5 (2024-06-06)

View File

@ -104,10 +104,16 @@ class LoginView(View):
# Ensure the user has a UserConfig defined. (This should normally be handled by # Ensure the user has a UserConfig defined. (This should normally be handled by
# create_userconfig() on user creation.) # create_userconfig() on user creation.)
if not hasattr(request.user, 'config'): if not hasattr(request.user, 'config'):
config = get_config() request.user.config = get_config()
UserConfig(user=request.user, data=config.DEFAULT_USER_PREFERENCES).save() UserConfig(user=request.user, data=request.user.config.DEFAULT_USER_PREFERENCES).save()
return self.redirect_to_next(request, logger) response = self.redirect_to_next(request, logger)
# Set the user's preferred language (if any)
if language := request.user.config.get('locale.language'):
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
return response
else: else:
logger.debug(f"Login form validation failed for username: {form['username'].value()}") logger.debug(f"Login form validation failed for username: {form['username'].value()}")
@ -145,9 +151,10 @@ class LogoutView(View):
logger.info(f"User {username} has logged out") logger.info(f"User {username} has logged out")
messages.info(request, "You have logged out.") messages.info(request, "You have logged out.")
# Delete session key cookie (if set) upon logout # Delete session key & language cookies (if set) upon logout
response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL)) response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL))
response.delete_cookie('session_key') response.delete_cookie('session_key')
response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
return response return response

View File

@ -63,10 +63,12 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
status = columns.ChoiceFieldColumn() status = columns.ChoiceFieldColumn()
termination_a = tables.TemplateColumn( termination_a = tables.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK, template_code=CIRCUITTERMINATION_LINK,
orderable=False,
verbose_name=_('Side A') verbose_name=_('Side A')
) )
termination_z = tables.TemplateColumn( termination_z = tables.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK, template_code=CIRCUITTERMINATION_LINK,
orderable=False,
verbose_name=_('Side Z') verbose_name=_('Side Z')
) )
commit_rate = CommitRateColumn( commit_rate = CommitRateColumn(

View File

@ -219,9 +219,9 @@ class RackViewSet(NetBoxModelViewSet):
) )
# Enable filtering rack units by ID # Enable filtering rack units by ID
q = data['q'] if q := data['q']:
if q: q = q.lower()
elevation = [u for u in elevation if q in str(u['id']) or q in str(u['name'])] elevation = [u for u in elevation if q in str(u['id']) or q in str(u['name']).lower()]
page = self.paginate_queryset(elevation) page = self.paginate_queryset(elevation)
if page is not None: if page is not None:

View File

@ -465,7 +465,10 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
label=_('Cluster'), label=_('Cluster'),
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
required=False, required=False,
selector=True selector=True,
query_params={
'site_id': ['$site', 'null']
},
) )
comments = CommentField() comments = CommentField()
local_context_data = JSONField( local_context_data = JSONField(

View File

@ -8,6 +8,7 @@ from dcim.models import *
from extras.models import CustomField from extras.models import CustomField
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.data import drange from utilities.data import drange
from virtualization.models import Cluster, ClusterType
class LocationTestCase(TestCase): class LocationTestCase(TestCase):
@ -533,6 +534,36 @@ class DeviceTestCase(TestCase):
device2.full_clean() device2.full_clean()
device2.save() device2.save()
def test_device_mismatched_site_cluster(self):
cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
Cluster.objects.create(name='Cluster 1', type=cluster_type)
sites = (
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
)
Site.objects.bulk_create(sites)
clusters = (
Cluster(name='Cluster 1', type=cluster_type, site=sites[0]),
Cluster(name='Cluster 2', type=cluster_type, site=sites[1]),
Cluster(name='Cluster 3', type=cluster_type, site=None),
)
Cluster.objects.bulk_create(clusters)
device_type = DeviceType.objects.first()
device_role = DeviceRole.objects.first()
# Device with site only should pass
Device(name='device1', site=sites[0], device_type=device_type, role=device_role).full_clean()
# Device with site, cluster non-site should pass
Device(name='device1', site=sites[0], device_type=device_type, role=device_role, cluster=clusters[2]).full_clean()
# Device with mismatched site & cluster should fail
with self.assertRaises(ValidationError):
Device(name='device1', site=sites[0], device_type=device_type, role=device_role, cluster=clusters[1]).full_clean()
class CableTestCase(TestCase): class CableTestCase(TestCase):

View File

@ -660,6 +660,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Validate date & time # Validate date & time
elif self.type == CustomFieldTypeChoices.TYPE_DATETIME: elif self.type == CustomFieldTypeChoices.TYPE_DATETIME:
if type(value) is not datetime: if type(value) is not datetime:
# Work around UTC issue for Python < 3.11; see
# https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat
if type(value) is str and value.endswith('Z'):
value = f'{value[:-1]}+00:00'
try: try:
datetime.fromisoformat(value) datetime.fromisoformat(value)
except ValueError: except ValueError:

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,30 @@
import { isTruthy } from '../util';
/**
* Handle saved filter change event.
*
* @param event "change" event for the saved filter select
*/
function handleSavedFilterChange(event: Event): void {
const savedFilter = event.currentTarget as HTMLSelectElement;
let baseUrl = savedFilter.baseURI.split('?')[0];
const preFilter = '?';
const selectedOptions = Array.from(savedFilter.options)
.filter(option => option.selected)
.map(option => `filter_id=${option.value}`)
.join('&');
baseUrl += `${preFilter}${selectedOptions}`;
document.location.href = baseUrl;
}
export function initSavedFilterSelect(): void {
const divResults = document.getElementById('results');
if (isTruthy(divResults)) {
const savedFilterSelect = document.getElementById('id_filter_id');
if (isTruthy(savedFilterSelect)) {
savedFilterSelect.addEventListener('change', handleSavedFilterChange);
}
}
}

View File

@ -13,6 +13,7 @@ import { initSideNav } from './sidenav';
import { initDashboard } from './dashboard'; import { initDashboard } from './dashboard';
import { initRackElevation } from './racks'; import { initRackElevation } from './racks';
import { initHtmx } from './htmx'; import { initHtmx } from './htmx';
import { initSavedFilterSelect } from './forms/savedFiltersSelect';
function initDocument(): void { function initDocument(): void {
for (const init of [ for (const init of [
@ -31,6 +32,7 @@ function initDocument(): void {
initDashboard, initDashboard,
initRackElevation, initRackElevation,
initHtmx, initHtmx,
initSavedFilterSelect,
]) { ]) {
init(); init();
} }

View File

@ -7,6 +7,7 @@
// Overrides of external libraries // Overrides of external libraries
@import 'overrides/bootstrap'; @import 'overrides/bootstrap';
@import 'overrides/tabler'; @import 'overrides/tabler';
@import 'overrides/tomselect';
// Transitional styling to ease migration of templates from NetBox v3.x // Transitional styling to ease migration of templates from NetBox v3.x
@import 'transitional/badges'; @import 'transitional/badges';

View File

@ -0,0 +1,8 @@
.ts-wrapper.multi {
.ts-control {
padding: 7px 7px 3px 7px;
div {
margin: 0 4px 4px 0;
}
}
}

View File

@ -35,6 +35,7 @@ Blocks:
{# User menu (mobile view) #} {# User menu (mobile view) #}
<div class="navbar-nav flex-row d-lg-none"> <div class="navbar-nav flex-row d-lg-none">
{% include 'inc/light_toggle.html' %}
{% include 'inc/user_menu.html' %} {% include 'inc/user_menu.html' %}
</div> </div>
@ -52,14 +53,7 @@ Blocks:
<div class="navbar-nav flex-row align-items-center order-md-last"> <div class="navbar-nav flex-row align-items-center order-md-last">
{# Dark/light mode toggle #} {# Dark/light mode toggle #}
<div class="d-none d-md-flex"> {% include 'inc/light_toggle.html' %}
<button class="btn color-mode-toggle hide-theme-dark" title="{% trans "Enable dark mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="mdi mdi-lightbulb"></i>
</button>
<button class="btn color-mode-toggle hide-theme-light" title="{% trans "Enable light mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="mdi mdi-lightbulb-on"></i>
</button>
</div>
{# User menu #} {# User menu #}
{% include 'inc/user_menu.html' %} {% include 'inc/user_menu.html' %}

View File

@ -28,7 +28,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">{% trans "Rack" %}</th> <th scope="row">{% trans "Rack" %}</th>
<td class="d-flex justify-content-between"> <td class="d-flex justify-content-between align-items-start">
{% if object.rack %} {% if object.rack %}
{{ object.rack|linkify }} {{ object.rack|linkify }}
<a href="{{ object.rack.get_absolute_url }}?device={{ object.pk }}" class="btn btn-primary btn-sm d-print-none" title="{% trans "Highlight device in rack" %}"> <a href="{{ object.rack.get_absolute_url }}?device={{ object.pk }}" class="btn btn-primary btn-sm d-print-none" title="{% trans "Highlight device in rack" %}">

View File

@ -73,7 +73,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">{% trans "Physical Address" %}</th> <th scope="row">{% trans "Physical Address" %}</th>
<td class="d-flex justify-content-between"> <td class="d-flex justify-content-between align-items-start">
{% if object.physical_address %} {% if object.physical_address %}
<span>{{ object.physical_address|linebreaksbr }}</span> <span>{{ object.physical_address|linebreaksbr }}</span>
{% if config.MAPS_URL %} {% if config.MAPS_URL %}

View File

@ -0,0 +1,10 @@
{% load i18n %}
<div class="d-flex">
<button class="btn color-mode-toggle hide-theme-dark" title="{% trans "Enable dark mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="mdi mdi-lightbulb"></i>
</button>
<button class="btn color-mode-toggle hide-theme-light" title="{% trans "Enable light mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="mdi mdi-lightbulb-on"></i>
</button>
</div>

View File

@ -1,25 +1,37 @@
{% load helpers %} {% load helpers %}
{% load i18n %} {% load i18n %}
<div class="row mb-3"> <div class="row mb-3" id="results">
<div class="col-auto d-print-none"> <div class="col-auto d-print-none">
<div class="input-group input-group-flat me-2 quicksearch" hx-disinherit="hx-select hx-swap"> <div class="input-group input-group-flat me-2 quicksearch" hx-disinherit="hx-select hx-swap">
<input type="search" results="5" name="q" id="quicksearch" class="form-control px-2 py-1" placeholder="Quick search" <input type="search" results="5" name="q" id="quicksearch" class="form-control" placeholder="{% trans "Quick search" %}"
hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" /> hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search"/>
<span class="input-group-text py-1"> <span class="input-group-text py-1">
<a href="#" id="quicksearch_clear" class="invisible text-secondary"><i class="mdi mdi-close-circle"></i></a> <a href="#" id="quicksearch_clear" class="invisible text-secondary"><i class="mdi mdi-close-circle"></i></a>
</span> </span>
{% block extra_table_controls %}{% endblock %} {% block extra_table_controls %}{% endblock %}
</div> </div>
</div> </div>
<div class="col-auto d-print-none">
<div class="input-group">
<div class="input-group-text">
<i class="mdi mdi-filter" title="{% trans "Saved filter" %}"></i>
</div>
{{ filter_form.filter_id }}
</div>
</div>
<div class="col-auto ms-auto d-print-none"> <div class="col-auto ms-auto d-print-none">
{% if request.user.is_authenticated and table_modal %} {% if request.user.is_authenticated and table_modal %}
<div class="table-configure input-group"> <div class="table-configure input-group">
<button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}" data-bs-target="#{{ table_modal }}" <button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}"
class="btn"> data-bs-target="#{{ table_modal }}"
class="btn">
<i class="mdi mdi-cog"></i> {% trans "Configure Table" %} <i class="mdi mdi-cog"></i> {% trans "Configure Table" %}
</button> </button>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-06-12 05:01+0000\n" "POT-Creation-Date: 2024-06-19 05:02+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -58,7 +58,7 @@ msgstr ""
msgid "Allowed IPs" msgid "Allowed IPs"
msgstr "" msgstr ""
#: netbox/account/views.py:197 #: netbox/account/views.py:204
msgid "Your preferences have been updated." msgid "Your preferences have been updated."
msgstr "" msgstr ""
@ -158,7 +158,7 @@ msgstr ""
#: netbox/circuits/forms/filtersets.py:207 #: netbox/circuits/forms/filtersets.py:207
#: netbox/circuits/forms/model_forms.py:136 #: netbox/circuits/forms/model_forms.py:136
#: netbox/circuits/forms/model_forms.py:152 #: netbox/circuits/forms/model_forms.py:152
#: netbox/circuits/tables/circuits.py:105 netbox/dcim/forms/bulk_edit.py:167 #: netbox/circuits/tables/circuits.py:107 netbox/dcim/forms/bulk_edit.py:167
#: netbox/dcim/forms/bulk_edit.py:239 netbox/dcim/forms/bulk_edit.py:575 #: netbox/dcim/forms/bulk_edit.py:239 netbox/dcim/forms/bulk_edit.py:575
#: netbox/dcim/forms/bulk_edit.py:771 netbox/dcim/forms/bulk_import.py:130 #: netbox/dcim/forms/bulk_edit.py:771 netbox/dcim/forms/bulk_import.py:130
#: netbox/dcim/forms/bulk_import.py:184 netbox/dcim/forms/bulk_import.py:257 #: netbox/dcim/forms/bulk_import.py:184 netbox/dcim/forms/bulk_import.py:257
@ -308,7 +308,7 @@ msgstr ""
#: netbox/circuits/forms/filtersets.py:212 #: netbox/circuits/forms/filtersets.py:212
#: netbox/circuits/forms/model_forms.py:109 #: netbox/circuits/forms/model_forms.py:109
#: netbox/circuits/forms/model_forms.py:131 #: netbox/circuits/forms/model_forms.py:131
#: netbox/circuits/tables/circuits.py:96 netbox/dcim/forms/connections.py:71 #: netbox/circuits/tables/circuits.py:98 netbox/dcim/forms/connections.py:71
#: netbox/templates/circuits/circuit.html:15 #: netbox/templates/circuits/circuit.html:15
#: netbox/templates/circuits/circuittermination.html:19 #: netbox/templates/circuits/circuittermination.html:19
#: netbox/templates/dcim/inc/cable_termination.html:55 #: netbox/templates/dcim/inc/cable_termination.html:55
@ -325,7 +325,7 @@ msgstr ""
#: netbox/circuits/tables/providers.py:33 netbox/dcim/forms/bulk_edit.py:127 #: netbox/circuits/tables/providers.py:33 netbox/dcim/forms/bulk_edit.py:127
#: netbox/dcim/forms/filtersets.py:188 netbox/dcim/forms/model_forms.py:122 #: netbox/dcim/forms/filtersets.py:188 netbox/dcim/forms/model_forms.py:122
#: netbox/dcim/tables/sites.py:94 netbox/ipam/models/asns.py:126 #: netbox/dcim/tables/sites.py:94 netbox/ipam/models/asns.py:126
#: netbox/ipam/tables/asn.py:27 netbox/ipam/views.py:219 #: netbox/ipam/tables/asn.py:27 netbox/ipam/views.py:210
#: netbox/netbox/navigation/menu.py:159 netbox/netbox/navigation/menu.py:162 #: netbox/netbox/navigation/menu.py:159 netbox/netbox/navigation/menu.py:162
#: netbox/templates/circuits/provider.html:23 #: netbox/templates/circuits/provider.html:23
msgid "ASNs" msgid "ASNs"
@ -469,7 +469,7 @@ msgstr ""
#: netbox/circuits/forms/model_forms.py:45 #: netbox/circuits/forms/model_forms.py:45
#: netbox/circuits/forms/model_forms.py:59 #: netbox/circuits/forms/model_forms.py:59
#: netbox/circuits/forms/model_forms.py:91 #: netbox/circuits/forms/model_forms.py:91
#: netbox/circuits/tables/circuits.py:56 netbox/circuits/tables/circuits.py:100 #: netbox/circuits/tables/circuits.py:56 netbox/circuits/tables/circuits.py:102
#: netbox/circuits/tables/providers.py:72 #: netbox/circuits/tables/providers.py:72
#: netbox/circuits/tables/providers.py:103 #: netbox/circuits/tables/providers.py:103
#: netbox/templates/circuits/circuit.html:18 #: netbox/templates/circuits/circuit.html:18
@ -748,7 +748,7 @@ msgstr ""
#: netbox/circuits/forms/bulk_edit.py:191 #: netbox/circuits/forms/bulk_edit.py:191
#: netbox/circuits/forms/bulk_edit.py:215 #: netbox/circuits/forms/bulk_edit.py:215
#: netbox/circuits/forms/model_forms.py:153 #: netbox/circuits/forms/model_forms.py:153
#: netbox/circuits/tables/circuits.py:109 #: netbox/circuits/tables/circuits.py:111
#: netbox/templates/circuits/inc/circuit_termination_fields.html:62 #: netbox/templates/circuits/inc/circuit_termination_fields.html:62
#: netbox/templates/circuits/providernetwork.html:17 #: netbox/templates/circuits/providernetwork.html:17
msgid "Provider Network" msgid "Provider Network"
@ -895,7 +895,7 @@ msgstr ""
#: netbox/dcim/forms/filtersets.py:653 netbox/dcim/forms/filtersets.py:1010 #: netbox/dcim/forms/filtersets.py:653 netbox/dcim/forms/filtersets.py:1010
#: netbox/netbox/navigation/menu.py:44 netbox/netbox/navigation/menu.py:46 #: netbox/netbox/navigation/menu.py:44 netbox/netbox/navigation/menu.py:46
#: netbox/tenancy/forms/filtersets.py:42 netbox/tenancy/tables/columns.py:70 #: netbox/tenancy/forms/filtersets.py:42 netbox/tenancy/tables/columns.py:70
#: netbox/tenancy/tables/contacts.py:25 netbox/tenancy/views.py:19 #: netbox/tenancy/tables/contacts.py:25 netbox/tenancy/views.py:18
#: netbox/virtualization/forms/filtersets.py:37 #: netbox/virtualization/forms/filtersets.py:37
#: netbox/virtualization/forms/filtersets.py:48 #: netbox/virtualization/forms/filtersets.py:48
#: netbox/virtualization/forms/filtersets.py:106 #: netbox/virtualization/forms/filtersets.py:106
@ -1328,21 +1328,21 @@ msgstr ""
msgid "Circuit ID" msgid "Circuit ID"
msgstr "" msgstr ""
#: netbox/circuits/tables/circuits.py:66 #: netbox/circuits/tables/circuits.py:67
#: netbox/wireless/forms/model_forms.py:160 #: netbox/wireless/forms/model_forms.py:160
msgid "Side A" msgid "Side A"
msgstr "" msgstr ""
#: netbox/circuits/tables/circuits.py:70 #: netbox/circuits/tables/circuits.py:72
msgid "Side Z" msgid "Side Z"
msgstr "" msgstr ""
#: netbox/circuits/tables/circuits.py:73 #: netbox/circuits/tables/circuits.py:75
#: netbox/templates/circuits/circuit.html:55 #: netbox/templates/circuits/circuit.html:55
msgid "Commit Rate" msgid "Commit Rate"
msgstr "" msgstr ""
#: netbox/circuits/tables/circuits.py:76 netbox/circuits/tables/providers.py:48 #: netbox/circuits/tables/circuits.py:78 netbox/circuits/tables/providers.py:48
#: netbox/circuits/tables/providers.py:82 #: netbox/circuits/tables/providers.py:82
#: netbox/circuits/tables/providers.py:107 netbox/dcim/tables/devices.py:1001 #: netbox/circuits/tables/providers.py:107 netbox/dcim/tables/devices.py:1001
#: netbox/dcim/tables/devicetypes.py:92 netbox/dcim/tables/modules.py:29 #: netbox/dcim/tables/devicetypes.py:92 netbox/dcim/tables/modules.py:29
@ -2067,8 +2067,8 @@ msgstr ""
msgid "No workers found" msgid "No workers found"
msgstr "" msgstr ""
#: netbox/core/views.py:335 netbox/core/views.py:378 netbox/core/views.py:401 #: netbox/core/views.py:331 netbox/core/views.py:374 netbox/core/views.py:397
#: netbox/core/views.py:419 netbox/core/views.py:454 #: netbox/core/views.py:415 netbox/core/views.py:450
#, python-brace-format #, python-brace-format
msgid "Job {job_id} not found" msgid "Job {job_id} not found"
msgstr "" msgstr ""
@ -2946,7 +2946,7 @@ msgstr ""
#: netbox/dcim/forms/bulk_create.py:40 netbox/extras/forms/filtersets.py:410 #: netbox/dcim/forms/bulk_create.py:40 netbox/extras/forms/filtersets.py:410
#: netbox/extras/forms/model_forms.py:443 #: netbox/extras/forms/model_forms.py:443
#: netbox/extras/forms/model_forms.py:495 netbox/netbox/forms/base.py:84 #: netbox/extras/forms/model_forms.py:495 netbox/netbox/forms/base.py:84
#: netbox/netbox/forms/mixins.py:81 netbox/netbox/tables/columns.py:458 #: netbox/netbox/forms/mixins.py:81 netbox/netbox/tables/columns.py:461
#: netbox/templates/circuits/inc/circuit_termination.html:32 #: netbox/templates/circuits/inc/circuit_termination.html:32
#: netbox/templates/generic/bulk_edit.html:65 #: netbox/templates/generic/bulk_edit.html:65
#: netbox/templates/inc/panels/tags.html:5 #: netbox/templates/inc/panels/tags.html:5
@ -5974,7 +5974,7 @@ msgstr ""
#: netbox/netbox/navigation/menu.py:60 netbox/netbox/navigation/menu.py:62 #: netbox/netbox/navigation/menu.py:60 netbox/netbox/navigation/menu.py:62
#: netbox/virtualization/forms/model_forms.py:122 #: netbox/virtualization/forms/model_forms.py:122
#: netbox/virtualization/tables/clusters.py:83 #: netbox/virtualization/tables/clusters.py:83
#: netbox/virtualization/views.py:210 #: netbox/virtualization/views.py:202
msgid "Devices" msgid "Devices"
msgstr "" msgstr ""
@ -6054,8 +6054,8 @@ msgid "Power outlets"
msgstr "" msgstr ""
#: netbox/dcim/tables/devices.py:243 netbox/dcim/tables/devices.py:1046 #: netbox/dcim/tables/devices.py:243 netbox/dcim/tables/devices.py:1046
#: netbox/dcim/tables/devicetypes.py:125 netbox/dcim/views.py:1006 #: netbox/dcim/tables/devicetypes.py:125 netbox/dcim/views.py:985
#: netbox/dcim/views.py:1245 netbox/dcim/views.py:1931 #: netbox/dcim/views.py:1224 netbox/dcim/views.py:1900
#: netbox/netbox/navigation/menu.py:81 netbox/netbox/navigation/menu.py:237 #: netbox/netbox/navigation/menu.py:81 netbox/netbox/navigation/menu.py:237
#: netbox/templates/dcim/device/base.html:37 #: netbox/templates/dcim/device/base.html:37
#: netbox/templates/dcim/device_list.html:43 #: netbox/templates/dcim/device_list.html:43
@ -6067,7 +6067,7 @@ msgstr ""
#: netbox/templates/virtualization/virtualmachine/base.html:27 #: netbox/templates/virtualization/virtualmachine/base.html:27
#: netbox/templates/virtualization/virtualmachine_list.html:14 #: netbox/templates/virtualization/virtualmachine_list.html:14
#: netbox/virtualization/tables/virtualmachines.py:100 #: netbox/virtualization/tables/virtualmachines.py:100
#: netbox/virtualization/views.py:367 netbox/wireless/tables/wirelesslan.py:55 #: netbox/virtualization/views.py:359 netbox/wireless/tables/wirelesslan.py:55
msgid "Interfaces" msgid "Interfaces"
msgstr "" msgstr ""
@ -6093,8 +6093,8 @@ msgid "Module Bay"
msgstr "" msgstr ""
#: netbox/dcim/tables/devices.py:310 netbox/dcim/tables/devicetypes.py:48 #: netbox/dcim/tables/devices.py:310 netbox/dcim/tables/devicetypes.py:48
#: netbox/dcim/tables/devicetypes.py:140 netbox/dcim/views.py:1081 #: netbox/dcim/tables/devicetypes.py:140 netbox/dcim/views.py:1060
#: netbox/dcim/views.py:2024 netbox/netbox/navigation/menu.py:90 #: netbox/dcim/views.py:1993 netbox/netbox/navigation/menu.py:90
#: netbox/templates/dcim/device/base.html:52 #: netbox/templates/dcim/device/base.html:52
#: netbox/templates/dcim/device_list.html:71 #: netbox/templates/dcim/device_list.html:71
#: netbox/templates/dcim/devicetype/base.html:49 #: netbox/templates/dcim/devicetype/base.html:49
@ -6124,8 +6124,8 @@ msgid "Allocated draw (W)"
msgstr "" msgstr ""
#: netbox/dcim/tables/devices.py:546 netbox/ipam/forms/model_forms.py:747 #: netbox/dcim/tables/devices.py:546 netbox/ipam/forms/model_forms.py:747
#: netbox/ipam/tables/fhrp.py:28 netbox/ipam/views.py:602 #: netbox/ipam/tables/fhrp.py:28 netbox/ipam/views.py:589
#: netbox/ipam/views.py:701 netbox/netbox/navigation/menu.py:145 #: netbox/ipam/views.py:688 netbox/netbox/navigation/menu.py:145
#: netbox/netbox/navigation/menu.py:147 #: netbox/netbox/navigation/menu.py:147
#: netbox/templates/dcim/interface.html:339 #: netbox/templates/dcim/interface.html:339
#: netbox/templates/ipam/ipaddress_bulk_add.html:15 #: netbox/templates/ipam/ipaddress_bulk_add.html:15
@ -6218,8 +6218,8 @@ msgstr ""
msgid "Instances" msgid "Instances"
msgstr "" msgstr ""
#: netbox/dcim/tables/devicetypes.py:113 netbox/dcim/views.py:946 #: netbox/dcim/tables/devicetypes.py:113 netbox/dcim/views.py:925
#: netbox/dcim/views.py:1185 netbox/dcim/views.py:1871 #: netbox/dcim/views.py:1164 netbox/dcim/views.py:1840
#: netbox/netbox/navigation/menu.py:84 #: netbox/netbox/navigation/menu.py:84
#: netbox/templates/dcim/device/base.html:25 #: netbox/templates/dcim/device/base.html:25
#: netbox/templates/dcim/device_list.html:15 #: netbox/templates/dcim/device_list.html:15
@ -6229,8 +6229,8 @@ msgstr ""
msgid "Console Ports" msgid "Console Ports"
msgstr "" msgstr ""
#: netbox/dcim/tables/devicetypes.py:116 netbox/dcim/views.py:961 #: netbox/dcim/tables/devicetypes.py:116 netbox/dcim/views.py:940
#: netbox/dcim/views.py:1200 netbox/dcim/views.py:1886 #: netbox/dcim/views.py:1179 netbox/dcim/views.py:1855
#: netbox/netbox/navigation/menu.py:85 #: netbox/netbox/navigation/menu.py:85
#: netbox/templates/dcim/device/base.html:28 #: netbox/templates/dcim/device/base.html:28
#: netbox/templates/dcim/device_list.html:22 #: netbox/templates/dcim/device_list.html:22
@ -6240,8 +6240,8 @@ msgstr ""
msgid "Console Server Ports" msgid "Console Server Ports"
msgstr "" msgstr ""
#: netbox/dcim/tables/devicetypes.py:119 netbox/dcim/views.py:976 #: netbox/dcim/tables/devicetypes.py:119 netbox/dcim/views.py:955
#: netbox/dcim/views.py:1215 netbox/dcim/views.py:1901 #: netbox/dcim/views.py:1194 netbox/dcim/views.py:1870
#: netbox/netbox/navigation/menu.py:86 #: netbox/netbox/navigation/menu.py:86
#: netbox/templates/dcim/device/base.html:31 #: netbox/templates/dcim/device/base.html:31
#: netbox/templates/dcim/device_list.html:29 #: netbox/templates/dcim/device_list.html:29
@ -6251,8 +6251,8 @@ msgstr ""
msgid "Power Ports" msgid "Power Ports"
msgstr "" msgstr ""
#: netbox/dcim/tables/devicetypes.py:122 netbox/dcim/views.py:991 #: netbox/dcim/tables/devicetypes.py:122 netbox/dcim/views.py:970
#: netbox/dcim/views.py:1230 netbox/dcim/views.py:1916 #: netbox/dcim/views.py:1209 netbox/dcim/views.py:1885
#: netbox/netbox/navigation/menu.py:87 #: netbox/netbox/navigation/menu.py:87
#: netbox/templates/dcim/device/base.html:34 #: netbox/templates/dcim/device/base.html:34
#: netbox/templates/dcim/device_list.html:36 #: netbox/templates/dcim/device_list.html:36
@ -6262,8 +6262,8 @@ msgstr ""
msgid "Power Outlets" msgid "Power Outlets"
msgstr "" msgstr ""
#: netbox/dcim/tables/devicetypes.py:128 netbox/dcim/views.py:1021 #: netbox/dcim/tables/devicetypes.py:128 netbox/dcim/views.py:1000
#: netbox/dcim/views.py:1260 netbox/dcim/views.py:1952 #: netbox/dcim/views.py:1239 netbox/dcim/views.py:1921
#: netbox/netbox/navigation/menu.py:82 #: netbox/netbox/navigation/menu.py:82
#: netbox/templates/dcim/device/base.html:40 #: netbox/templates/dcim/device/base.html:40
#: netbox/templates/dcim/devicetype/base.html:37 #: netbox/templates/dcim/devicetype/base.html:37
@ -6272,8 +6272,8 @@ msgstr ""
msgid "Front Ports" msgid "Front Ports"
msgstr "" msgstr ""
#: netbox/dcim/tables/devicetypes.py:131 netbox/dcim/views.py:1036 #: netbox/dcim/tables/devicetypes.py:131 netbox/dcim/views.py:1015
#: netbox/dcim/views.py:1275 netbox/dcim/views.py:1967 #: netbox/dcim/views.py:1254 netbox/dcim/views.py:1936
#: netbox/netbox/navigation/menu.py:83 #: netbox/netbox/navigation/menu.py:83
#: netbox/templates/dcim/device/base.html:43 #: netbox/templates/dcim/device/base.html:43
#: netbox/templates/dcim/device_list.html:50 #: netbox/templates/dcim/device_list.html:50
@ -6283,16 +6283,16 @@ msgstr ""
msgid "Rear Ports" msgid "Rear Ports"
msgstr "" msgstr ""
#: netbox/dcim/tables/devicetypes.py:134 netbox/dcim/views.py:1066 #: netbox/dcim/tables/devicetypes.py:134 netbox/dcim/views.py:1045
#: netbox/dcim/views.py:2005 netbox/netbox/navigation/menu.py:89 #: netbox/dcim/views.py:1974 netbox/netbox/navigation/menu.py:89
#: netbox/templates/dcim/device/base.html:49 #: netbox/templates/dcim/device/base.html:49
#: netbox/templates/dcim/device_list.html:57 #: netbox/templates/dcim/device_list.html:57
#: netbox/templates/dcim/devicetype/base.html:46 #: netbox/templates/dcim/devicetype/base.html:46
msgid "Device Bays" msgid "Device Bays"
msgstr "" msgstr ""
#: netbox/dcim/tables/devicetypes.py:137 netbox/dcim/views.py:1051 #: netbox/dcim/tables/devicetypes.py:137 netbox/dcim/views.py:1030
#: netbox/dcim/views.py:1986 netbox/netbox/navigation/menu.py:88 #: netbox/dcim/views.py:1955 netbox/netbox/navigation/menu.py:88
#: netbox/templates/dcim/device/base.html:46 #: netbox/templates/dcim/device/base.html:46
#: netbox/templates/dcim/device_list.html:64 #: netbox/templates/dcim/device_list.html:64
#: netbox/templates/dcim/devicetype/base.html:43 #: netbox/templates/dcim/devicetype/base.html:43
@ -6350,38 +6350,38 @@ msgstr ""
msgid "Test case must set peer_termination_type" msgid "Test case must set peer_termination_type"
msgstr "" msgstr ""
#: netbox/dcim/views.py:137 #: netbox/dcim/views.py:139
#, python-brace-format #, python-brace-format
msgid "Disconnected {count} {type}" msgid "Disconnected {count} {type}"
msgstr "" msgstr ""
#: netbox/dcim/views.py:698 netbox/netbox/navigation/menu.py:28 #: netbox/dcim/views.py:684 netbox/netbox/navigation/menu.py:28
msgid "Reservations" msgid "Reservations"
msgstr "" msgstr ""
#: netbox/dcim/views.py:716 netbox/templates/dcim/location.html:90 #: netbox/dcim/views.py:702 netbox/templates/dcim/location.html:90
#: netbox/templates/dcim/site.html:140 #: netbox/templates/dcim/site.html:140
msgid "Non-Racked Devices" msgid "Non-Racked Devices"
msgstr "" msgstr ""
#: netbox/dcim/views.py:2037 netbox/extras/forms/model_forms.py:453 #: netbox/dcim/views.py:2006 netbox/extras/forms/model_forms.py:453
#: netbox/templates/extras/configcontext.html:10 #: netbox/templates/extras/configcontext.html:10
#: netbox/virtualization/forms/model_forms.py:225 #: netbox/virtualization/forms/model_forms.py:225
#: netbox/virtualization/views.py:407 #: netbox/virtualization/views.py:399
msgid "Config Context" msgid "Config Context"
msgstr "" msgstr ""
#: netbox/dcim/views.py:2047 netbox/virtualization/views.py:417 #: netbox/dcim/views.py:2016 netbox/virtualization/views.py:409
msgid "Render Config" msgid "Render Config"
msgstr "" msgstr ""
#: netbox/dcim/views.py:2097 netbox/extras/tables/tables.py:440 #: netbox/dcim/views.py:2066 netbox/extras/tables/tables.py:440
#: netbox/netbox/navigation/menu.py:234 netbox/netbox/navigation/menu.py:236 #: netbox/netbox/navigation/menu.py:234 netbox/netbox/navigation/menu.py:236
#: netbox/virtualization/views.py:185 #: netbox/virtualization/views.py:177
msgid "Virtual Machines" msgid "Virtual Machines"
msgstr "" msgstr ""
#: netbox/dcim/views.py:2989 netbox/ipam/tables/ip.py:233 #: netbox/dcim/views.py:2948 netbox/ipam/tables/ip.py:233
msgid "Children" msgid "Children"
msgstr "" msgstr ""
@ -7686,56 +7686,56 @@ msgstr ""
msgid "Date values must be in ISO 8601 format (YYYY-MM-DD)." msgid "Date values must be in ISO 8601 format (YYYY-MM-DD)."
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:667 #: netbox/extras/models/customfields.py:671
msgid "Date and time values must be in ISO 8601 format (YYYY-MM-DD HH:MM:SS)." msgid "Date and time values must be in ISO 8601 format (YYYY-MM-DD HH:MM:SS)."
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:674 #: netbox/extras/models/customfields.py:678
#, python-brace-format #, python-brace-format
msgid "Invalid choice ({value}) for choice set {choiceset}." msgid "Invalid choice ({value}) for choice set {choiceset}."
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:684 #: netbox/extras/models/customfields.py:688
#, python-brace-format #, python-brace-format
msgid "Invalid choice(s) ({value}) for choice set {choiceset}." msgid "Invalid choice(s) ({value}) for choice set {choiceset}."
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:693 #: netbox/extras/models/customfields.py:697
#, python-brace-format #, python-brace-format
msgid "Value must be an object ID, not {type}" msgid "Value must be an object ID, not {type}"
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:699 #: netbox/extras/models/customfields.py:703
#, python-brace-format #, python-brace-format
msgid "Value must be a list of object IDs, not {type}" msgid "Value must be a list of object IDs, not {type}"
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:703 #: netbox/extras/models/customfields.py:707
#, python-brace-format #, python-brace-format
msgid "Found invalid object ID: {id}" msgid "Found invalid object ID: {id}"
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:706 #: netbox/extras/models/customfields.py:710
msgid "Required field cannot be empty." msgid "Required field cannot be empty."
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:725 #: netbox/extras/models/customfields.py:729
msgid "Base set of predefined choices (optional)" msgid "Base set of predefined choices (optional)"
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:737 #: netbox/extras/models/customfields.py:741
msgid "Choices are automatically ordered alphabetically" msgid "Choices are automatically ordered alphabetically"
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:744 #: netbox/extras/models/customfields.py:748
msgid "custom field choice set" msgid "custom field choice set"
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:745 #: netbox/extras/models/customfields.py:749
msgid "custom field choice sets" msgid "custom field choice sets"
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:781 #: netbox/extras/models/customfields.py:785
msgid "Must define base or extra choices." msgid "Must define base or extra choices."
msgstr "" msgstr ""
@ -9415,7 +9415,7 @@ msgid "The primary function of this VLAN"
msgstr "" msgstr ""
#: netbox/ipam/models/vlans.py:215 netbox/ipam/tables/ip.py:175 #: netbox/ipam/models/vlans.py:215 netbox/ipam/tables/ip.py:175
#: netbox/ipam/tables/vlans.py:78 netbox/ipam/views.py:978 #: netbox/ipam/tables/vlans.py:78 netbox/ipam/views.py:961
#: netbox/netbox/navigation/menu.py:180 netbox/netbox/navigation/menu.py:182 #: netbox/netbox/navigation/menu.py:180 netbox/netbox/navigation/menu.py:182
msgid "VLANs" msgid "VLANs"
msgstr "" msgstr ""
@ -9487,7 +9487,7 @@ msgid "Added"
msgstr "" msgstr ""
#: netbox/ipam/tables/ip.py:127 netbox/ipam/tables/ip.py:165 #: netbox/ipam/tables/ip.py:127 netbox/ipam/tables/ip.py:165
#: netbox/ipam/tables/vlans.py:138 netbox/ipam/views.py:349 #: netbox/ipam/tables/vlans.py:138 netbox/ipam/views.py:342
#: netbox/netbox/navigation/menu.py:152 netbox/netbox/navigation/menu.py:154 #: netbox/netbox/navigation/menu.py:152 netbox/netbox/navigation/menu.py:154
#: netbox/templates/ipam/vlan.html:84 #: netbox/templates/ipam/vlan.html:84
msgid "Prefixes" msgid "Prefixes"
@ -9588,23 +9588,23 @@ msgid ""
"are allowed in DNS names" "are allowed in DNS names"
msgstr "" msgstr ""
#: netbox/ipam/views.py:541 #: netbox/ipam/views.py:528
msgid "Child Prefixes" msgid "Child Prefixes"
msgstr "" msgstr ""
#: netbox/ipam/views.py:576 #: netbox/ipam/views.py:563
msgid "Child Ranges" msgid "Child Ranges"
msgstr "" msgstr ""
#: netbox/ipam/views.py:902 #: netbox/ipam/views.py:889
msgid "Related IPs" msgid "Related IPs"
msgstr "" msgstr ""
#: netbox/ipam/views.py:1133 #: netbox/ipam/views.py:1116
msgid "Device Interfaces" msgid "Device Interfaces"
msgstr "" msgstr ""
#: netbox/ipam/views.py:1150 #: netbox/ipam/views.py:1133
msgid "VM Interfaces" msgid "VM Interfaces"
msgstr "" msgstr ""
@ -10159,7 +10159,7 @@ msgstr ""
#: netbox/templates/virtualization/virtualmachine/base.html:32 #: netbox/templates/virtualization/virtualmachine/base.html:32
#: netbox/templates/virtualization/virtualmachine_list.html:21 #: netbox/templates/virtualization/virtualmachine_list.html:21
#: netbox/virtualization/tables/virtualmachines.py:103 #: netbox/virtualization/tables/virtualmachines.py:103
#: netbox/virtualization/views.py:388 #: netbox/virtualization/views.py:380
msgid "Virtual Disks" msgid "Virtual Disks"
msgstr "" msgstr ""
@ -10498,15 +10498,15 @@ msgstr ""
msgid "Chinese" msgid "Chinese"
msgstr "" msgstr ""
#: netbox/netbox/tables/columns.py:185 #: netbox/netbox/tables/columns.py:188
msgid "Toggle all" msgid "Toggle all"
msgstr "" msgstr ""
#: netbox/netbox/tables/columns.py:287 #: netbox/netbox/tables/columns.py:290
msgid "Toggle Dropdown" msgid "Toggle Dropdown"
msgstr "" msgstr ""
#: netbox/netbox/tables/columns.py:552 netbox/templates/core/job.html:35 #: netbox/netbox/tables/columns.py:555 netbox/templates/core/job.html:35
msgid "Error" msgid "Error"
msgstr "" msgstr ""
@ -10790,36 +10790,28 @@ msgstr ""
msgid "NetBox Logo" msgid "NetBox Logo"
msgstr "" msgstr ""
#: netbox/templates/base/layout.html:56 #: netbox/templates/base/layout.html:139
msgid "Enable dark mode"
msgstr ""
#: netbox/templates/base/layout.html:59
msgid "Enable light mode"
msgstr ""
#: netbox/templates/base/layout.html:145
msgid "Docs" msgid "Docs"
msgstr "" msgstr ""
#: netbox/templates/base/layout.html:151 #: netbox/templates/base/layout.html:145
#: netbox/templates/rest_framework/api.html:10 #: netbox/templates/rest_framework/api.html:10
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: netbox/templates/base/layout.html:157 #: netbox/templates/base/layout.html:151
msgid "REST API documentation" msgid "REST API documentation"
msgstr "" msgstr ""
#: netbox/templates/base/layout.html:164 #: netbox/templates/base/layout.html:158
msgid "GraphQL API" msgid "GraphQL API"
msgstr "" msgstr ""
#: netbox/templates/base/layout.html:171 #: netbox/templates/base/layout.html:165
msgid "Source Code" msgid "Source Code"
msgstr "" msgstr ""
#: netbox/templates/base/layout.html:177 #: netbox/templates/base/layout.html:171
msgid "Community" msgid "Community"
msgstr "" msgstr ""
@ -11112,8 +11104,8 @@ msgstr ""
#: netbox/templates/core/rq_worker_list.html:45 #: netbox/templates/core/rq_worker_list.html:45
#: netbox/templates/extras/script_result.html:49 #: netbox/templates/extras/script_result.html:49
#: netbox/templates/extras/script_result.html:51 #: netbox/templates/extras/script_result.html:51
#: netbox/templates/inc/table_controls_htmx.html:18 #: netbox/templates/inc/table_controls_htmx.html:28
#: netbox/templates/inc/table_controls_htmx.html:20 #: netbox/templates/inc/table_controls_htmx.html:31
msgid "Configure Table" msgid "Configure Table"
msgstr "" msgstr ""
@ -12689,6 +12681,14 @@ msgstr ""
msgid "Reset" msgid "Reset"
msgstr "" msgstr ""
#: netbox/templates/inc/light_toggle.html:4
msgid "Enable dark mode"
msgstr ""
#: netbox/templates/inc/light_toggle.html:7
msgid "Enable light mode"
msgstr ""
#: netbox/templates/inc/missing_prerequisites.html:8 #: netbox/templates/inc/missing_prerequisites.html:8
#, python-format #, python-format
msgid "" msgid ""
@ -12729,6 +12729,14 @@ msgstr ""
msgid "Data is out of sync with upstream file" msgid "Data is out of sync with upstream file"
msgstr "" msgstr ""
#: netbox/templates/inc/table_controls_htmx.html:7
msgid "Quick search"
msgstr ""
#: netbox/templates/inc/table_controls_htmx.html:19
msgid "Saved filter"
msgstr ""
#: netbox/templates/inc/user_menu.html:23 #: netbox/templates/inc/user_menu.html:23
msgid "Django Admin" msgid "Django Admin"
msgstr "" msgstr ""
@ -14072,17 +14080,17 @@ msgstr ""
msgid "{value} is not a valid regular expression." msgid "{value} is not a valid regular expression."
msgstr "" msgstr ""
#: netbox/utilities/views.py:40 #: netbox/utilities/views.py:44
#, python-brace-format #, python-brace-format
msgid "{self.__class__.__name__} must implement get_required_permission()" msgid "{self.__class__.__name__} must implement get_required_permission()"
msgstr "" msgstr ""
#: netbox/utilities/views.py:76 #: netbox/utilities/views.py:80
#, python-brace-format #, python-brace-format
msgid "{class_name} must implement get_required_permission()" msgid "{class_name} must implement get_required_permission()"
msgstr "" msgstr ""
#: netbox/utilities/views.py:100 #: netbox/utilities/views.py:104
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"{class_name} has no queryset defined. ObjectPermissionRequiredMixin may only " "{class_name} has no queryset defined. ObjectPermissionRequiredMixin may only "

View File

@ -29,7 +29,7 @@ def linkify_phone(value):
""" """
if value is None: if value is None:
return None return None
return f"tel:{value}" return f"tel:{value.replace(' ', '')}"
def register_table_column(column, name, *tables): def register_table_column(column, name, *tables):

View File

@ -11,7 +11,7 @@
{% elif customfield.type == 'date' and value %} {% elif customfield.type == 'date' and value %}
{{ value|isodate }} {{ value|isodate }}
{% elif customfield.type == 'datetime' and value %} {% elif customfield.type == 'datetime' and value %}
{{ value|isodate }} {{ value|isodatetime }} {{ value|isodatetime }}
{% elif customfield.type == 'url' and value %} {% elif customfield.type == 'url' and value %}
<a href="{{ value }}">{{ value|truncatechars:70 }}</a> <a href="{{ value }}">{{ value|truncatechars:70 }}</a>
{% elif customfield.type == 'json' and value %} {% elif customfield.type == 'json' and value %}

View File

@ -1,4 +1,5 @@
from django import template from django import template
from django.utils.safestring import mark_safe
from extras.choices import CustomFieldTypeChoices from extras.choices import CustomFieldTypeChoices
from utilities.querydict import dict_to_querydict from utilities.querydict import dict_to_querydict
@ -124,5 +125,5 @@ def formaction(context):
if HTMX navigation is enabled (per the user's preferences). if HTMX navigation is enabled (per the user's preferences).
""" """
if context.get('htmx_navigation', False): if context.get('htmx_navigation', False):
return 'hx-push-url="true" hx-post' return mark_safe('hx-push-url="true" hx-post')
return 'formaction' return 'formaction'

View File

@ -281,6 +281,10 @@ def applied_filters(context, model, form, query_params):
if filter_name not in querydict: if filter_name not in querydict:
continue continue
# Skip saved filters, as they're displayed alongside the quick search widget
if filter_name == 'filter_id':
continue
bound_field = form.fields[filter_name].get_bound_field(form, filter_name) bound_field = form.fields[filter_name].get_bound_field(form, filter_name)
querydict.pop(filter_name) querydict.pop(filter_name)
display_value = ', '.join([str(v) for v in get_selected_values(form, filter_name)]) display_value = ', '.join([str(v) for v in get_selected_values(form, filter_name)])

View File

@ -178,8 +178,8 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
required=False, required=False,
selector=True, selector=True,
query_params={ query_params={
'site_id': '$site', 'site_id': ['$site', 'null']
} },
) )
device = DynamicModelChoiceField( device = DynamicModelChoiceField(
label=_('Device'), label=_('Device'),

View File

@ -180,7 +180,7 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
}) })
# Validate site for cluster & device # Validate site for cluster & device
if self.cluster and self.site and self.cluster.site != self.site: if self.cluster and self.cluster.site is not None and self.cluster.site != self.site:
raise ValidationError({ raise ValidationError({
'cluster': _( 'cluster': _(
'The selected cluster ({cluster}) is not assigned to this site ({site}).' 'The selected cluster ({cluster}) is not assigned to this site ({site}).'

View File

@ -63,6 +63,9 @@ class VirtualMachineTestCase(TestCase):
# VM with site only should pass # VM with site only should pass
VirtualMachine(name='vm1', site=sites[0]).full_clean() VirtualMachine(name='vm1', site=sites[0]).full_clean()
# VM with site, cluster non-site should pass
VirtualMachine(name='vm1', site=sites[0], cluster=clusters[2]).full_clean()
# VM with non-site cluster only should pass # VM with non-site cluster only should pass
VirtualMachine(name='vm1', cluster=clusters[2]).full_clean() VirtualMachine(name='vm1', cluster=clusters[2]).full_clean()