mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop' into develop-2.7
This commit is contained in:
23
.github/lock.yml
vendored
Normal file
23
.github/lock.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
# Configuration for Lock (https://github.com/apps/lock)
|
||||
|
||||
# Number of days of inactivity before a closed issue or pull request is locked
|
||||
daysUntilLock: 90
|
||||
|
||||
# Skip issues and pull requests created before a given timestamp. Timestamp must
|
||||
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
|
||||
skipCreatedBefore: 2020-01-01
|
||||
|
||||
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
|
||||
exemptLabels: []
|
||||
|
||||
# Label to add before locking, such as `outdated`. Set to `false` to disable
|
||||
lockLabel: false
|
||||
|
||||
# Comment to post before locking. Set to `false` to disable
|
||||
lockComment: false
|
||||
|
||||
# Assign `resolved` as the reason for locking. Set to `false` to disable
|
||||
setLockReason: true
|
||||
|
||||
# Limit to only `issues` or `pulls`
|
||||
# only: issues
|
7
.github/stale.yml
vendored
7
.github/stale.yml
vendored
@ -1,20 +1,27 @@
|
||||
# Configuration for Stale (https://github.com/apps/stale)
|
||||
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 14
|
||||
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- "status: accepted"
|
||||
- "status: gathering feedback"
|
||||
- "status: blocked"
|
||||
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: wontfix
|
||||
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. NetBox
|
||||
is governed by a small group of core maintainers which means not all opened
|
||||
issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
|
||||
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: >
|
||||
This issue has been automatically closed due to lack of activity. In an
|
||||
|
@ -69,6 +69,14 @@ If the new field will be included in the object list view, add a column to the m
|
||||
|
||||
Edit the object's view template to display the new field. There may also be a custom add/edit form template that needs to be updated.
|
||||
|
||||
### 11. Adjust API and model tests
|
||||
### 11. Create/extend test cases
|
||||
|
||||
Extend the model and/or API tests to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields.
|
||||
Create or extend the relevant test cases to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields. NetBox incorporates various test suites, including:
|
||||
|
||||
* API serializer/view tests
|
||||
* Filter tests
|
||||
* Form tests
|
||||
* Model tests
|
||||
* View tests
|
||||
|
||||
Be diligent to ensure all of the relevant test suites are adapted or extended as necessary to test any new functionality.
|
||||
|
@ -6,9 +6,13 @@
|
||||
* [#2050](https://github.com/netbox-community/netbox/issues/2050) - Preview image attachments when hovering the link
|
||||
* [#2113](https://github.com/netbox-community/netbox/issues/2113) - Allow NAPALM driver settings to be changed with request headers
|
||||
* [#2589](https://github.com/netbox-community/netbox/issues/2589) - Toggle for showing available prefixes/ip addresses
|
||||
* [#3009](https://github.com/netbox-community/netbox/issues/3009) - Search by description when assigning IP address
|
||||
* [#3090](https://github.com/netbox-community/netbox/issues/3090) - Add filter field for device interfaces
|
||||
* [#3187](https://github.com/netbox-community/netbox/issues/3187) - Add rack selection field to rack elevations
|
||||
* [#3393](https://github.com/netbox-community/netbox/issues/3393) - Paginate the circuits at the provider details view
|
||||
* [#3440](https://github.com/netbox-community/netbox/issues/3440) - Add total length to cable trace
|
||||
* [#3623](https://github.com/netbox-community/netbox/issues/3623) - Add word expansion during interface creation
|
||||
* [#3668](https://github.com/netbox-community/netbox/issues/3668) - Search by DNS name when assigning IP address
|
||||
* [#3851](https://github.com/netbox-community/netbox/issues/3851) - Allow passing initial data to custom script forms
|
||||
|
||||
## Bug Fixes
|
||||
@ -21,6 +25,8 @@
|
||||
* [#3862](https://github.com/netbox-community/netbox/issues/3862) - Allow filtering device components by multiple device names
|
||||
* [#3864](https://github.com/netbox-community/netbox/issues/3864) - Disallow /0 masks
|
||||
* [#3872](https://github.com/netbox-community/netbox/issues/3872) - Paginate related IPs of an address
|
||||
* [#3876](https://github.com/netbox-community/netbox/issues/3876) - Fixed min/max to ASN input field at the site creation page
|
||||
* [#3882](https://github.com/netbox-community/netbox/issues/3882) - Fix filtering of devices by rack group
|
||||
|
||||
---
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
@ -5,9 +6,11 @@ from django.db import transaction
|
||||
from django.db.models import Count, OuterRef, Subquery
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.views.generic import View
|
||||
from django_tables2 import RequestConfig
|
||||
|
||||
from extras.models import Graph
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.views import (
|
||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
@ -38,9 +41,18 @@ class ProviderView(PermissionRequiredMixin, View):
|
||||
circuits = Circuit.objects.filter(provider=provider).prefetch_related('type', 'tenant', 'terminations__site')
|
||||
show_graphs = Graph.objects.filter(type__model='provider').exists()
|
||||
|
||||
circuits_table = tables.CircuitTable(circuits, orderable=False)
|
||||
circuits_table.columns.hide('provider')
|
||||
|
||||
paginate = {
|
||||
'paginator_class': EnhancedPaginator,
|
||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(circuits_table)
|
||||
|
||||
return render(request, 'circuits/provider.html', {
|
||||
'provider': provider,
|
||||
'circuits': circuits,
|
||||
'circuits_table': circuits_table,
|
||||
'show_graphs': show_graphs,
|
||||
})
|
||||
|
||||
|
@ -1,5 +1,8 @@
|
||||
from .choices import InterfaceTypeChoices
|
||||
|
||||
# BGP ASN bounds
|
||||
BGP_ASN_MIN = 1
|
||||
BGP_ASN_MAX = 2**32 - 1
|
||||
|
||||
#
|
||||
# Interface type groups
|
||||
|
@ -3,14 +3,21 @@ from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.db import models
|
||||
from netaddr import AddrFormatError, EUI, mac_unix_expanded
|
||||
|
||||
from .constants import *
|
||||
|
||||
|
||||
class ASNField(models.BigIntegerField):
|
||||
description = "32-bit ASN field"
|
||||
default_validators = [
|
||||
MinValueValidator(1),
|
||||
MaxValueValidator(4294967295),
|
||||
MinValueValidator(BGP_ASN_MIN),
|
||||
MaxValueValidator(BGP_ASN_MAX),
|
||||
]
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'min_value': BGP_ASN_MIN, 'max_value': BGP_ASN_MAX}
|
||||
defaults.update(**kwargs)
|
||||
return super().formfield(**defaults)
|
||||
|
||||
|
||||
class mac_unix_expanded_uppercase(mac_unix_expanded):
|
||||
word_fmt = '%.2X'
|
||||
|
@ -320,8 +320,8 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
||||
)
|
||||
)
|
||||
asn = forms.IntegerField(
|
||||
min_value=1,
|
||||
max_value=4294967295,
|
||||
min_value=BGP_ASN_MIN,
|
||||
max_value=BGP_ASN_MAX,
|
||||
required=False,
|
||||
label='ASN'
|
||||
)
|
||||
@ -764,7 +764,7 @@ class RackElevationFilterForm(RackFilterForm):
|
||||
|
||||
# Filter the rack field based on the site and group
|
||||
self.fields['site'].widget.add_filter_for('id', 'site')
|
||||
self.fields['group_id'].widget.add_filter_for('id', 'group_id')
|
||||
self.fields['rack_group_id'].widget.add_filter_for('id', 'group_id')
|
||||
|
||||
|
||||
#
|
||||
@ -1936,7 +1936,7 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
||||
class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldFilterForm):
|
||||
model = Device
|
||||
field_order = [
|
||||
'q', 'region', 'site', 'group_id', 'rack_id', 'status', 'role', 'tenant_group', 'tenant',
|
||||
'q', 'region', 'site', 'rack_group_id', 'rack_id', 'status', 'role', 'tenant_group', 'tenant',
|
||||
'manufacturer_id', 'device_type_id', 'mac_address', 'has_primary_ip',
|
||||
]
|
||||
q = forms.CharField(
|
||||
@ -1962,12 +1962,12 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
filter_for={
|
||||
'group_id': 'site',
|
||||
'rack_group_id': 'site',
|
||||
'rack_id': 'site',
|
||||
}
|
||||
)
|
||||
)
|
||||
group_id = FilterChoiceField(
|
||||
rack_group_id = FilterChoiceField(
|
||||
queryset=RackGroup.objects.prefetch_related(
|
||||
'site'
|
||||
),
|
||||
|
@ -933,7 +933,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
|
||||
|
||||
|
||||
class IPAddressAssignForm(BootstrapMixin, forms.Form):
|
||||
vrf = forms.ModelChoiceField(
|
||||
vrf_id = forms.ModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF',
|
||||
@ -942,8 +942,9 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form):
|
||||
api_url="/api/ipam/vrfs/"
|
||||
)
|
||||
)
|
||||
address = forms.CharField(
|
||||
label='IP Address'
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search',
|
||||
)
|
||||
|
||||
|
||||
|
@ -377,7 +377,7 @@ class IPAddressAssignTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
fields = ('address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'description')
|
||||
fields = ('address', 'dns_name', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'description')
|
||||
orderable = False
|
||||
|
||||
|
||||
|
@ -756,13 +756,12 @@ class IPAddressAssignView(PermissionRequiredMixin, View):
|
||||
|
||||
if form.is_valid():
|
||||
|
||||
queryset = IPAddress.objects.prefetch_related(
|
||||
addresses = IPAddress.objects.prefetch_related(
|
||||
'vrf', 'tenant', 'interface__device', 'interface__virtual_machine'
|
||||
).filter(
|
||||
vrf=form.cleaned_data['vrf'],
|
||||
address__istartswith=form.cleaned_data['address'],
|
||||
)[:100] # Limit to 100 results
|
||||
table = tables.IPAddressAssignTable(queryset)
|
||||
)
|
||||
# Limit to 100 results
|
||||
addresses = filters.IPAddressFilter(request.POST, addresses).qs[:100]
|
||||
table = tables.IPAddressAssignTable(addresses)
|
||||
|
||||
return render(request, 'ipam/ipaddress_assign.html', {
|
||||
'form': form,
|
||||
|
@ -122,58 +122,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Circuits</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<tr>
|
||||
<th>Circuit ID</th>
|
||||
<th>Type</th>
|
||||
<th>Tenant</th>
|
||||
<th>A Side</th>
|
||||
<th>Z Side</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
{% for c in circuits %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'circuits:circuit' pk=c.pk %}">{{ c.cid }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'circuits:circuit_list' %}?type={{ c.type.slug }}">{{ c.type }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if c.tenant %}
|
||||
<a href="{% url 'tenancy:tenant' slug=c.tenant.slug %}">{{ c.tenant }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if c.termination_a %}
|
||||
<a href="{% url 'dcim:site' slug=c.termination_a.site.slug %}">{{ c.termination_a.site }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if c.termination_z %}
|
||||
<a href="{% url 'dcim:site' slug=c.termination_z.site.slug %}">{{ c.termination_z.site }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if c.description %}
|
||||
{{ c.description }}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-muted">None</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% include 'inc/table.html' with table=circuits_table %}
|
||||
{% if perms.circuits.add_circuit %}
|
||||
<div class="panel-footer text-right noprint">
|
||||
<a href="{% url 'circuits:circuit_add' %}?provider={{ provider.pk }}" class="btn btn-xs btn-primary">
|
||||
@ -182,6 +131,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/modal.html' with modal_name='graphs' %}
|
||||
|
@ -24,8 +24,8 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Select IP Address</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.vrf %}
|
||||
{% render_field form.address %}
|
||||
{% render_field form.vrf_id %}
|
||||
{% render_field form.q %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -61,6 +61,14 @@ def parse_alphanumeric_range(string):
|
||||
for n in list(range(int(begin), int(end) + 1)):
|
||||
values.append(n)
|
||||
else:
|
||||
# Value-based
|
||||
if begin == end:
|
||||
values.append(begin)
|
||||
# Range-based
|
||||
else:
|
||||
# Not a valid range (more than a single character)
|
||||
if not len(begin) == len(end) == 1:
|
||||
raise forms.ValidationError('Range "{}" is invalid.'.format(dash_range))
|
||||
for n in list(range(ord(begin), ord(end) + 1)):
|
||||
values.append(chr(n))
|
||||
return values
|
||||
@ -482,6 +490,7 @@ class ExpandableNameField(forms.CharField):
|
||||
'Mixed cases and types within a single range are not supported.<br />' \
|
||||
'Examples:<ul><li><code>ge-0/0/[0-23,25,30]</code></li>' \
|
||||
'<li><code>e[0-3][a-d,f]</code></li>' \
|
||||
'<li><code>[xe,ge]-0/0/0</code></li>' \
|
||||
'<li><code>e[0-3,a-d,f]</code></li></ul>'
|
||||
|
||||
def to_python(self, value):
|
||||
|
283
netbox/utilities/tests/test_forms.py
Normal file
283
netbox/utilities/tests/test_forms.py
Normal file
@ -0,0 +1,283 @@
|
||||
from django import forms
|
||||
from django.test import TestCase
|
||||
|
||||
from utilities.forms import *
|
||||
|
||||
|
||||
class ExpandIPAddress(TestCase):
|
||||
"""
|
||||
Validate the operation of expand_ipaddress_pattern().
|
||||
"""
|
||||
def test_ipv4_range(self):
|
||||
input = '1.2.3.[9-10]/32'
|
||||
output = sorted([
|
||||
'1.2.3.9/32',
|
||||
'1.2.3.10/32',
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output)
|
||||
|
||||
def test_ipv4_set(self):
|
||||
input = '1.2.3.[4,44]/32'
|
||||
output = sorted([
|
||||
'1.2.3.4/32',
|
||||
'1.2.3.44/32',
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output)
|
||||
|
||||
def test_ipv4_multiple_ranges(self):
|
||||
input = '1.[9-10].3.[9-11]/32'
|
||||
output = sorted([
|
||||
'1.9.3.9/32',
|
||||
'1.9.3.10/32',
|
||||
'1.9.3.11/32',
|
||||
'1.10.3.9/32',
|
||||
'1.10.3.10/32',
|
||||
'1.10.3.11/32',
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output)
|
||||
|
||||
def test_ipv4_multiple_sets(self):
|
||||
input = '1.[2,22].3.[4,44]/32'
|
||||
output = sorted([
|
||||
'1.2.3.4/32',
|
||||
'1.2.3.44/32',
|
||||
'1.22.3.4/32',
|
||||
'1.22.3.44/32',
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output)
|
||||
|
||||
def test_ipv4_set_and_range(self):
|
||||
input = '1.[2,22].3.[9-11]/32'
|
||||
output = sorted([
|
||||
'1.2.3.9/32',
|
||||
'1.2.3.10/32',
|
||||
'1.2.3.11/32',
|
||||
'1.22.3.9/32',
|
||||
'1.22.3.10/32',
|
||||
'1.22.3.11/32',
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output)
|
||||
|
||||
def test_ipv6_range(self):
|
||||
input = 'fec::abcd:[9-b]/64'
|
||||
output = sorted([
|
||||
'fec::abcd:9/64',
|
||||
'fec::abcd:a/64',
|
||||
'fec::abcd:b/64',
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output)
|
||||
|
||||
def test_ipv6_range_multichar_field(self):
|
||||
input = 'fec::abcd:[f-11]/64'
|
||||
output = sorted([
|
||||
'fec::abcd:f/64',
|
||||
'fec::abcd:10/64',
|
||||
'fec::abcd:11/64',
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output)
|
||||
|
||||
def test_ipv6_set(self):
|
||||
input = 'fec::abcd:[9,ab]/64'
|
||||
output = sorted([
|
||||
'fec::abcd:9/64',
|
||||
'fec::abcd:ab/64',
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output)
|
||||
|
||||
def test_ipv6_multiple_ranges(self):
|
||||
input = 'fec::[1-2]bcd:[9-b]/64'
|
||||
output = sorted([
|
||||
'fec::1bcd:9/64',
|
||||
'fec::1bcd:a/64',
|
||||
'fec::1bcd:b/64',
|
||||
'fec::2bcd:9/64',
|
||||
'fec::2bcd:a/64',
|
||||
'fec::2bcd:b/64',
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output)
|
||||
|
||||
def test_ipv6_multiple_sets(self):
|
||||
input = 'fec::[a,f]bcd:[9,ab]/64'
|
||||
output = sorted([
|
||||
'fec::abcd:9/64',
|
||||
'fec::abcd:ab/64',
|
||||
'fec::fbcd:9/64',
|
||||
'fec::fbcd:ab/64',
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output)
|
||||
|
||||
def test_ipv6_set_and_range(self):
|
||||
input = 'fec::[dead,beaf]:[9-b]/64'
|
||||
output = sorted([
|
||||
'fec::dead:9/64',
|
||||
'fec::dead:a/64',
|
||||
'fec::dead:b/64',
|
||||
'fec::beaf:9/64',
|
||||
'fec::beaf:a/64',
|
||||
'fec::beaf:b/64',
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output)
|
||||
|
||||
def test_invalid_address_family(self):
|
||||
with self.assertRaisesRegex(Exception, 'Invalid IP address family: 5'):
|
||||
sorted(expand_ipaddress_pattern(None, 5))
|
||||
|
||||
def test_invalid_non_pattern(self):
|
||||
with self.assertRaises(ValueError):
|
||||
sorted(expand_ipaddress_pattern('1.2.3.4/32', 4))
|
||||
|
||||
def test_invalid_range(self):
|
||||
with self.assertRaises(ValueError):
|
||||
sorted(expand_ipaddress_pattern('1.2.3.[4-]/32', 4))
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
sorted(expand_ipaddress_pattern('1.2.3.[-4]/32', 4))
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
sorted(expand_ipaddress_pattern('1.2.3.[4--5]/32', 4))
|
||||
|
||||
def test_invalid_range_bounds(self):
|
||||
self.assertEqual(sorted(expand_ipaddress_pattern('1.2.3.[4-3]/32', 6)), [])
|
||||
|
||||
def test_invalid_set(self):
|
||||
with self.assertRaises(ValueError):
|
||||
sorted(expand_ipaddress_pattern('1.2.3.[4]/32', 4))
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
sorted(expand_ipaddress_pattern('1.2.3.[4,]/32', 4))
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
sorted(expand_ipaddress_pattern('1.2.3.[,4]/32', 4))
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
sorted(expand_ipaddress_pattern('1.2.3.[4,,5]/32', 4))
|
||||
|
||||
|
||||
class ExpandAlphanumeric(TestCase):
|
||||
"""
|
||||
Validate the operation of expand_alphanumeric_pattern().
|
||||
"""
|
||||
def test_range_numberic(self):
|
||||
input = 'r[9-11]a'
|
||||
output = sorted([
|
||||
'r9a',
|
||||
'r10a',
|
||||
'r11a',
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
|
||||
|
||||
def test_range_alpha(self):
|
||||
input = '[r-t]1a'
|
||||
output = sorted([
|
||||
'r1a',
|
||||
's1a',
|
||||
't1a',
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
|
||||
|
||||
def test_set(self):
|
||||
input = '[r,t]1a'
|
||||
output = sorted([
|
||||
'r1a',
|
||||
't1a',
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
|
||||
|
||||
def test_set_multichar(self):
|
||||
input = '[ra,tb]1a'
|
||||
output = sorted([
|
||||
'ra1a',
|
||||
'tb1a',
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
|
||||
|
||||
def test_multiple_ranges(self):
|
||||
input = '[r-t]1[a-b]'
|
||||
output = sorted([
|
||||
'r1a',
|
||||
'r1b',
|
||||
's1a',
|
||||
's1b',
|
||||
't1a',
|
||||
't1b',
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
|
||||
|
||||
def test_multiple_sets(self):
|
||||
input = '[ra,tb]1[ax,by]'
|
||||
output = sorted([
|
||||
'ra1ax',
|
||||
'ra1by',
|
||||
'tb1ax',
|
||||
'tb1by',
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
|
||||
|
||||
def test_set_and_range(self):
|
||||
input = '[ra,tb]1[a-c]'
|
||||
output = sorted([
|
||||
'ra1a',
|
||||
'ra1b',
|
||||
'ra1c',
|
||||
'tb1a',
|
||||
'tb1b',
|
||||
'tb1c',
|
||||
])
|
||||
|
||||
self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
|
||||
|
||||
def test_invalid_non_pattern(self):
|
||||
with self.assertRaises(ValueError):
|
||||
sorted(expand_alphanumeric_pattern('r9a'))
|
||||
|
||||
def test_invalid_range(self):
|
||||
with self.assertRaises(ValueError):
|
||||
sorted(expand_alphanumeric_pattern('r[8-]a'))
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
sorted(expand_alphanumeric_pattern('r[-8]a'))
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
sorted(expand_alphanumeric_pattern('r[8--9]a'))
|
||||
|
||||
def test_invalid_range_alphanumeric(self):
|
||||
self.assertEqual(sorted(expand_alphanumeric_pattern('r[9-a]a')), [])
|
||||
self.assertEqual(sorted(expand_alphanumeric_pattern('r[a-9]a')), [])
|
||||
|
||||
def test_invalid_range_bounds(self):
|
||||
self.assertEqual(sorted(expand_alphanumeric_pattern('r[9-8]a')), [])
|
||||
self.assertEqual(sorted(expand_alphanumeric_pattern('r[b-a]a')), [])
|
||||
|
||||
def test_invalid_range_len(self):
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
sorted(expand_alphanumeric_pattern('r[a-bb]a'))
|
||||
|
||||
def test_invalid_set(self):
|
||||
with self.assertRaises(ValueError):
|
||||
sorted(expand_alphanumeric_pattern('r[a]a'))
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
sorted(expand_alphanumeric_pattern('r[a,]a'))
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
sorted(expand_alphanumeric_pattern('r[,a]a'))
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
sorted(expand_alphanumeric_pattern('r[a,,b]a'))
|
Reference in New Issue
Block a user