Compare commits

...

12 Commits

Author SHA1 Message Date
Daniel Sheppard
ef7880a013 Add test
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-08-05 08:55:49 -05:00
Daniel Sheppard
8fd8493d11 Fix image path construction
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-07-30 23:14:45 -05:00
Daniel Sheppard
db805053d9 Additional Unblackening
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-07-28 10:00:43 -05:00
Daniel Sheppard
cf4db67e0b Unblacken code 2025-07-28 09:55:15 -05:00
Daniel Sheppard
f48e1cb534 Fixes: #19669 - Add an API endpoint to download image attachments 2025-07-24 14:16:02 -05:00
Jonathan Ramstedt
ffa9a52667 Closes #18936: add color name support for cable bulk import (#19949)
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-07-24 09:54:49 -07:00
bctiemann
47320f9958 Merge pull request #19912 from miaow2/19903-regexp
Closes #19903: Add `regex` and `iregex` filter lookup expressions and corresponding tests
2025-07-24 12:32:19 -04:00
github-actions
d08a1bd07d Update source translation strings 2025-07-24 05:05:44 +00:00
Martin Hauser
14c4aeca54 Closes #19840 - Enable Site Filtering for Devices in Cable Bulk Import (#19923)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
* feat(dcim): Add site fields to Cable bulk import form

Introduces `side_a_site` and `side_b_site` fields for the Cable bulk
import form. Limits device choices on both sides to the selected site
for improved input validation and consistency.

* feat(dcim): Enhance test data setup with multiple sites

Refactors tests to create multiple sites and assign devices accordingly.
Updates CSV data to include `side_a_site` and `side_b_site` fields for
scenarios involving multiple sites. This improves test coverage and
alignment with real-world use cases.

* docs(dcim): Update comments explaining indent for CSV import

Improved the inline comments to clarify the rationale behind allowing
devices with duplicate names on different sites during CSV bulk import.
2025-07-23 15:50:05 -05:00
Jason Novinger
26bec1275f Fixes #19934: add description field to Tenant bulk edit form (#19937) 2025-07-23 13:41:00 -07:00
Jason Novinger
fa2d7f6516 Fixes #19916: restore Rack device representation behavior
The select list of 'Images and Label', 'Images Only', and 'Label Only'
was broken during recent work while implementing #19823.

This fixes the issue by placing the `rack_elevation` class attribute on
the <div> element that contains the SVG after being loaded by HTMX. In
addition, we needed to slightly modify the selectors in the frontend
code that looked for the elements within the SVG to hide and/or show.
Previously, it was looking inside of a contentDocument embedded in an
<object> element. The simplified version just looks inside of the
SVG containing div.
2025-07-23 08:45:40 -04:00
Artem Kotik
c40bfb1445 Add regex and iregex filter lookup expressions and corresponding tests 2025-07-18 16:56:54 +02:00
14 changed files with 279 additions and 107 deletions

View File

@@ -80,18 +80,20 @@ GET /api/ipam/vlans/?vid__gt=900
String based (char) fields (Name, Address, etc) support these lookup expressions:
| Filter | Description |
|---------|----------------------------------------|
| `n` | Not equal to |
| `ic` | Contains (case-insensitive) |
| `nic` | Does not contain (case-insensitive) |
| `isw` | Starts with (case-insensitive) |
| `nisw` | Does not start with (case-insensitive) |
| `iew` | Ends with (case-insensitive) |
| `niew` | Does not end with (case-insensitive) |
| `ie` | Exact match (case-insensitive) |
| `nie` | Inverse exact match (case-insensitive) |
| `empty` | Is empty/null (boolean) |
| Filter | Description |
|----------|----------------------------------------|
| `n` | Not equal to |
| `ic` | Contains (case-insensitive) |
| `nic` | Does not contain (case-insensitive) |
| `isw` | Starts with (case-insensitive) |
| `nisw` | Does not start with (case-insensitive) |
| `iew` | Ends with (case-insensitive) |
| `niew` | Does not end with (case-insensitive) |
| `ie` | Exact match (case-insensitive) |
| `nie` | Inverse exact match (case-insensitive) |
| `empty` | Is empty/null (boolean) |
| `regex` | Regexp matching |
| `iregex` | Regexp matching (case-insensitive) |
Here is an example of a lookup expression on a string field that will return all devices with `switch` in the name:

View File

