mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-18 09:42:18 -06:00
Compare commits
12 Commits
v4.3.4
...
576099e3b8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
576099e3b8 | ||
|
|
a5d1500307 | ||
|
|
ffa9a52667 | ||
|
|
47320f9958 | ||
|
|
d08a1bd07d | ||
|
|
14c4aeca54 | ||
|
|
26bec1275f | ||
|
|
fa2d7f6516 | ||
|
|
d571cb4867 | ||
|
|
2129355c30 | ||
|
|
c40bfb1445 | ||
|
|
b88b5b0b1b |
@@ -302,13 +302,6 @@ Quit the server with CONTROL-C.
|
||||
|
||||
Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on port 8000; for example, <http://127.0.0.1:8000/>. You should be greeted with the NetBox home page. Try logging in using the username and password specified when creating a superuser.
|
||||
|
||||
!!! note
|
||||
By default RHEL based distros will likely block your testing attempts with firewalld. The development server port can be opened with `firewall-cmd` (add `--permanent` if you want the rule to survive server restarts):
|
||||
|
||||
```no-highlight
|
||||
firewall-cmd --zone=public --add-port=8000/tcp
|
||||
```
|
||||
|
||||
!!! danger "Not for production use"
|
||||
The development server is for development and testing purposes only. It is neither performant nor secure enough for production use. **Do not use it in production.**
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -3,6 +3,7 @@ import svgwrite
|
||||
from svgwrite.container import Hyperlink
|
||||
from svgwrite.image import Image
|
||||
from svgwrite.gradients import LinearGradient
|
||||
from svgwrite.masking import ClipPath
|
||||
from svgwrite.shapes import Rect
|
||||
from svgwrite.text import Text
|
||||
|
||||
@@ -67,6 +68,20 @@ def get_device_description(device):
|
||||
return description
|
||||
|
||||
|
||||
def truncate_text(text, width, font_size=15):
|
||||
"""
|
||||
Truncate text to fit within the width of a rectangle.
|
||||
|
||||
:param text: The text to truncate
|
||||
:param width: Width of rectangle
|
||||
:param font_size: Font size (default is 15, ~0.875rem)
|
||||
"""
|
||||
char_width = font_size * 0.6 # 0.6 is an approximation of the average character width in pixels
|
||||
max_char = int(width / char_width)
|
||||
|
||||
return text if len(text) <= max_char else text[:max_char] + '...'
|
||||
|
||||
|
||||
class RackElevationSVG:
|
||||
"""
|
||||
Use this class to render a rack elevation as an SVG image.
|
||||
@@ -177,12 +192,26 @@ class RackElevationSVG:
|
||||
link = Hyperlink(href=f'{self.base_url}{device.get_absolute_url()}', target="_parent")
|
||||
link.set_desc(description)
|
||||
|
||||
# Create clipPath element
|
||||
# This is necessary as fallback because the truncate_text method is an approximation
|
||||
clip_id = f"clip-{device.id}"
|
||||
clip_path = ClipPath(id=clip_id)
|
||||
clip_path.add(Rect(coords, size))
|
||||
|
||||
self.drawing.defs.add(clip_path)
|
||||
|
||||
# Name to display
|
||||
display_name = truncate_text(name, size[0])
|
||||
|
||||
# Add rect element to hyperlink
|
||||
if color:
|
||||
link.add(Rect(coords, size, style=f'fill: #{color}', class_=f'slot{css_extra}'))
|
||||
else:
|
||||
link.add(Rect(coords, size, class_=f'slot blocked{css_extra}'))
|
||||
link.add(Text(name, insert=text_coords, fill=text_color, class_=f'label{css_extra}'))
|
||||
link.add(
|
||||
Text(display_name, insert=text_coords, fill=text_color, clip_path=f"url(#{clip_id})",
|
||||
class_=f'label{css_extra}')
|
||||
)
|
||||
|
||||
# Embed device type image if provided
|
||||
if self.include_images and image:
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
2
netbox/project-static/dist/netbox.js
vendored
2
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
6
netbox/project-static/dist/netbox.js.map
vendored
6
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -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');
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ Blocks:
|
||||
{# Release info #}
|
||||
<div class="text-muted text-center fs-5 my-3">
|
||||
{{ settings.RELEASE.name }}
|
||||
{% if not settings.RELEASE.features.commercial %}
|
||||
{% if not settings.RELEASE.features.commercial and not settings.ISOLATED_DEPLOYMENT %}
|
||||
<div>
|
||||
<a href="https://netboxlabs.com/netbox-cloud/" class="text-muted">{% trans "Get" %} Cloud</a> |
|
||||
<a href="https://netboxlabs.com/netbox-enterprise/" class="text-muted">{% trans "Get" %} Enterprise</a>
|
||||
@@ -184,7 +184,7 @@ Blocks:
|
||||
{% endif %}
|
||||
|
||||
{# Commercial links #}
|
||||
{% if settings.RELEASE.features.commercial %}
|
||||
{% if settings.RELEASE.features.commercial and not settings.ISOLATED_DEPLOYMENT %}
|
||||
{# LinkedIn #}
|
||||
<li class="list-inline-item">
|
||||
<a href="https://www.linkedin.com/company/netboxlabs/" target="_blank" class="link-secondary" rel="noopener" aria-label="LinkedIn">
|
||||
@@ -200,18 +200,20 @@ Blocks:
|
||||
|
||||
{# Community links #}
|
||||
{% else %}
|
||||
{# GitHub #}
|
||||
<li class="list-inline-item">
|
||||
<a href="https://github.com/netbox-community/netbox" target="_blank" class="link-secondary" rel="noopener" aria-label="{% trans "Source Code" %}">
|
||||
<i title="{% trans "Source Code" %}" class="mdi mdi-github text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
||||
</a>
|
||||
</li>
|
||||
{# NetDev Slack #}
|
||||
<li class="list-inline-item">
|
||||
<a href="https://netdev.chat" target="_blank" class="link-secondary" rel="noopener" aria-label="{% trans "Community" %}">
|
||||
<i title="{% trans "Community" %}" class="mdi mdi-slack text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
||||
</a>
|
||||
</li>
|
||||
{% if not settings.ISOLATED_DEPLOYMENT %}
|
||||
{# GitHub #}
|
||||
<li class="list-inline-item">
|
||||
<a href="https://github.com/netbox-community/netbox" target="_blank" class="link-secondary" rel="noopener" aria-label="{% trans "Source Code" %}">
|
||||
<i title="{% trans "Source Code" %}" class="mdi mdi-github text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
||||
</a>
|
||||
</li>
|
||||
{# NetDev Slack #}
|
||||
<li class="list-inline-item">
|
||||
<a href="https://netdev.chat" target="_blank" class="link-secondary" rel="noopener" aria-label="{% trans "Community" %}">
|
||||
<i title="{% trans "Community" %}" class="mdi mdi-slack text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock footer_links %}
|
||||
</ul>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -98,6 +98,7 @@ class TenantTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'group': tenant_groups[1].pk,
|
||||
'description': 'Bulk edit description',
|
||||
}
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user