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
.coverage
.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.
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

View File

@ -2,6 +2,26 @@
## 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)

View File

@ -104,10 +104,16 @@ class LoginView(View):
# Ensure the user has a UserConfig defined. (This should normally be handled by
# create_userconfig() on user creation.)
if not hasattr(request.user, 'config'):
config = get_config()
UserConfig(user=request.user, data=config.DEFAULT_USER_PREFERENCES).save()
request.user.config = get_config()
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:
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")
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.delete_cookie('session_key')
response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
return response

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ from dcim.models import *
from extras.models import CustomField
from tenancy.models import Tenant
from utilities.data import drange
from virtualization.models import Cluster, ClusterType
class LocationTestCase(TestCase):
@ -533,6 +534,36 @@ class DeviceTestCase(TestCase):
device2.full_clean()
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):

View File

@ -660,6 +660,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Validate date & time
elif self.type == CustomFieldTypeChoices.TYPE_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:
datetime.fromisoformat(value)
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 { initRackElevation } from './racks';
import { initHtmx } from './htmx';
import { initSavedFilterSelect } from './forms/savedFiltersSelect';
function initDocument(): void {
for (const init of [
@ -31,6 +32,7 @@ function initDocument(): void {
initDashboard,
initRackElevation,
initHtmx,
initSavedFilterSelect,
]) {
init();
}

View File

@ -7,6 +7,7 @@
// Overrides of external libraries
@import 'overrides/bootstrap';
@import 'overrides/tabler';
@import 'overrides/tomselect';
// Transitional styling to ease migration of templates from NetBox v3.x
@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) #}
<div class="navbar-nav flex-row d-lg-none">
{% include 'inc/light_toggle.html' %}
{% include 'inc/user_menu.html' %}
</div>
@ -52,14 +53,7 @@ Blocks:
<div class="navbar-nav flex-row align-items-center order-md-last">
{# Dark/light mode toggle #}
<div class="d-none d-md-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>
{% include 'inc/light_toggle.html' %}
{# User menu #}
{% include 'inc/user_menu.html' %}

View File

@ -28,7 +28,7 @@
</tr>
<tr>
<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 %}
{{ 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" %}">

View File

@ -73,7 +73,7 @@
</tr>
<tr>
<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 %}
<span>{{ object.physical_address|linebreaksbr }}</span>
{% 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 i18n %}
<div class="row mb-3">
<div class="row mb-3" id="results">
<div class="col-auto d-print-none">
<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"
hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, 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"/>
<span class="input-group-text py-1">
<a href="#" id="quicksearch_clear" class="invisible text-secondary"><i class="mdi mdi-close-circle"></i></a>
</span>
{% block extra_table_controls %}{% endblock %}
{% block extra_table_controls %}{% endblock %}
</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">
{% if request.user.is_authenticated and table_modal %}
<div class="table-configure input-group">
<button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}" data-bs-target="#{{ table_modal }}"
class="btn">
<button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}"
data-bs-target="#{{ table_modal }}"
class="btn">
<i class="mdi mdi-cog"></i> {% trans "Configure Table" %}
</button>
</div>
{% endif %}
</div>
</div>

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -58,7 +58,7 @@ msgstr ""
msgid "Allowed IPs"
msgstr ""
#: netbox/account/views.py:197
#: netbox/account/views.py:204
msgid "Your preferences have been updated."
msgstr ""
@ -158,7 +158,7 @@ msgstr ""
#: netbox/circuits/forms/filtersets.py:207
#: netbox/circuits/forms/model_forms.py:136
#: 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:771 netbox/dcim/forms/bulk_import.py:130
#: 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/model_forms.py:109
#: 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/circuittermination.html:19
#: 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/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/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/templates/circuits/provider.html:23
msgid "ASNs"
@ -469,7 +469,7 @@ msgstr ""
#: netbox/circuits/forms/model_forms.py:45
#: netbox/circuits/forms/model_forms.py:59
#: 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:103
#: netbox/templates/circuits/circuit.html:18
@ -748,7 +748,7 @@ msgstr ""
#: netbox/circuits/forms/bulk_edit.py:191
#: netbox/circuits/forms/bulk_edit.py:215
#: 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/providernetwork.html:17
msgid "Provider Network"
@ -895,7 +895,7 @@ msgstr ""
#: 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/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:48
#: netbox/virtualization/forms/filtersets.py:106
@ -1328,21 +1328,21 @@ msgstr ""
msgid "Circuit ID"
msgstr ""
#: netbox/circuits/tables/circuits.py:66
#: netbox/circuits/tables/circuits.py:67
#: netbox/wireless/forms/model_forms.py:160
msgid "Side A"
msgstr ""
#: netbox/circuits/tables/circuits.py:70
#: netbox/circuits/tables/circuits.py:72
msgid "Side Z"
msgstr ""
#: netbox/circuits/tables/circuits.py:73
#: netbox/circuits/tables/circuits.py:75
#: netbox/templates/circuits/circuit.html:55
msgid "Commit Rate"
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:107 netbox/dcim/tables/devices.py:1001
#: netbox/dcim/tables/devicetypes.py:92 netbox/dcim/tables/modules.py:29
@ -2067,8 +2067,8 @@ msgstr ""
msgid "No workers found"
msgstr ""
#: netbox/core/views.py:335 netbox/core/views.py:378 netbox/core/views.py:401
#: netbox/core/views.py:419 netbox/core/views.py:454
#: netbox/core/views.py:331 netbox/core/views.py:374 netbox/core/views.py:397
#: netbox/core/views.py:415 netbox/core/views.py:450
#, python-brace-format
msgid "Job {job_id} not found"
msgstr ""
@ -2946,7 +2946,7 @@ msgstr ""
#: 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: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/generic/bulk_edit.html:65
#: 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/virtualization/forms/model_forms.py:122
#: netbox/virtualization/tables/clusters.py:83
#: netbox/virtualization/views.py:210
#: netbox/virtualization/views.py:202
msgid "Devices"
msgstr ""
@ -6054,8 +6054,8 @@ msgid "Power outlets"
msgstr ""
#: 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/views.py:1245 netbox/dcim/views.py:1931
#: netbox/dcim/tables/devicetypes.py:125 netbox/dcim/views.py:985
#: netbox/dcim/views.py:1224 netbox/dcim/views.py:1900
#: netbox/netbox/navigation/menu.py:81 netbox/netbox/navigation/menu.py:237
#: netbox/templates/dcim/device/base.html:37
#: netbox/templates/dcim/device_list.html:43
@ -6067,7 +6067,7 @@ msgstr ""
#: netbox/templates/virtualization/virtualmachine/base.html:27
#: netbox/templates/virtualization/virtualmachine_list.html:14
#: 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"
msgstr ""
@ -6093,8 +6093,8 @@ msgid "Module Bay"
msgstr ""
#: 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/views.py:2024 netbox/netbox/navigation/menu.py:90
#: netbox/dcim/tables/devicetypes.py:140 netbox/dcim/views.py:1060
#: netbox/dcim/views.py:1993 netbox/netbox/navigation/menu.py:90
#: netbox/templates/dcim/device/base.html:52
#: netbox/templates/dcim/device_list.html:71
#: netbox/templates/dcim/devicetype/base.html:49
@ -6124,8 +6124,8 @@ msgid "Allocated draw (W)"
msgstr ""
#: 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/views.py:701 netbox/netbox/navigation/menu.py:145
#: netbox/ipam/tables/fhrp.py:28 netbox/ipam/views.py:589
#: netbox/ipam/views.py:688 netbox/netbox/navigation/menu.py:145
#: netbox/netbox/navigation/menu.py:147
#: netbox/templates/dcim/interface.html:339
#: netbox/templates/ipam/ipaddress_bulk_add.html:15
@ -6218,8 +6218,8 @@ msgstr ""
msgid "Instances"
msgstr ""
#: netbox/dcim/tables/devicetypes.py:113 netbox/dcim/views.py:946
#: netbox/dcim/views.py:1185 netbox/dcim/views.py:1871
#: netbox/dcim/tables/devicetypes.py:113 netbox/dcim/views.py:925
#: netbox/dcim/views.py:1164 netbox/dcim/views.py:1840
#: netbox/netbox/navigation/menu.py:84
#: netbox/templates/dcim/device/base.html:25
#: netbox/templates/dcim/device_list.html:15
@ -6229,8 +6229,8 @@ msgstr ""
msgid "Console Ports"
msgstr ""
#: netbox/dcim/tables/devicetypes.py:116 netbox/dcim/views.py:961
#: netbox/dcim/views.py:1200 netbox/dcim/views.py:1886
#: netbox/dcim/tables/devicetypes.py:116 netbox/dcim/views.py:940
#: netbox/dcim/views.py:1179 netbox/dcim/views.py:1855
#: netbox/netbox/navigation/menu.py:85
#: netbox/templates/dcim/device/base.html:28
#: netbox/templates/dcim/device_list.html:22
@ -6240,8 +6240,8 @@ msgstr ""
msgid "Console Server Ports"
msgstr ""
#: netbox/dcim/tables/devicetypes.py:119 netbox/dcim/views.py:976
#: netbox/dcim/views.py:1215 netbox/dcim/views.py:1901
#: netbox/dcim/tables/devicetypes.py:119 netbox/dcim/views.py:955
#: netbox/dcim/views.py:1194 netbox/dcim/views.py:1870
#: netbox/netbox/navigation/menu.py:86
#: netbox/templates/dcim/device/base.html:31
#: netbox/templates/dcim/device_list.html:29
@ -6251,8 +6251,8 @@ msgstr ""
msgid "Power Ports"
msgstr ""
#: netbox/dcim/tables/devicetypes.py:122 netbox/dcim/views.py:991
#: netbox/dcim/views.py:1230 netbox/dcim/views.py:1916
#: netbox/dcim/tables/devicetypes.py:122 netbox/dcim/views.py:970
#: netbox/dcim/views.py:1209 netbox/dcim/views.py:1885
#: netbox/netbox/navigation/menu.py:87
#: netbox/templates/dcim/device/base.html:34
#: netbox/templates/dcim/device_list.html:36
@ -6262,8 +6262,8 @@ msgstr ""
msgid "Power Outlets"
msgstr ""
#: netbox/dcim/tables/devicetypes.py:128 netbox/dcim/views.py:1021
#: netbox/dcim/views.py:1260 netbox/dcim/views.py:1952
#: netbox/dcim/tables/devicetypes.py:128 netbox/dcim/views.py:1000
#: netbox/dcim/views.py:1239 netbox/dcim/views.py:1921
#: netbox/netbox/navigation/menu.py:82
#: netbox/templates/dcim/device/base.html:40
#: netbox/templates/dcim/devicetype/base.html:37
@ -6272,8 +6272,8 @@ msgstr ""
msgid "Front Ports"
msgstr ""
#: netbox/dcim/tables/devicetypes.py:131 netbox/dcim/views.py:1036
#: netbox/dcim/views.py:1275 netbox/dcim/views.py:1967
#: netbox/dcim/tables/devicetypes.py:131 netbox/dcim/views.py:1015
#: netbox/dcim/views.py:1254 netbox/dcim/views.py:1936
#: netbox/netbox/navigation/menu.py:83
#: netbox/templates/dcim/device/base.html:43
#: netbox/templates/dcim/device_list.html:50
@ -6283,16 +6283,16 @@ msgstr ""
msgid "Rear Ports"
msgstr ""
#: netbox/dcim/tables/devicetypes.py:134 netbox/dcim/views.py:1066
#: netbox/dcim/views.py:2005 netbox/netbox/navigation/menu.py:89
#: netbox/dcim/tables/devicetypes.py:134 netbox/dcim/views.py:1045
#: netbox/dcim/views.py:1974 netbox/netbox/navigation/menu.py:89
#: netbox/templates/dcim/device/base.html:49
#: netbox/templates/dcim/device_list.html:57
#: netbox/templates/dcim/devicetype/base.html:46
msgid "Device Bays"
msgstr ""
#: netbox/dcim/tables/devicetypes.py:137 netbox/dcim/views.py:1051
#: netbox/dcim/views.py:1986 netbox/netbox/navigation/menu.py:88
#: netbox/dcim/tables/devicetypes.py:137 netbox/dcim/views.py:1030
#: netbox/dcim/views.py:1955 netbox/netbox/navigation/menu.py:88
#: netbox/templates/dcim/device/base.html:46
#: netbox/templates/dcim/device_list.html:64
#: netbox/templates/dcim/devicetype/base.html:43
@ -6350,38 +6350,38 @@ msgstr ""
msgid "Test case must set peer_termination_type"
msgstr ""
#: netbox/dcim/views.py:137
#: netbox/dcim/views.py:139
#, python-brace-format
msgid "Disconnected {count} {type}"
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"
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
msgid "Non-Racked Devices"
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/virtualization/forms/model_forms.py:225
#: netbox/virtualization/views.py:407
#: netbox/virtualization/views.py:399
msgid "Config Context"
msgstr ""
#: netbox/dcim/views.py:2047 netbox/virtualization/views.py:417
#: netbox/dcim/views.py:2016 netbox/virtualization/views.py:409
msgid "Render Config"
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/virtualization/views.py:185
#: netbox/virtualization/views.py:177
msgid "Virtual Machines"
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"
msgstr ""
@ -7686,56 +7686,56 @@ msgstr ""
msgid "Date values must be in ISO 8601 format (YYYY-MM-DD)."
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)."
msgstr ""
#: netbox/extras/models/customfields.py:674
#: netbox/extras/models/customfields.py:678
#, python-brace-format
msgid "Invalid choice ({value}) for choice set {choiceset}."
msgstr ""
#: netbox/extras/models/customfields.py:684
#: netbox/extras/models/customfields.py:688
#, python-brace-format
msgid "Invalid choice(s) ({value}) for choice set {choiceset}."
msgstr ""
#: netbox/extras/models/customfields.py:693
#: netbox/extras/models/customfields.py:697
#, python-brace-format
msgid "Value must be an object ID, not {type}"
msgstr ""
#: netbox/extras/models/customfields.py:699
#: netbox/extras/models/customfields.py:703
#, python-brace-format
msgid "Value must be a list of object IDs, not {type}"
msgstr ""
#: netbox/extras/models/customfields.py:703
#: netbox/extras/models/customfields.py:707
#, python-brace-format
msgid "Found invalid object ID: {id}"
msgstr ""
#: netbox/extras/models/customfields.py:706
#: netbox/extras/models/customfields.py:710
msgid "Required field cannot be empty."
msgstr ""
#: netbox/extras/models/customfields.py:725
#: netbox/extras/models/customfields.py:729
msgid "Base set of predefined choices (optional)"
msgstr ""
#: netbox/extras/models/customfields.py:737
#: netbox/extras/models/customfields.py:741
msgid "Choices are automatically ordered alphabetically"
msgstr ""
#: netbox/extras/models/customfields.py:744
#: netbox/extras/models/customfields.py:748
msgid "custom field choice set"
msgstr ""
#: netbox/extras/models/customfields.py:745
#: netbox/extras/models/customfields.py:749
msgid "custom field choice sets"
msgstr ""
#: netbox/extras/models/customfields.py:781
#: netbox/extras/models/customfields.py:785
msgid "Must define base or extra choices."
msgstr ""
@ -9415,7 +9415,7 @@ msgid "The primary function of this VLAN"
msgstr ""
#: 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
msgid "VLANs"
msgstr ""
@ -9487,7 +9487,7 @@ msgid "Added"
msgstr ""
#: 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/templates/ipam/vlan.html:84
msgid "Prefixes"
@ -9588,23 +9588,23 @@ msgid ""
"are allowed in DNS names"
msgstr ""
#: netbox/ipam/views.py:541
#: netbox/ipam/views.py:528
msgid "Child Prefixes"
msgstr ""
#: netbox/ipam/views.py:576
#: netbox/ipam/views.py:563
msgid "Child Ranges"
msgstr ""
#: netbox/ipam/views.py:902
#: netbox/ipam/views.py:889
msgid "Related IPs"
msgstr ""
#: netbox/ipam/views.py:1133
#: netbox/ipam/views.py:1116
msgid "Device Interfaces"
msgstr ""
#: netbox/ipam/views.py:1150
#: netbox/ipam/views.py:1133
msgid "VM Interfaces"
msgstr ""
@ -10159,7 +10159,7 @@ msgstr ""
#: netbox/templates/virtualization/virtualmachine/base.html:32
#: netbox/templates/virtualization/virtualmachine_list.html:21
#: netbox/virtualization/tables/virtualmachines.py:103
#: netbox/virtualization/views.py:388
#: netbox/virtualization/views.py:380
msgid "Virtual Disks"
msgstr ""
@ -10498,15 +10498,15 @@ msgstr ""
msgid "Chinese"
msgstr ""
#: netbox/netbox/tables/columns.py:185
#: netbox/netbox/tables/columns.py:188
msgid "Toggle all"
msgstr ""
#: netbox/netbox/tables/columns.py:287
#: netbox/netbox/tables/columns.py:290
msgid "Toggle Dropdown"
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"
msgstr ""
@ -10790,36 +10790,28 @@ msgstr ""
msgid "NetBox Logo"
msgstr ""
#: netbox/templates/base/layout.html:56
msgid "Enable dark mode"
msgstr ""
#: netbox/templates/base/layout.html:59
msgid "Enable light mode"
msgstr ""
#: netbox/templates/base/layout.html:145
#: netbox/templates/base/layout.html:139
msgid "Docs"
msgstr ""
#: netbox/templates/base/layout.html:151
#: netbox/templates/base/layout.html:145
#: netbox/templates/rest_framework/api.html:10
msgid "REST API"
msgstr ""
#: netbox/templates/base/layout.html:157
#: netbox/templates/base/layout.html:151
msgid "REST API documentation"
msgstr ""
#: netbox/templates/base/layout.html:164
#: netbox/templates/base/layout.html:158
msgid "GraphQL API"
msgstr ""
#: netbox/templates/base/layout.html:171
#: netbox/templates/base/layout.html:165
msgid "Source Code"
msgstr ""
#: netbox/templates/base/layout.html:177
#: netbox/templates/base/layout.html:171
msgid "Community"
msgstr ""
@ -11112,8 +11104,8 @@ msgstr ""
#: netbox/templates/core/rq_worker_list.html:45
#: netbox/templates/extras/script_result.html:49
#: netbox/templates/extras/script_result.html:51
#: netbox/templates/inc/table_controls_htmx.html:18
#: netbox/templates/inc/table_controls_htmx.html:20
#: netbox/templates/inc/table_controls_htmx.html:28
#: netbox/templates/inc/table_controls_htmx.html:31
msgid "Configure Table"
msgstr ""
@ -12689,6 +12681,14 @@ msgstr ""
msgid "Reset"
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
#, python-format
msgid ""
@ -12729,6 +12729,14 @@ msgstr ""
msgid "Data is out of sync with upstream file"
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
msgid "Django Admin"
msgstr ""
@ -14072,17 +14080,17 @@ msgstr ""
msgid "{value} is not a valid regular expression."
msgstr ""
#: netbox/utilities/views.py:40
#: netbox/utilities/views.py:44
#, python-brace-format
msgid "{self.__class__.__name__} must implement get_required_permission()"
msgstr ""
#: netbox/utilities/views.py:76
#: netbox/utilities/views.py:80
#, python-brace-format
msgid "{class_name} must implement get_required_permission()"
msgstr ""
#: netbox/utilities/views.py:100
#: netbox/utilities/views.py:104
#, python-brace-format
msgid ""
"{class_name} has no queryset defined. ObjectPermissionRequiredMixin may only "

View File

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

View File

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

View File

@ -1,4 +1,5 @@
from django import template
from django.utils.safestring import mark_safe
from extras.choices import CustomFieldTypeChoices
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 context.get('htmx_navigation', False):
return 'hx-push-url="true" hx-post'
return mark_safe('hx-push-url="true" hx-post')
return 'formaction'

View File

@ -281,6 +281,10 @@ def applied_filters(context, model, form, query_params):
if filter_name not in querydict:
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)
querydict.pop(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,
selector=True,
query_params={
'site_id': '$site',
}
'site_id': ['$site', 'null']
},
)
device = DynamicModelChoiceField(
label=_('Device'),

View File

@ -180,7 +180,7 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
})
# 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({
'cluster': _(
'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
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
VirtualMachine(name='vm1', cluster=clusters[2]).full_clean()