@@ -1335,6 +1335,13 @@ class MACAddressImportForm(NetBoxModelImportForm):
class CableImportForm(NetBoxModelImportForm):
# Termination A
side_a_site = CSVModelChoiceField(
label=_('Side A site'),
queryset=Site.objects.all(),
required=False,
to_field_name='name',
help_text=_('Site of parent device A (if any)'),
)
side_a_device = CSVModelChoiceField(
label=_('Side A device'),
queryset=Device.objects.all(),
@@ -1353,6 +1360,13 @@ class CableImportForm(NetBoxModelImportForm):
)
# Termination B
side_b_site = CSVModelChoiceField(
label=_('Side B site'),
queryset=Site.objects.all(),
required=False,
to_field_name='name',
help_text=_('Site of parent device B (if any)'),
)
side_b_device = CSVModelChoiceField(
label=_('Side B device'),
queryset=Device.objects.all(),
@@ -1396,14 +1410,39 @@ class CableImportForm(NetBoxModelImportForm):
required=False,
help_text=_('Length unit')
)
color = forms.CharField(
label=_('Color'),
required=False,
max_length=16,
help_text=_('Color name (e.g. "Red") or hex code (e.g. "f44336")')
)
class Meta:
model = Cable
fields = [
'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags',
'side_a_site', 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_site', 'side_b_device', 'side_b_type',
'side_b_name', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description',
'comments', 'tags',
]
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
if data:
# Limit choices for side_a_device to the assigned side_a_site
if side_a_site := data.get('side_a_site'):
side_a_device_params = {f'site__{self.fields["side_a_site"].to_field_name}': side_a_site}
self.fields['side_a_device'].queryset = self.fields['side_a_device'].queryset.filter(
**side_a_device_params
)
# Limit choices for side_b_device to the assigned side_b_site
if side_b_site := data.get('side_b_site'):
side_b_device_params = {f'site__{self.fields["side_b_site"].to_field_name}': side_b_site}
self.fields['side_b_device'].queryset = self.fields['side_b_device'].queryset.filter(
**side_b_device_params
)
def _clean_side(self, side):
"""
Derive a Cable's A/B termination objects.
@@ -1440,6 +1479,24 @@ class CableImportForm(NetBoxModelImportForm):
setattr(self.instance, f'{side}_terminations', [termination_object])
return termination_object
def _clean_color(self, color):
"""
Derive a colors hex code
:param color: color as hex or color name
"""
color_parsed = color.strip().lower()
for hex_code, label in ColorChoices.CHOICES:
if color.lower() == label.lower():
color_parsed = hex_code
if len(color_parsed) > 6:
raise forms.ValidationError(
_(f"{color} did not match any used color name and was longer than six characters: invalid hex.")
)
return color_parsed
def clean_side_a_name(self):
return self._clean_side('a')
@@ -1451,11 +1508,14 @@ class CableImportForm(NetBoxModelImportForm):
length_unit = self.cleaned_data.get('length_unit', None)
return length_unit if length_unit is not None else ''
def clean_color(self):
color = self.cleaned_data.get('color', None)
return self._clean_color(color) if color is not None else ''
#
# Virtual chassis
#
class VirtualChassisImportForm(NetBoxModelImportForm):
master = CSVModelChoiceField(
label=_('Master'),

View File

@@ -3266,17 +3266,27 @@ class CableTestCase(
@classmethod
def setUpTestData(cls):
site = Site.objects.create(name='Site 1', slug='site-1')
sites = (
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
)
Site.objects.bulk_create(sites)
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetype = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
vc = VirtualChassis.objects.create(name='Virtual Chassis')
# NOTE: By design, NetBox now allows for the creation of devices with the same name if they belong to
# different sites.
# The CSV test below demonstrates that devices with identical names on different sites can be created
# and referenced successfully.
devices = (
Device(name='Device 1', site=site, device_type=devicetype, role=role),
Device(name='Device 2', site=site, device_type=devicetype, role=role),
Device(name='Device 3', site=site, device_type=devicetype, role=role),
Device(name='Device 4', site=site, device_type=devicetype, role=role),
# Create 'Device 1' assigned to 'Site 1'
Device(name='Device 1', site=sites[0], device_type=devicetype, role=role),
Device(name='Device 2', site=sites[0], device_type=devicetype, role=role),
Device(name='Device 3', site=sites[0], device_type=devicetype, role=role),
# Create 'Device 1' assigned to 'Site 2' (allowed since the site is different)
Device(name='Device 1', site=sites[1], device_type=devicetype, role=role),
)
Device.objects.bulk_create(devices)
@@ -3327,13 +3337,15 @@ class CableTestCase(
'tags': [t.pk for t in tags],
}
# Ensure that CSV bulk import supports assigning terminations from parent devices that share
# the same device name, provided those devices belong to different sites.
cls.csv_data = (
"side_a_device,side_a_type,side_a_name,side_b_device,side_b_type,side_b_name",
"Device 3,dcim.interface,Interface 1,Device 4,dcim.interface,Interface 1",
"Device 3,dcim.interface,Interface 2,Device 4,dcim.interface,Interface 2",
"Device 3,dcim.interface,Interface 3,Device 4,dcim.interface,Interface 3",
"Device 1,dcim.interface,Device 2 Interface,Device 4,dcim.interface,Interface 4",
"Device 1,dcim.interface,Device 3 Interface,Device 4,dcim.interface,Interface 5",
"side_a_site,side_a_device,side_a_type,side_a_name,side_b_site,side_b_device,side_b_type,side_b_name",
"Site 1,Device 3,dcim.interface,Interface 1,Site 2,Device 1,dcim.interface,Interface 1",
"Site 1,Device 3,dcim.interface,Interface 2,Site 2,Device 1,dcim.interface,Interface 2",
"Site 1,Device 3,dcim.interface,Interface 3,Site 2,Device 1,dcim.interface,Interface 3",
"Site 1,Device 1,dcim.interface,Device 2 Interface,Site 2,Device 1,dcim.interface,Interface 4",
"Site 1,Device 1,dcim.interface,Device 3 Interface,Site 2,Device 1,dcim.interface,Interface 5",
)
cls.csv_update_data = (

View File

@@ -1,5 +1,7 @@
from django.conf import settings
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.views.static import serve
from django_rq.queues import get_connection
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import status
@@ -200,6 +202,17 @@ class ImageAttachmentViewSet(NetBoxModelViewSet):
serializer_class = serializers.ImageAttachmentSerializer
filterset_class = filtersets.ImageAttachmentFilterSet
@action(
methods=['GET'],
detail=True,
url_path='download',
url_name='download',
)
def download(self, request, pk, *args, **kwargs):
obj = get_object_or_404(self.queryset, pk=pk)
# Render and return the elevation as an SVG drawing with the correct content type
return serve(request, obj.image.name, document_root=settings.MEDIA_ROOT)
#
# Journal entries

View File

@@ -1,6 +1,8 @@
import datetime
from PIL import Image
from django.contrib.contenttypes.models import ContentType
from django.core.files.base import File
from django.urls import reverse
from django.utils.timezone import make_aware, now
from rest_framework import status
@@ -616,6 +618,38 @@ class ImageAttachmentTest(
)
ImageAttachment.objects.bulk_create(image_attachments)
def test_image_download(self):
self.add_permissions('extras.view_imageattachment')
ct = ContentType.objects.get_for_model(Site)
site = Site.objects.get(name='Site 1', slug='site-1')
image = Image.new('RGB', size=(1, 1), color=(255, 0, 0))
image.save('test_image_download.png', format='PNG')
image_file = File(open('test_image_download.png', 'rb'))
content = image_file.read()
attachment = ImageAttachment(
object_type=ct,
object_id=site.pk,
name='Image Attachment 4',
image_height=1,
image_width=1
)
attachment.image.save('test_image_download.png', image_file, save=True)
attachment.save()
image = ImageAttachment.objects.get(name='Image Attachment 4')
url = reverse('extras-api:imageattachment-download', kwargs={'pk': image.pk})
response = self.client.get(url, **self.header)
downloaded_content = b''.join(response.streaming_content)
self.assertEqual(response.headers.get('Content-Type'), 'image/png')
self.assertEqual(response.headers.get('Content-Length'), '69')
self.assertEqual(
response.headers.get('Content-Disposition'), f'inline; filename="site_{site.pk}_Image_Attachment_4.png"'
)
self.assertEqual(content, downloaded_content)
class JournalEntryTest(APIViewTestCases.APIViewTestCase):
model = JournalEntry

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -35,7 +35,7 @@ function showRackElements(
selector: string,
elevation: HTMLObjectElement,
): void {
const elements = elevation.contentDocument?.querySelectorAll(selector) ?? [];
const elements = elevation.querySelectorAll(selector) ?? [];
for (const element of elements) {
element.classList.remove('hidden');
}
@@ -45,7 +45,7 @@ function hideRackElements(
selector: string,
elevation: HTMLObjectElement,
): void {
const elements = elevation.contentDocument?.querySelectorAll(selector) ?? [];
const elements = elevation.querySelectorAll(selector) ?? [];
for (const element of elements) {
element.classList.add('hidden');
}

View File

@@ -1,5 +1,5 @@
{% load i18n %}
<div style="margin-left: -30px">
<div style="margin-left: -30px" class="rack_elevation">
<div
hx-get="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{ face }}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}"
hx-trigger="intersect"

View File

@@ -45,12 +45,17 @@ class TenantBulkEditForm(NetBoxModelBulkEditForm):
queryset=TenantGroup.objects.all(),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
model = Tenant
fieldsets = (
FieldSet('group'),
FieldSet('group', 'description'),
)
nullable_fields = ('group',)
nullable_fields = ('group', 'description')
#

View File

@@ -98,6 +98,7 @@ class TenantTestCase(ViewTestCases.PrimaryObjectViewTestCase):
cls.bulk_edit_data = {
'group': tenant_groups[1].pk,
'description': 'Bulk edit description',
}

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-16 05:05+0000\n"
"POT-Creation-Date: 2025-07-24 05:05+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"
@@ -215,8 +215,8 @@ msgstr ""
#: netbox/dcim/forms/bulk_edit.py:344 netbox/dcim/forms/bulk_edit.py:730
#: netbox/dcim/forms/bulk_edit.py:935 netbox/dcim/forms/bulk_import.py:134
#: netbox/dcim/forms/bulk_import.py:236 netbox/dcim/forms/bulk_import.py:337
#: netbox/dcim/forms/bulk_import.py:598 netbox/dcim/forms/bulk_import.py:1479
#: netbox/dcim/forms/bulk_import.py:1507 netbox/dcim/forms/filtersets.py:89
#: netbox/dcim/forms/bulk_import.py:598 netbox/dcim/forms/bulk_import.py:1512
#: netbox/dcim/forms/bulk_import.py:1540 netbox/dcim/forms/filtersets.py:89
#: netbox/dcim/forms/filtersets.py:227 netbox/dcim/forms/filtersets.py:344
#: netbox/dcim/forms/filtersets.py:441 netbox/dcim/forms/filtersets.py:773
#: netbox/dcim/forms/filtersets.py:992 netbox/dcim/forms/filtersets.py:1065
@@ -587,9 +587,10 @@ msgstr ""
#: netbox/templates/wireless/wirelesslan.html:34
#: netbox/templates/wireless/wirelesslangroup.html:33
#: netbox/templates/wireless/wirelesslink.html:34
#: netbox/tenancy/forms/bulk_edit.py:32 netbox/tenancy/forms/bulk_edit.py:82
#: netbox/tenancy/forms/bulk_edit.py:130 netbox/users/forms/bulk_edit.py:64
#: netbox/users/forms/bulk_edit.py:82 netbox/users/forms/bulk_edit.py:112
#: netbox/tenancy/forms/bulk_edit.py:32 netbox/tenancy/forms/bulk_edit.py:49
#: netbox/tenancy/forms/bulk_edit.py:87 netbox/tenancy/forms/bulk_edit.py:135
#: netbox/users/forms/bulk_edit.py:64 netbox/users/forms/bulk_edit.py:82
#: netbox/users/forms/bulk_edit.py:112
#: netbox/virtualization/forms/bulk_edit.py:33
#: netbox/virtualization/forms/bulk_edit.py:47
#: netbox/virtualization/forms/bulk_edit.py:82
@@ -692,8 +693,8 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:766 netbox/dcim/forms/bulk_import.py:792
#: netbox/dcim/forms/bulk_import.py:818 netbox/dcim/forms/bulk_import.py:838
#: netbox/dcim/forms/bulk_import.py:924 netbox/dcim/forms/bulk_import.py:1018
#: netbox/dcim/forms/bulk_import.py:1060 netbox/dcim/forms/bulk_import.py:1381
#: netbox/dcim/forms/bulk_import.py:1544 netbox/dcim/forms/filtersets.py:1023
#: netbox/dcim/forms/bulk_import.py:1060 netbox/dcim/forms/bulk_import.py:1395
#: netbox/dcim/forms/bulk_import.py:1577 netbox/dcim/forms/filtersets.py:1023
#: netbox/dcim/forms/filtersets.py:1122 netbox/dcim/forms/filtersets.py:1243
#: netbox/dcim/forms/filtersets.py:1315 netbox/dcim/forms/filtersets.py:1340
#: netbox/dcim/forms/filtersets.py:1364 netbox/dcim/forms/filtersets.py:1384
@@ -763,8 +764,8 @@ msgstr ""
#: netbox/dcim/forms/bulk_edit.py:1819 netbox/dcim/forms/bulk_import.py:91
#: netbox/dcim/forms/bulk_import.py:150 netbox/dcim/forms/bulk_import.py:254
#: netbox/dcim/forms/bulk_import.py:563 netbox/dcim/forms/bulk_import.py:717
#: netbox/dcim/forms/bulk_import.py:1168 netbox/dcim/forms/bulk_import.py:1375
#: netbox/dcim/forms/bulk_import.py:1539 netbox/dcim/forms/bulk_import.py:1603
#: netbox/dcim/forms/bulk_import.py:1168 netbox/dcim/forms/bulk_import.py:1389
#: netbox/dcim/forms/bulk_import.py:1572 netbox/dcim/forms/bulk_import.py:1636
#: netbox/dcim/forms/filtersets.py:180 netbox/dcim/forms/filtersets.py:239
#: netbox/dcim/forms/filtersets.py:361 netbox/dcim/forms/filtersets.py:819
#: netbox/dcim/forms/filtersets.py:944 netbox/dcim/forms/filtersets.py:1026
@@ -841,8 +842,8 @@ msgstr ""
#: netbox/dcim/forms/bulk_edit.py:856 netbox/dcim/forms/bulk_edit.py:1824
#: netbox/dcim/forms/bulk_import.py:110 netbox/dcim/forms/bulk_import.py:155
#: netbox/dcim/forms/bulk_import.py:247 netbox/dcim/forms/bulk_import.py:362
#: netbox/dcim/forms/bulk_import.py:537 netbox/dcim/forms/bulk_import.py:1387
#: netbox/dcim/forms/bulk_import.py:1596 netbox/dcim/forms/filtersets.py:175
#: netbox/dcim/forms/bulk_import.py:537 netbox/dcim/forms/bulk_import.py:1401
#: netbox/dcim/forms/bulk_import.py:1629 netbox/dcim/forms/filtersets.py:175
#: netbox/dcim/forms/filtersets.py:207 netbox/dcim/forms/filtersets.py:325
#: netbox/dcim/forms/filtersets.py:401 netbox/dcim/forms/filtersets.py:422
#: netbox/dcim/forms/filtersets.py:742 netbox/dcim/forms/filtersets.py:936
@@ -1012,7 +1013,7 @@ msgstr ""
#: netbox/circuits/forms/bulk_edit.py:215
#: netbox/circuits/forms/model_forms.py:171
#: netbox/dcim/forms/bulk_import.py:1348 netbox/dcim/forms/bulk_import.py:1366
#: netbox/dcim/forms/bulk_import.py:1355 netbox/dcim/forms/bulk_import.py:1380
msgid "Termination type"
msgstr ""
@@ -1065,7 +1066,7 @@ msgstr ""
#: netbox/templates/dcim/virtualchassis.html:68
#: netbox/templates/dcim/virtualchassis_edit.html:60
#: netbox/templates/ipam/inc/panels/fhrp_groups.html:26
#: netbox/tenancy/forms/bulk_edit.py:159 netbox/tenancy/forms/filtersets.py:110
#: netbox/tenancy/forms/bulk_edit.py:164 netbox/tenancy/forms/filtersets.py:110
msgid "Priority"
msgstr ""
@@ -1116,7 +1117,7 @@ msgstr ""
#: netbox/templates/virtualization/virtualmachine.html:23
#: netbox/templates/vpn/tunneltermination.html:17
#: netbox/templates/wireless/inc/wirelesslink_interface.html:20
#: netbox/tenancy/forms/bulk_edit.py:154 netbox/tenancy/forms/filtersets.py:107
#: netbox/tenancy/forms/bulk_edit.py:159 netbox/tenancy/forms/filtersets.py:107
#: netbox/tenancy/forms/model_forms.py:139
#: netbox/tenancy/tables/contacts.py:110
#: netbox/virtualization/forms/bulk_edit.py:127
@@ -1148,7 +1149,7 @@ msgstr ""
#: netbox/circuits/forms/bulk_import.py:229 netbox/dcim/forms/bulk_import.py:93
#: netbox/dcim/forms/bulk_import.py:152 netbox/dcim/forms/bulk_import.py:256
#: netbox/dcim/forms/bulk_import.py:565 netbox/dcim/forms/bulk_import.py:719
#: netbox/dcim/forms/bulk_import.py:1170 netbox/dcim/forms/bulk_import.py:1541
#: netbox/dcim/forms/bulk_import.py:1170 netbox/dcim/forms/bulk_import.py:1574
#: netbox/ipam/forms/bulk_import.py:197 netbox/ipam/forms/bulk_import.py:265
#: netbox/ipam/forms/bulk_import.py:301 netbox/ipam/forms/bulk_import.py:498
#: netbox/ipam/forms/bulk_import.py:511
@@ -1164,8 +1165,8 @@ msgstr ""
#: netbox/circuits/forms/bulk_import.py:236
#: netbox/dcim/forms/bulk_import.py:114 netbox/dcim/forms/bulk_import.py:159
#: netbox/dcim/forms/bulk_import.py:366 netbox/dcim/forms/bulk_import.py:541
#: netbox/dcim/forms/bulk_import.py:1391 netbox/dcim/forms/bulk_import.py:1536
#: netbox/dcim/forms/bulk_import.py:1600 netbox/ipam/forms/bulk_import.py:45
#: netbox/dcim/forms/bulk_import.py:1405 netbox/dcim/forms/bulk_import.py:1569
#: netbox/dcim/forms/bulk_import.py:1633 netbox/ipam/forms/bulk_import.py:45
#: netbox/ipam/forms/bulk_import.py:74 netbox/ipam/forms/bulk_import.py:102
#: netbox/ipam/forms/bulk_import.py:122 netbox/ipam/forms/bulk_import.py:142
#: netbox/ipam/forms/bulk_import.py:171 netbox/ipam/forms/bulk_import.py:260
@@ -1245,8 +1246,8 @@ msgstr ""
#: netbox/dcim/forms/bulk_edit.py:466 netbox/dcim/forms/bulk_edit.py:735
#: netbox/dcim/forms/bulk_edit.py:790 netbox/dcim/forms/bulk_edit.py:944
#: netbox/dcim/forms/bulk_import.py:241 netbox/dcim/forms/bulk_import.py:343
#: netbox/dcim/forms/bulk_import.py:604 netbox/dcim/forms/bulk_import.py:1485
#: netbox/dcim/forms/bulk_import.py:1519 netbox/dcim/forms/filtersets.py:97
#: netbox/dcim/forms/bulk_import.py:604 netbox/dcim/forms/bulk_import.py:1518
#: netbox/dcim/forms/bulk_import.py:1552 netbox/dcim/forms/filtersets.py:97
#: netbox/dcim/forms/filtersets.py:324 netbox/dcim/forms/filtersets.py:358
#: netbox/dcim/forms/filtersets.py:398 netbox/dcim/forms/filtersets.py:449
#: netbox/dcim/forms/filtersets.py:739 netbox/dcim/forms/filtersets.py:782
@@ -1948,7 +1949,7 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:1007 netbox/dcim/forms/bulk_import.py:1055
#: netbox/dcim/forms/bulk_import.py:1072 netbox/dcim/forms/bulk_import.py:1084
#: netbox/dcim/forms/bulk_import.py:1132 netbox/dcim/forms/bulk_import.py:1254
#: netbox/dcim/forms/bulk_import.py:1590 netbox/dcim/forms/connections.py:24
#: netbox/dcim/forms/bulk_import.py:1623 netbox/dcim/forms/connections.py:24
#: netbox/dcim/forms/filtersets.py:133 netbox/dcim/forms/filtersets.py:941
#: netbox/dcim/forms/filtersets.py:973 netbox/dcim/forms/filtersets.py:1119
#: netbox/dcim/forms/filtersets.py:1310 netbox/dcim/forms/filtersets.py:1335
@@ -3106,7 +3107,7 @@ msgstr ""
#: netbox/templates/tenancy/tenantgroup.html:37
#: netbox/templates/virtualization/vminterface.html:39
#: netbox/templates/wireless/wirelesslangroup.html:37
#: netbox/tenancy/forms/bulk_edit.py:27 netbox/tenancy/forms/bulk_edit.py:62
#: netbox/tenancy/forms/bulk_edit.py:27 netbox/tenancy/forms/bulk_edit.py:67
#: netbox/tenancy/forms/bulk_import.py:24
#: netbox/tenancy/forms/bulk_import.py:58
#: netbox/tenancy/forms/model_forms.py:25
@@ -4193,8 +4194,8 @@ msgstr ""
#: netbox/dcim/forms/bulk_edit.py:465 netbox/dcim/forms/bulk_edit.py:972
#: netbox/dcim/forms/bulk_import.py:350 netbox/dcim/forms/bulk_import.py:353
#: netbox/dcim/forms/bulk_import.py:611 netbox/dcim/forms/bulk_import.py:1526
#: netbox/dcim/forms/bulk_import.py:1530 netbox/dcim/forms/filtersets.py:106
#: netbox/dcim/forms/bulk_import.py:611 netbox/dcim/forms/bulk_import.py:1559
#: netbox/dcim/forms/bulk_import.py:1563 netbox/dcim/forms/filtersets.py:106
#: netbox/dcim/forms/filtersets.py:326 netbox/dcim/forms/filtersets.py:407
#: netbox/dcim/forms/filtersets.py:421 netbox/dcim/forms/filtersets.py:459
#: netbox/dcim/forms/filtersets.py:792 netbox/dcim/forms/filtersets.py:1005
@@ -4394,8 +4395,8 @@ msgstr ""
msgid "Length"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:875 netbox/dcim/forms/bulk_import.py:1394
#: netbox/dcim/forms/bulk_import.py:1397 netbox/dcim/forms/filtersets.py:1140
#: netbox/dcim/forms/bulk_edit.py:875 netbox/dcim/forms/bulk_import.py:1408
#: netbox/dcim/forms/bulk_import.py:1411 netbox/dcim/forms/filtersets.py:1140
msgid "Length unit"
msgstr ""
@@ -4404,17 +4405,17 @@ msgstr ""
msgid "Domain"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:967 netbox/dcim/forms/bulk_import.py:1513
#: netbox/dcim/forms/bulk_edit.py:967 netbox/dcim/forms/bulk_import.py:1546
#: netbox/dcim/forms/filtersets.py:1226 netbox/dcim/forms/model_forms.py:855
msgid "Power panel"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:989 netbox/dcim/forms/bulk_import.py:1549
#: netbox/dcim/forms/bulk_edit.py:989 netbox/dcim/forms/bulk_import.py:1582
#: netbox/dcim/forms/filtersets.py:1248 netbox/templates/dcim/powerfeed.html:83
msgid "Supply"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:995 netbox/dcim/forms/bulk_import.py:1554
#: netbox/dcim/forms/bulk_edit.py:995 netbox/dcim/forms/bulk_import.py:1587
#: netbox/dcim/forms/filtersets.py:1253 netbox/templates/dcim/powerfeed.html:95
msgid "Phase"
msgstr ""
@@ -4652,7 +4653,7 @@ msgid "available options"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:137 netbox/dcim/forms/bulk_import.py:601
#: netbox/dcim/forms/bulk_import.py:1510 netbox/ipam/forms/bulk_import.py:479
#: netbox/dcim/forms/bulk_import.py:1543 netbox/ipam/forms/bulk_import.py:479
#: netbox/virtualization/forms/bulk_import.py:64
#: netbox/virtualization/forms/bulk_import.py:95
msgid "Assigned site"
@@ -4715,7 +4716,7 @@ msgstr ""
msgid "Parent site"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:347 netbox/dcim/forms/bulk_import.py:1523
#: netbox/dcim/forms/bulk_import.py:347 netbox/dcim/forms/bulk_import.py:1556
msgid "Rack's location (if any)"
msgstr ""
@@ -4766,7 +4767,7 @@ msgstr ""
msgid "Limit platform assignments to this manufacturer"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:534 netbox/dcim/forms/bulk_import.py:1593
#: netbox/dcim/forms/bulk_import.py:534 netbox/dcim/forms/bulk_import.py:1626
#: netbox/tenancy/forms/bulk_import.py:105
msgid "Assigned role"
msgstr ""
@@ -4956,7 +4957,7 @@ msgid "Corresponding rear port"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1020 netbox/dcim/forms/bulk_import.py:1061
#: netbox/dcim/forms/bulk_import.py:1384
#: netbox/dcim/forms/bulk_import.py:1398
msgid "Physical medium classification"
msgstr ""
@@ -5045,102 +5046,120 @@ msgid "Must specify the parent device or VM when assigning an interface"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1339
msgid "Side A site"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1343
#: netbox/wireless/forms/bulk_import.py:94
msgid "Site of parent device A (if any)"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1346
msgid "Side A device"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1342 netbox/dcim/forms/bulk_import.py:1360
#: netbox/dcim/forms/bulk_import.py:1349 netbox/dcim/forms/bulk_import.py:1374
msgid "Device name"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1345
#: netbox/dcim/forms/bulk_import.py:1352
msgid "Side A type"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1351
#: netbox/dcim/forms/bulk_import.py:1358
msgid "Side A name"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1352 netbox/dcim/forms/bulk_import.py:1370
#: netbox/dcim/forms/bulk_import.py:1359 netbox/dcim/forms/bulk_import.py:1384
msgid "Termination name"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1357
#: netbox/dcim/forms/bulk_import.py:1364
msgid "Side B site"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1368
#: netbox/wireless/forms/bulk_import.py:115
msgid "Site of parent device B (if any)"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1371
msgid "Side B device"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1363
#: netbox/dcim/forms/bulk_import.py:1377
msgid "Side B type"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1369
#: netbox/dcim/forms/bulk_import.py:1383
msgid "Side B name"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1378
#: netbox/dcim/forms/bulk_import.py:1392
#: netbox/wireless/forms/bulk_import.py:134
msgid "Connection status"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1430
#: netbox/dcim/forms/bulk_import.py:1463
#, python-brace-format
msgid "Side {side_upper}: {device} {termination_object} is already connected"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1436
#: netbox/dcim/forms/bulk_import.py:1469
#, python-brace-format
msgid "{side_upper} side termination not found: {device} {name}"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1461 netbox/dcim/forms/model_forms.py:891
#: netbox/dcim/forms/bulk_import.py:1494 netbox/dcim/forms/model_forms.py:891
#: netbox/dcim/tables/devices.py:1069 netbox/templates/dcim/device.html:138
#: netbox/templates/dcim/virtualchassis.html:27
#: netbox/templates/dcim/virtualchassis.html:67
msgid "Master"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1465
#: netbox/dcim/forms/bulk_import.py:1498
msgid "Master device"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1482
#: netbox/dcim/forms/bulk_import.py:1515
msgid "Name of parent site"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1516
#: netbox/dcim/forms/bulk_import.py:1549
msgid "Upstream power panel"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1546
#: netbox/dcim/forms/bulk_import.py:1579
msgid "Primary or redundant"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1551
#: netbox/dcim/forms/bulk_import.py:1584
msgid "Supply type (AC/DC)"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1556
#: netbox/dcim/forms/bulk_import.py:1589
msgid "Single or three-phase"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1607 netbox/dcim/forms/model_forms.py:1847
#: netbox/dcim/forms/bulk_import.py:1640 netbox/dcim/forms/model_forms.py:1847
#: netbox/templates/dcim/device.html:196
#: netbox/templates/dcim/virtualdevicecontext.html:30
#: netbox/templates/virtualization/virtualmachine.html:52
msgid "Primary IPv4"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1611
#: netbox/dcim/forms/bulk_import.py:1644
msgid "IPv4 address with mask, e.g. 1.2.3.4/24"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1614 netbox/dcim/forms/model_forms.py:1856
#: netbox/dcim/forms/bulk_import.py:1647 netbox/dcim/forms/model_forms.py:1856
#: netbox/templates/dcim/device.html:212
#: netbox/templates/dcim/virtualdevicecontext.html:41
#: netbox/templates/virtualization/virtualmachine.html:68
msgid "Primary IPv6"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1618
#: netbox/dcim/forms/bulk_import.py:1651
msgid "IPv6 address with prefix length, e.g. 2001:db8::1/64"
msgstr ""
@@ -7784,7 +7803,7 @@ msgid "No"
msgstr ""
#: netbox/extras/choices.py:108 netbox/templates/tenancy/contact.html:67
#: netbox/tenancy/forms/bulk_edit.py:125
#: netbox/tenancy/forms/bulk_edit.py:130
#: netbox/wireless/forms/model_forms.py:173
msgid "Link"
msgstr ""
@@ -8325,7 +8344,7 @@ msgstr ""
#: netbox/netbox/navigation/menu.py:433
#: netbox/templates/extras/notificationgroup.html:31
#: netbox/templates/tenancy/contact.html:21
#: netbox/tenancy/forms/bulk_edit.py:139 netbox/tenancy/forms/filtersets.py:78
#: netbox/tenancy/forms/bulk_edit.py:144 netbox/tenancy/forms/filtersets.py:78
#: netbox/tenancy/forms/model_forms.py:99 netbox/tenancy/tables/contacts.py:68
#: netbox/users/forms/model_forms.py:182 netbox/users/forms/model_forms.py:194
#: netbox/users/forms/model_forms.py:306 netbox/users/tables.py:35
@@ -9855,7 +9874,7 @@ msgstr ""
#: netbox/ipam/filtersets.py:466 netbox/ipam/filtersets.py:470
#: netbox/ipam/filtersets.py:562 netbox/ipam/forms/model_forms.py:506
#: netbox/templates/tenancy/contact.html:63
#: netbox/tenancy/forms/bulk_edit.py:120
#: netbox/tenancy/forms/bulk_edit.py:125
msgid "Address"
msgstr ""
@@ -12311,7 +12330,7 @@ msgstr ""
#: netbox/templates/account/profile.html:27
#: netbox/templates/tenancy/contact.html:53 netbox/templates/users/user.html:23
#: netbox/tenancy/forms/bulk_edit.py:116
#: netbox/tenancy/forms/bulk_edit.py:121
msgid "Email"
msgstr ""
@@ -14826,7 +14845,7 @@ msgid ""
msgstr ""
#: netbox/templates/tenancy/contact.html:18 netbox/tenancy/filtersets.py:152
#: netbox/tenancy/forms/bulk_edit.py:149 netbox/tenancy/forms/filtersets.py:102
#: netbox/tenancy/forms/bulk_edit.py:154 netbox/tenancy/forms/filtersets.py:102
#: netbox/tenancy/forms/forms.py:57 netbox/tenancy/forms/model_forms.py:108
#: netbox/tenancy/forms/model_forms.py:132
#: netbox/tenancy/tables/contacts.py:106
@@ -14834,12 +14853,12 @@ msgid "Contact"
msgstr ""
#: netbox/templates/tenancy/contact.html:39
#: netbox/tenancy/forms/bulk_edit.py:106
#: netbox/tenancy/forms/bulk_edit.py:111
msgid "Title"
msgstr ""
#: netbox/templates/tenancy/contact.html:43
#: netbox/tenancy/forms/bulk_edit.py:111 netbox/tenancy/tables/contacts.py:72
#: netbox/tenancy/forms/bulk_edit.py:116 netbox/tenancy/tables/contacts.py:72
msgid "Phone"
msgstr ""
@@ -15208,15 +15227,15 @@ msgstr ""
msgid "Tenant Group (slug)"
msgstr ""
#: netbox/tenancy/forms/bulk_edit.py:67
#: netbox/tenancy/forms/bulk_edit.py:72
msgid "Desciption"
msgstr ""
#: netbox/tenancy/forms/bulk_edit.py:96
#: netbox/tenancy/forms/bulk_edit.py:101
msgid "Add groups"
msgstr ""
#: netbox/tenancy/forms/bulk_edit.py:101
#: netbox/tenancy/forms/bulk_edit.py:106
msgid "Remove groups"
msgstr ""
@@ -16673,10 +16692,6 @@ msgstr ""
msgid "Bridged VLAN"
msgstr ""
#: netbox/wireless/forms/bulk_import.py:94
msgid "Site of parent device A (if any)"
msgstr ""
#: netbox/wireless/forms/bulk_import.py:100
msgid "Parent device of assigned interface A"
msgstr ""
@@ -16690,10 +16705,6 @@ msgstr ""
msgid "Assigned interface A"
msgstr ""
#: netbox/wireless/forms/bulk_import.py:115
msgid "Site of parent device B (if any)"
msgstr ""
#: netbox/wireless/forms/bulk_import.py:121
msgid "Parent device of assigned interface B"
msgstr ""

View File

@@ -13,6 +13,8 @@ FILTER_CHAR_BASED_LOOKUP_MAP = dict(
ie='iexact',
nie='iexact',
empty='empty',
regex='regex',
iregex='iregex',
)
FILTER_NUMERIC_BASED_LOOKUP_MAP = dict(

View File

@@ -180,6 +180,10 @@ class BaseFilterSetTest(TestCase):
self.assertEqual(self.filters['charfield__niew'].exclude, True)
self.assertEqual(self.filters['charfield__empty'].lookup_expr, 'empty')
self.assertEqual(self.filters['charfield__empty'].exclude, False)
self.assertEqual(self.filters['charfield__regex'].lookup_expr, 'regex')
self.assertEqual(self.filters['charfield__regex'].exclude, False)
self.assertEqual(self.filters['charfield__iregex'].lookup_expr, 'iregex')
self.assertEqual(self.filters['charfield__iregex'].exclude, False)
def test_number_filter(self):
self.assertIsInstance(self.filters['numberfield'], django_filters.NumberFilter)
@@ -220,6 +224,10 @@ class BaseFilterSetTest(TestCase):
self.assertEqual(self.filters['macaddressfield__iew'].exclude, False)
self.assertEqual(self.filters['macaddressfield__niew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['macaddressfield__niew'].exclude, True)
self.assertEqual(self.filters['macaddressfield__regex'].lookup_expr, 'regex')
self.assertEqual(self.filters['macaddressfield__regex'].exclude, False)
self.assertEqual(self.filters['macaddressfield__iregex'].lookup_expr, 'iregex')
self.assertEqual(self.filters['macaddressfield__iregex'].exclude, False)
def test_model_choice_filter(self):
self.assertIsInstance(self.filters['modelchoicefield'], django_filters.ModelChoiceFilter)
@@ -257,6 +265,10 @@ class BaseFilterSetTest(TestCase):
self.assertEqual(self.filters['multivaluecharfield__iew'].exclude, False)
self.assertEqual(self.filters['multivaluecharfield__niew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['multivaluecharfield__niew'].exclude, True)
self.assertEqual(self.filters['multivaluecharfield__regex'].lookup_expr, 'regex')
self.assertEqual(self.filters['multivaluecharfield__regex'].exclude, False)
self.assertEqual(self.filters['multivaluecharfield__iregex'].lookup_expr, 'iregex')
self.assertEqual(self.filters['multivaluecharfield__iregex'].exclude, False)
def test_multi_value_date_filter(self):
self.assertIsInstance(self.filters['datefield'], MultiValueDateFilter)
@@ -340,6 +352,10 @@ class BaseFilterSetTest(TestCase):
self.assertEqual(self.filters['multiplechoicefield__iew'].exclude, False)
self.assertEqual(self.filters['multiplechoicefield__niew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['multiplechoicefield__niew'].exclude, True)
self.assertEqual(self.filters['multiplechoicefield__regex'].lookup_expr, 'regex')
self.assertEqual(self.filters['multiplechoicefield__regex'].exclude, False)
self.assertEqual(self.filters['multiplechoicefield__iregex'].lookup_expr, 'iregex')
self.assertEqual(self.filters['multiplechoicefield__iregex'].exclude, False)
def test_tag_filter(self):
self.assertIsInstance(self.filters['tagfield'], TagFilter)
@@ -534,6 +550,14 @@ class DynamicFilterLookupExpressionTest(TestCase):
params = {'slug__niew': ['-1']}
self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 2)
def test_site_slug_regex(self):
params = {'slug__regex': ['^def-[a-z]*-2$']}
self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 1)
def test_site_slug_iregex(self):
params = {'slug__iregex': ['^DEF-[a-z]*-2$']}
self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 1)
def test_provider_asn_lt(self):
params = {'asn__lt': [65101]}
self.assertEqual(ASNFilterSet(params, ASN.objects.all()).qs.count(), 1)
@@ -618,6 +642,14 @@ class DynamicFilterLookupExpressionTest(TestCase):
params = {'mac_address__nic': ['aa:', 'bb']}
self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 1)
def test_device_mac_address_regex(self):
params = {'mac_address__regex': ['^cc.*:03$']}
self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 1)
def test_device_mac_address_iregex(self):
params = {'mac_address__iregex': ['^CC.*:03$']}
self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 1)
def test_interface_rf_role_empty(self):
params = {'rf_role__empty': 'true'}
self.assertEqual(InterfaceFilterSet(params, Interface.objects.all()).qs.count(), 5)