mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 04:02:52 -06:00
merge develop into issue
This commit is contained in:
commit
ef6576bdd6
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.1.8
|
placeholder: v3.1.9
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.1.8
|
placeholder: v3.1.9
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
|
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
:loudspeaker: The **[2022 NetBox community survey](https://forms.gle/KR8YbR8GiJ9EYXM28)** is now open! We collect this feedback and demographic data from NetBox users around the world to help shape the project's long-term development goals. Please take a few minutes to share your responses!
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
NetBox is an infrastructure resource modeling (IRM) tool designed to empower
|
NetBox is an infrastructure resource modeling (IRM) tool designed to empower
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{style="height: 100px; margin-bottom: 3em"}
|
{style="height: 100px; margin-bottom: 3em"}
|
||||||
|
|
||||||
|
:loudspeaker: The **[2022 NetBox community survey](https://forms.gle/KR8YbR8GiJ9EYXM28)** is now open! We collect this feedback and demographic data from NetBox users around the world to help shape the project's long-term development goals. Please take a few minutes to share your responses!
|
||||||
|
|
||||||
# What is NetBox?
|
# What is NetBox?
|
||||||
|
|
||||||
NetBox is an infrastructure resource modeling (IRM) application designed to empower network automation. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers. NetBox is made available as open source under the Apache 2 license. It encompasses the following aspects of network management:
|
NetBox is an infrastructure resource modeling (IRM) application designed to empower network automation. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers. NetBox is made available as open source under the Apache 2 license. It encompasses the following aspects of network management:
|
||||||
|
@ -1,13 +1,35 @@
|
|||||||
# NetBox v3.1
|
# NetBox v3.1
|
||||||
|
|
||||||
## v3.1.9 (FUTURE)
|
## v3.1.10 (FUTURE)
|
||||||
|
|
||||||
|
* [#8820](https://github.com/netbox-community/netbox/issues/8820) - Fix navbar background color in dark mode
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.1.9 (2022-03-07)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#8594](https://github.com/netbox-community/netbox/issues/8594) - Enable filtering by exact description match for all applicable models
|
||||||
|
* [#8629](https://github.com/netbox-community/netbox/issues/8629) - Add description to tag table search function
|
||||||
|
* [#8664](https://github.com/netbox-community/netbox/issues/8664) - Show assigned ASNs/sites under list views
|
||||||
|
* [#8736](https://github.com/netbox-community/netbox/issues/8736) - Add PC and UPC fiber end faces for LC/SC/LSH port types
|
||||||
|
* [#8758](https://github.com/netbox-community/netbox/issues/8758) - Allow empty string substitution when renaming objects in bulk
|
||||||
|
* [#8762](https://github.com/netbox-community/netbox/issues/8762) - Link to rack elevations list from site view
|
||||||
|
* [#8766](https://github.com/netbox-community/netbox/issues/8766) - Add SCTP to service protocols list
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* [#8546](https://github.com/netbox-community/netbox/issues/8546) - Fix bulk import to restrict bridge, parent, and LAG to device interfaces
|
* [#8546](https://github.com/netbox-community/netbox/issues/8546) - Fix bulk import to restrict bridge, parent, and LAG to device interfaces
|
||||||
|
* [#8633](https://github.com/netbox-community/netbox/issues/8633) - Prevent navigation sidebar pin from disappearing at certain breakpoints
|
||||||
* [#8674](https://github.com/netbox-community/netbox/issues/8674) - Fix rendering of tabbed content in documentation
|
* [#8674](https://github.com/netbox-community/netbox/issues/8674) - Fix rendering of tabbed content in documentation
|
||||||
|
* [#8710](https://github.com/netbox-community/netbox/issues/8710) - Fix dynamic scope selection form fields when creating a VLAN group
|
||||||
* [#8713](https://github.com/netbox-community/netbox/issues/8713) - Restore missing "add" button on services list view
|
* [#8713](https://github.com/netbox-community/netbox/issues/8713) - Restore missing "add" button on services list view
|
||||||
|
* [#8715](https://github.com/netbox-community/netbox/issues/8715) - Avoid returning multiple objects when restricting querysets using multiple tags in permissions
|
||||||
* [#8717](https://github.com/netbox-community/netbox/issues/8717) - Fix redirection after bulk edit/delete of prefixes from aggregate view
|
* [#8717](https://github.com/netbox-community/netbox/issues/8717) - Fix redirection after bulk edit/delete of prefixes from aggregate view
|
||||||
|
* [#8724](https://github.com/netbox-community/netbox/issues/8724) - Fix exception during device import with invalid device type
|
||||||
|
* [#8807](https://github.com/netbox-community/netbox/issues/8807) - Correct REST API URL for FHRP group assignments
|
||||||
|
* [#8808](https://github.com/netbox-community/netbox/issues/8808) - Fix members count under FHRP group list
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -8,11 +8,13 @@ theme:
|
|||||||
icon:
|
icon:
|
||||||
repo: fontawesome/brands/github
|
repo: fontawesome/brands/github
|
||||||
palette:
|
palette:
|
||||||
- scheme: default
|
- media: "(prefers-color-scheme: light)"
|
||||||
|
scheme: default
|
||||||
toggle:
|
toggle:
|
||||||
icon: material/lightbulb-outline
|
icon: material/lightbulb-outline
|
||||||
name: Switch to Dark Mode
|
name: Switch to Dark Mode
|
||||||
- scheme: slate
|
- media: "(prefers-color-scheme: dark)"
|
||||||
|
scheme: slate
|
||||||
toggle:
|
toggle:
|
||||||
icon: material/lightbulb
|
icon: material/lightbulb
|
||||||
name: Switch to Light Mode
|
name: Switch to Light Mode
|
||||||
|
@ -98,7 +98,7 @@ class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProviderNetwork
|
model = ProviderNetwork
|
||||||
fields = ['id', 'name']
|
fields = ['id', 'name', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -115,7 +115,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fields = ['id', 'name', 'slug']
|
fields = ['id', 'name', 'slug', 'description']
|
||||||
|
|
||||||
|
|
||||||
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||||
@ -193,7 +193,7 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ['id', 'cid', 'install_date', 'commit_rate']
|
fields = ['id', 'cid', 'description', 'install_date', 'commit_rate']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -234,7 +234,7 @@ class CircuitTerminationFilterSet(ChangeLoggedModelFilterSet, CableTerminationFi
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id']
|
fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -108,8 +108,8 @@ class CircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
CircuitType.objects.bulk_create((
|
CircuitType.objects.bulk_create((
|
||||||
CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
|
CircuitType(name='Circuit Type 1', slug='circuit-type-1', description='foobar1'),
|
||||||
CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
|
CircuitType(name='Circuit Type 2', slug='circuit-type-2', description='foobar2'),
|
||||||
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
|
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
|
||||||
))
|
))
|
||||||
|
|
||||||
@ -121,6 +121,10 @@ class CircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'slug': ['circuit-type-1']}
|
params = {'slug': ['circuit-type-1']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = Circuit.objects.all()
|
queryset = Circuit.objects.all()
|
||||||
@ -187,8 +191,8 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||||
|
|
||||||
circuits = (
|
circuits = (
|
||||||
Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE),
|
Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1'),
|
||||||
Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE),
|
Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2'),
|
||||||
Circuit(provider=providers[0], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED),
|
Circuit(provider=providers[0], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED),
|
||||||
Circuit(provider=providers[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED),
|
Circuit(provider=providers[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED),
|
||||||
Circuit(provider=providers[1], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE),
|
Circuit(provider=providers[1], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE),
|
||||||
@ -241,6 +245,10 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
|
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_region(self):
|
def test_region(self):
|
||||||
regions = Region.objects.all()[:2]
|
regions = Region.objects.all()[:2]
|
||||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||||
@ -319,8 +327,8 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Circuit.objects.bulk_create(circuits)
|
Circuit.objects.bulk_create(circuits)
|
||||||
|
|
||||||
circuit_terminations = ((
|
circuit_terminations = ((
|
||||||
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000, upstream_speed=1000, xconnect_id='ABC'),
|
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000, upstream_speed=1000, xconnect_id='ABC', description='foobar1'),
|
||||||
CircuitTermination(circuit=circuits[0], site=sites[1], term_side='Z', port_speed=1000, upstream_speed=1000, xconnect_id='DEF'),
|
CircuitTermination(circuit=circuits[0], site=sites[1], term_side='Z', port_speed=1000, upstream_speed=1000, xconnect_id='DEF', description='foobar2'),
|
||||||
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A', port_speed=2000, upstream_speed=2000, xconnect_id='GHI'),
|
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A', port_speed=2000, upstream_speed=2000, xconnect_id='GHI'),
|
||||||
CircuitTermination(circuit=circuits[1], site=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'),
|
CircuitTermination(circuit=circuits[1], site=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'),
|
||||||
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'),
|
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'),
|
||||||
@ -349,6 +357,10 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'xconnect_id': ['ABC', 'DEF']}
|
params = {'xconnect_id': ['ABC', 'DEF']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_circuit_id(self):
|
def test_circuit_id(self):
|
||||||
circuits = Circuit.objects.all()[:2]
|
circuits = Circuit.objects.all()[:2]
|
||||||
params = {'circuit_id': [circuits[0].pk, circuits[1].pk]}
|
params = {'circuit_id': [circuits[0].pk, circuits[1].pk]}
|
||||||
@ -386,8 +398,8 @@ class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Provider.objects.bulk_create(providers)
|
Provider.objects.bulk_create(providers)
|
||||||
|
|
||||||
provider_networks = (
|
provider_networks = (
|
||||||
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
|
ProviderNetwork(name='Provider Network 1', provider=providers[0], description='foobar1'),
|
||||||
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
|
ProviderNetwork(name='Provider Network 2', provider=providers[1], description='foobar2'),
|
||||||
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
|
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
|
||||||
)
|
)
|
||||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||||
@ -396,6 +408,10 @@ class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'name': ['Provider Network 1', 'Provider Network 2']}
|
params = {'name': ['Provider Network 1', 'Provider Network 2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_provider(self):
|
def test_provider(self):
|
||||||
providers = Provider.objects.all()[:2]
|
providers = Provider.objects.all()[:2]
|
||||||
params = {'provider_id': [providers[0].pk, providers[1].pk]}
|
params = {'provider_id': [providers[0].pk, providers[1].pk]}
|
||||||
|
@ -1013,13 +1013,19 @@ class PortTypeChoices(ChoiceSet):
|
|||||||
TYPE_MRJ21 = 'mrj21'
|
TYPE_MRJ21 = 'mrj21'
|
||||||
TYPE_ST = 'st'
|
TYPE_ST = 'st'
|
||||||
TYPE_SC = 'sc'
|
TYPE_SC = 'sc'
|
||||||
|
TYPE_SC_PC = 'sc-pc'
|
||||||
|
TYPE_SC_UPC = 'sc-upc'
|
||||||
TYPE_SC_APC = 'sc-apc'
|
TYPE_SC_APC = 'sc-apc'
|
||||||
TYPE_FC = 'fc'
|
TYPE_FC = 'fc'
|
||||||
TYPE_LC = 'lc'
|
TYPE_LC = 'lc'
|
||||||
|
TYPE_LC_PC = 'lc-pc'
|
||||||
|
TYPE_LC_UPC = 'lc-upc'
|
||||||
TYPE_LC_APC = 'lc-apc'
|
TYPE_LC_APC = 'lc-apc'
|
||||||
TYPE_MTRJ = 'mtrj'
|
TYPE_MTRJ = 'mtrj'
|
||||||
TYPE_MPO = 'mpo'
|
TYPE_MPO = 'mpo'
|
||||||
TYPE_LSH = 'lsh'
|
TYPE_LSH = 'lsh'
|
||||||
|
TYPE_LSH_PC = 'lsh-pc'
|
||||||
|
TYPE_LSH_UPC = 'lsh-upc'
|
||||||
TYPE_LSH_APC = 'lsh-apc'
|
TYPE_LSH_APC = 'lsh-apc'
|
||||||
TYPE_SPLICE = 'splice'
|
TYPE_SPLICE = 'splice'
|
||||||
TYPE_CS = 'cs'
|
TYPE_CS = 'cs'
|
||||||
@ -1059,12 +1065,18 @@ class PortTypeChoices(ChoiceSet):
|
|||||||
(
|
(
|
||||||
(TYPE_FC, 'FC'),
|
(TYPE_FC, 'FC'),
|
||||||
(TYPE_LC, 'LC'),
|
(TYPE_LC, 'LC'),
|
||||||
|
(TYPE_LC_PC, 'LC/PC'),
|
||||||
|
(TYPE_LC_UPC, 'LC/UPC'),
|
||||||
(TYPE_LC_APC, 'LC/APC'),
|
(TYPE_LC_APC, 'LC/APC'),
|
||||||
(TYPE_LSH, 'LSH'),
|
(TYPE_LSH, 'LSH'),
|
||||||
|
(TYPE_LSH_PC, 'LSH/PC'),
|
||||||
|
(TYPE_LSH_UPC, 'LSH/UPC'),
|
||||||
(TYPE_LSH_APC, 'LSH/APC'),
|
(TYPE_LSH_APC, 'LSH/APC'),
|
||||||
(TYPE_MPO, 'MPO'),
|
(TYPE_MPO, 'MPO'),
|
||||||
(TYPE_MTRJ, 'MTRJ'),
|
(TYPE_MTRJ, 'MTRJ'),
|
||||||
(TYPE_SC, 'SC'),
|
(TYPE_SC, 'SC'),
|
||||||
|
(TYPE_SC_PC, 'SC/PC'),
|
||||||
|
(TYPE_SC_UPC, 'SC/UPC'),
|
||||||
(TYPE_SC_APC, 'SC/APC'),
|
(TYPE_SC_APC, 'SC/APC'),
|
||||||
(TYPE_ST, 'ST'),
|
(TYPE_ST, 'ST'),
|
||||||
(TYPE_CS, 'CS'),
|
(TYPE_CS, 'CS'),
|
||||||
|
@ -142,7 +142,7 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterS
|
|||||||
model = Site
|
model = Site
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'slug', 'facility', 'asn', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
'id', 'name', 'slug', 'facility', 'asn', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||||
'contact_email',
|
'contact_email', 'description'
|
||||||
]
|
]
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
@ -237,7 +237,7 @@ class RackRoleFilterSet(OrganizationalModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackRole
|
model = RackRole
|
||||||
fields = ['id', 'name', 'slug', 'color']
|
fields = ['id', 'name', 'slug', 'color', 'description']
|
||||||
|
|
||||||
|
|
||||||
class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||||
@ -385,7 +385,7 @@ class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
fields = ['id', 'created']
|
fields = ['id', 'created', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -586,7 +586,7 @@ class DeviceRoleFilterSet(OrganizationalModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = ['id', 'name', 'slug', 'color', 'vm_role']
|
fields = ['id', 'name', 'slug', 'color', 'vm_role', 'description']
|
||||||
|
|
||||||
|
|
||||||
class PlatformFilterSet(OrganizationalModelFilterSet):
|
class PlatformFilterSet(OrganizationalModelFilterSet):
|
||||||
|
@ -670,6 +670,7 @@ class Device(PrimaryModel, ConfigContextModel):
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Prevent 0U devices from being assigned to a specific position
|
# Prevent 0U devices from being assigned to a specific position
|
||||||
|
if hasattr(self, 'device_type'):
|
||||||
if self.position and self.device_type.u_height == 0:
|
if self.position and self.device_type.u_height == 0:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'position': f"A U0 device type ({self.device_type}) cannot be assigned to a rack position."
|
'position': f"A U0 device type ({self.device_type}) cannot be assigned to a rack position."
|
||||||
|
@ -91,6 +91,10 @@ class SiteTable(BaseTable):
|
|||||||
accessor=tables.A('asns__count'),
|
accessor=tables.A('asns__count'),
|
||||||
viewname='ipam:asn_list',
|
viewname='ipam:asn_list',
|
||||||
url_params={'site_id': 'pk'},
|
url_params={'site_id': 'pk'},
|
||||||
|
verbose_name='ASN Count'
|
||||||
|
)
|
||||||
|
asns = tables.ManyToManyColumn(
|
||||||
|
linkify_item=True,
|
||||||
verbose_name='ASNs'
|
verbose_name='ASNs'
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
@ -105,9 +109,15 @@ class SiteTable(BaseTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Site
|
model = Site
|
||||||
fields = (
|
fields = (
|
||||||
|
<<<<<<< HEAD
|
||||||
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn_count', 'time_zone',
|
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn_count', 'time_zone',
|
||||||
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
|
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
|
||||||
'contact_phone', 'contact_email', 'contacts', 'comments', 'tags', 'created', 'last_updated',
|
'contact_phone', 'contact_email', 'contacts', 'comments', 'tags', 'created', 'last_updated',
|
||||||
|
=======
|
||||||
|
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asns', 'asn_count',
|
||||||
|
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
|
||||||
|
'contact_phone', 'contact_email', 'comments', 'tags', 'created', 'last_updated',
|
||||||
|
>>>>>>> develop
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description')
|
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description')
|
||||||
|
|
||||||
|
@ -151,8 +151,8 @@ class SiteTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
ASN.objects.bulk_create(asns)
|
ASN.objects.bulk_create(asns)
|
||||||
|
|
||||||
sites = (
|
sites = (
|
||||||
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0], tenant=tenants[0], status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', asn=65001, latitude=10, longitude=10, contact_name='Contact 1', contact_phone='123-555-0001', contact_email='contact1@example.com'),
|
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0], tenant=tenants[0], status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', asn=65001, latitude=10, longitude=10, contact_name='Contact 1', contact_phone='123-555-0001', contact_email='contact1@example.com', description='foobar1'),
|
||||||
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1], tenant=tenants[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', asn=65002, latitude=20, longitude=20, contact_name='Contact 2', contact_phone='123-555-0002', contact_email='contact2@example.com'),
|
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1], tenant=tenants[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', asn=65002, latitude=20, longitude=20, contact_name='Contact 2', contact_phone='123-555-0002', contact_email='contact2@example.com', description='foobar2'),
|
||||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2], tenant=tenants[2], status=SiteStatusChoices.STATUS_RETIRED, facility='Facility 3', asn=65003, latitude=30, longitude=30, contact_name='Contact 3', contact_phone='123-555-0003', contact_email='contact3@example.com'),
|
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2], tenant=tenants[2], status=SiteStatusChoices.STATUS_RETIRED, facility='Facility 3', asn=65003, latitude=30, longitude=30, contact_name='Contact 3', contact_phone='123-555-0003', contact_email='contact3@example.com'),
|
||||||
)
|
)
|
||||||
Site.objects.bulk_create(sites)
|
Site.objects.bulk_create(sites)
|
||||||
@ -201,6 +201,10 @@ class SiteTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'contact_email': ['contact1@example.com', 'contact2@example.com']}
|
params = {'contact_email': ['contact1@example.com', 'contact2@example.com']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_status(self):
|
def test_status(self):
|
||||||
params = {'status': [SiteStatusChoices.STATUS_ACTIVE, SiteStatusChoices.STATUS_PLANNED]}
|
params = {'status': [SiteStatusChoices.STATUS_ACTIVE, SiteStatusChoices.STATUS_PLANNED]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
@ -329,8 +333,8 @@ class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
rack_roles = (
|
rack_roles = (
|
||||||
RackRole(name='Rack Role 1', slug='rack-role-1', color='ff0000'),
|
RackRole(name='Rack Role 1', slug='rack-role-1', color='ff0000', description='foobar1'),
|
||||||
RackRole(name='Rack Role 2', slug='rack-role-2', color='00ff00'),
|
RackRole(name='Rack Role 2', slug='rack-role-2', color='00ff00', description='foobar2'),
|
||||||
RackRole(name='Rack Role 3', slug='rack-role-3', color='0000ff'),
|
RackRole(name='Rack Role 3', slug='rack-role-3', color='0000ff'),
|
||||||
)
|
)
|
||||||
RackRole.objects.bulk_create(rack_roles)
|
RackRole.objects.bulk_create(rack_roles)
|
||||||
@ -347,6 +351,10 @@ class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'color': ['ff0000', '00ff00']}
|
params = {'color': ['ff0000', '00ff00']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = Rack.objects.all()
|
queryset = Rack.objects.all()
|
||||||
@ -570,8 +578,8 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
reservations = (
|
reservations = (
|
||||||
RackReservation(rack=racks[0], units=[1, 2, 3], user=users[0], tenant=tenants[0]),
|
RackReservation(rack=racks[0], units=[1, 2, 3], user=users[0], tenant=tenants[0], description='foobar1'),
|
||||||
RackReservation(rack=racks[1], units=[4, 5, 6], user=users[1], tenant=tenants[1]),
|
RackReservation(rack=racks[1], units=[4, 5, 6], user=users[1], tenant=tenants[1], description='foobar2'),
|
||||||
RackReservation(rack=racks[2], units=[7, 8, 9], user=users[2], tenant=tenants[2]),
|
RackReservation(rack=racks[2], units=[7, 8, 9], user=users[2], tenant=tenants[2]),
|
||||||
)
|
)
|
||||||
RackReservation.objects.bulk_create(reservations)
|
RackReservation.objects.bulk_create(reservations)
|
||||||
@ -604,6 +612,10 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
|
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_tenant_group(self):
|
def test_tenant_group(self):
|
||||||
tenant_groups = TenantGroup.objects.all()[:2]
|
tenant_groups = TenantGroup.objects.all()[:2]
|
||||||
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
|
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
|
||||||
@ -1088,8 +1100,8 @@ class DeviceRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
device_roles = (
|
device_roles = (
|
||||||
DeviceRole(name='Device Role 1', slug='device-role-1', color='ff0000', vm_role=True),
|
DeviceRole(name='Device Role 1', slug='device-role-1', color='ff0000', vm_role=True, description='foobar1'),
|
||||||
DeviceRole(name='Device Role 2', slug='device-role-2', color='00ff00', vm_role=True),
|
DeviceRole(name='Device Role 2', slug='device-role-2', color='00ff00', vm_role=True, description='foobar2'),
|
||||||
DeviceRole(name='Device Role 3', slug='device-role-3', color='0000ff', vm_role=False),
|
DeviceRole(name='Device Role 3', slug='device-role-3', color='0000ff', vm_role=False),
|
||||||
)
|
)
|
||||||
DeviceRole.objects.bulk_create(device_roles)
|
DeviceRole.objects.bulk_create(device_roles)
|
||||||
@ -1112,6 +1124,10 @@ class DeviceRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'vm_role': 'false'}
|
params = {'vm_role': 'false'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = Platform.objects.all()
|
queryset = Platform.objects.all()
|
||||||
|
@ -62,7 +62,7 @@ class CustomFieldFilterSet(BaseFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight']
|
fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -103,7 +103,7 @@ class ExportTemplateFilterSet(BaseFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ExportTemplate
|
model = ExportTemplate
|
||||||
fields = ['id', 'content_type', 'name']
|
fields = ['id', 'content_type', 'name', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -177,14 +177,15 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tag
|
model = Tag
|
||||||
fields = ['id', 'name', 'slug', 'color']
|
fields = ['id', 'name', 'slug', 'color', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(name__icontains=value) |
|
Q(name__icontains=value) |
|
||||||
Q(slug__icontains=value)
|
Q(slug__icontains=value) |
|
||||||
|
Q(description__icontains=value)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _content_type(self, queryset, name, values):
|
def _content_type(self, queryset, name, values):
|
||||||
|
@ -153,8 +153,8 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
|
|||||||
content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
|
content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
|
||||||
|
|
||||||
export_templates = (
|
export_templates = (
|
||||||
ExportTemplate(name='Export Template 1', content_type=content_types[0], template_code='TESTING'),
|
ExportTemplate(name='Export Template 1', content_type=content_types[0], template_code='TESTING', description='foobar1'),
|
||||||
ExportTemplate(name='Export Template 2', content_type=content_types[1], template_code='TESTING'),
|
ExportTemplate(name='Export Template 2', content_type=content_types[1], template_code='TESTING', description='foobar2'),
|
||||||
ExportTemplate(name='Export Template 3', content_type=content_types[2], template_code='TESTING'),
|
ExportTemplate(name='Export Template 3', content_type=content_types[2], template_code='TESTING'),
|
||||||
)
|
)
|
||||||
ExportTemplate.objects.bulk_create(export_templates)
|
ExportTemplate.objects.bulk_create(export_templates)
|
||||||
@ -167,6 +167,10 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
|
|||||||
params = {'content_type': ContentType.objects.get(model='site').pk}
|
params = {'content_type': ContentType.objects.get(model='site').pk}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
|
class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
|
||||||
queryset = ImageAttachment.objects.all()
|
queryset = ImageAttachment.objects.all()
|
||||||
@ -542,8 +546,8 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
tags = (
|
tags = (
|
||||||
Tag(name='Tag 1', slug='tag-1', color='ff0000'),
|
Tag(name='Tag 1', slug='tag-1', color='ff0000', description='foobar1'),
|
||||||
Tag(name='Tag 2', slug='tag-2', color='00ff00'),
|
Tag(name='Tag 2', slug='tag-2', color='00ff00', description='foobar2'),
|
||||||
Tag(name='Tag 3', slug='tag-3', color='0000ff'),
|
Tag(name='Tag 3', slug='tag-3', color='0000ff'),
|
||||||
)
|
)
|
||||||
Tag.objects.bulk_create(tags)
|
Tag.objects.bulk_create(tags)
|
||||||
@ -567,6 +571,10 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'color': ['ff0000', '00ff00']}
|
params = {'color': ['ff0000', '00ff00']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_content_type(self):
|
def test_content_type(self):
|
||||||
params = {'content_type': ['dcim.site', 'circuits.provider']}
|
params = {'content_type': ['dcim.site', 'circuits.provider']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
@ -126,7 +126,7 @@ class FHRPGroupSerializer(PrimaryModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class FHRPGroupAssignmentSerializer(PrimaryModelSerializer):
|
class FHRPGroupAssignmentSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail')
|
||||||
group = NestedFHRPGroupSerializer()
|
group = NestedFHRPGroupSerializer()
|
||||||
interface_type = ContentTypeField(
|
interface_type = ContentTypeField(
|
||||||
queryset=ContentType.objects.all()
|
queryset=ContentType.objects.all()
|
||||||
|
@ -189,8 +189,10 @@ class ServiceProtocolChoices(ChoiceSet):
|
|||||||
|
|
||||||
PROTOCOL_TCP = 'tcp'
|
PROTOCOL_TCP = 'tcp'
|
||||||
PROTOCOL_UDP = 'udp'
|
PROTOCOL_UDP = 'udp'
|
||||||
|
PROTOCOL_SCTP = 'sctp'
|
||||||
|
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
(PROTOCOL_TCP, 'TCP'),
|
(PROTOCOL_TCP, 'TCP'),
|
||||||
(PROTOCOL_UDP, 'UDP'),
|
(PROTOCOL_UDP, 'UDP'),
|
||||||
|
(PROTOCOL_SCTP, 'SCTP'),
|
||||||
)
|
)
|
||||||
|
@ -75,7 +75,7 @@ class VRFFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VRF
|
model = VRF
|
||||||
fields = ['id', 'name', 'rd', 'enforce_unique']
|
fields = ['id', 'name', 'rd', 'enforce_unique', 'description']
|
||||||
|
|
||||||
|
|
||||||
class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||||
@ -117,7 +117,7 @@ class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RouteTarget
|
model = RouteTarget
|
||||||
fields = ['id', 'name']
|
fields = ['id', 'name', 'description']
|
||||||
|
|
||||||
|
|
||||||
class RIRFilterSet(OrganizationalModelFilterSet):
|
class RIRFilterSet(OrganizationalModelFilterSet):
|
||||||
@ -155,7 +155,7 @@ class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
fields = ['id', 'date_added']
|
fields = ['id', 'date_added', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -203,7 +203,7 @@ class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ASN
|
model = ASN
|
||||||
fields = ['id', 'asn']
|
fields = ['id', 'asn', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -225,7 +225,7 @@ class RoleFilterSet(OrganizationalModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Role
|
model = Role
|
||||||
fields = ['id', 'name', 'slug']
|
fields = ['id', 'name', 'slug', 'description']
|
||||||
|
|
||||||
|
|
||||||
class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||||
@ -354,7 +354,7 @@ class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ['id', 'is_pool', 'mark_utilized']
|
fields = ['id', 'is_pool', 'mark_utilized', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -460,7 +460,7 @@ class IPRangeFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPRange
|
model = IPRange
|
||||||
fields = ['id']
|
fields = ['id', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -839,7 +839,7 @@ class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = ['id', 'vid', 'name']
|
fields = ['id', 'vid', 'name', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -891,7 +891,7 @@ class ServiceFilterSet(PrimaryModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Service
|
model = Service
|
||||||
fields = ['id', 'name', 'protocol']
|
fields = ['id', 'name', 'protocol', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -27,8 +27,8 @@ class FHRPGroupTable(BaseTable):
|
|||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='IP Addresses'
|
verbose_name='IP Addresses'
|
||||||
)
|
)
|
||||||
interface_count = tables.Column(
|
member_count = tables.Column(
|
||||||
verbose_name='Interfaces'
|
verbose_name='Members'
|
||||||
)
|
)
|
||||||
tags = TagColumn(
|
tags = TagColumn(
|
||||||
url_name='ipam:fhrpgroup_list'
|
url_name='ipam:fhrpgroup_list'
|
||||||
@ -37,10 +37,10 @@ class FHRPGroupTable(BaseTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = FHRPGroup
|
model = FHRPGroup
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'group_id', 'protocol', 'auth_type', 'auth_key', 'description', 'ip_addresses', 'interface_count',
|
'pk', 'group_id', 'protocol', 'auth_type', 'auth_key', 'description', 'ip_addresses', 'member_count',
|
||||||
'tags', 'created', 'last_updated',
|
'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'group_id', 'protocol', 'auth_type', 'description', 'ip_addresses', 'interface_count')
|
default_columns = ('pk', 'group_id', 'protocol', 'auth_type', 'description', 'ip_addresses', 'member_count')
|
||||||
|
|
||||||
|
|
||||||
class FHRPGroupAssignmentTable(BaseTable):
|
class FHRPGroupAssignmentTable(BaseTable):
|
||||||
|
@ -117,6 +117,10 @@ class ASNTable(BaseTable):
|
|||||||
site_count = LinkedCountColumn(
|
site_count = LinkedCountColumn(
|
||||||
viewname='dcim:site_list',
|
viewname='dcim:site_list',
|
||||||
url_params={'asn_id': 'pk'},
|
url_params={'asn_id': 'pk'},
|
||||||
|
verbose_name='Site Count'
|
||||||
|
)
|
||||||
|
sites = tables.ManyToManyColumn(
|
||||||
|
linkify_item=True,
|
||||||
verbose_name='Sites'
|
verbose_name='Sites'
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
@ -129,7 +133,7 @@ class ASNTable(BaseTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = ASN
|
model = ASN
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'tenant', 'description', 'actions', 'created',
|
'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'tenant', 'description', 'sites', 'actions', 'created',
|
||||||
'last_updated', 'tags',
|
'last_updated', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'asn', 'rir', 'site_count', 'sites', 'description', 'tenant', 'actions')
|
default_columns = ('pk', 'asn', 'rir', 'site_count', 'sites', 'description', 'tenant', 'actions')
|
||||||
|
@ -35,8 +35,8 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
]
|
]
|
||||||
|
|
||||||
asns = (
|
asns = (
|
||||||
ASN(asn=64512, rir=rirs[0], tenant=tenants[0]),
|
ASN(asn=64512, rir=rirs[0], tenant=tenants[0], description='foobar1'),
|
||||||
ASN(asn=64513, rir=rirs[0], tenant=tenants[0]),
|
ASN(asn=64513, rir=rirs[0], tenant=tenants[0], description='foobar2'),
|
||||||
ASN(asn=64514, rir=rirs[0], tenant=tenants[1]),
|
ASN(asn=64514, rir=rirs[0], tenant=tenants[1]),
|
||||||
ASN(asn=64515, rir=rirs[0], tenant=tenants[2]),
|
ASN(asn=64515, rir=rirs[0], tenant=tenants[2]),
|
||||||
ASN(asn=64516, rir=rirs[0], tenant=tenants[3]),
|
ASN(asn=64516, rir=rirs[0], tenant=tenants[3]),
|
||||||
@ -86,6 +86,10 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 9)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 9)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class VRFTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class VRFTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = VRF.objects.all()
|
queryset = VRF.objects.all()
|
||||||
@ -117,8 +121,8 @@ class VRFTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
vrfs = (
|
vrfs = (
|
||||||
VRF(name='VRF 1', rd='65000:100', tenant=tenants[0], enforce_unique=False),
|
VRF(name='VRF 1', rd='65000:100', tenant=tenants[0], enforce_unique=False, description='foobar1'),
|
||||||
VRF(name='VRF 2', rd='65000:200', tenant=tenants[0], enforce_unique=False),
|
VRF(name='VRF 2', rd='65000:200', tenant=tenants[0], enforce_unique=False, description='foobar2'),
|
||||||
VRF(name='VRF 3', rd='65000:300', tenant=tenants[1], enforce_unique=False),
|
VRF(name='VRF 3', rd='65000:300', tenant=tenants[1], enforce_unique=False),
|
||||||
VRF(name='VRF 4', rd='65000:400', tenant=tenants[1], enforce_unique=True),
|
VRF(name='VRF 4', rd='65000:400', tenant=tenants[1], enforce_unique=True),
|
||||||
VRF(name='VRF 5', rd='65000:500', tenant=tenants[2], enforce_unique=True),
|
VRF(name='VRF 5', rd='65000:500', tenant=tenants[2], enforce_unique=True),
|
||||||
@ -174,6 +178,10 @@ class VRFTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = RouteTarget.objects.all()
|
queryset = RouteTarget.objects.all()
|
||||||
@ -198,8 +206,8 @@ class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
route_targets = (
|
route_targets = (
|
||||||
RouteTarget(name='65000:1001', tenant=tenants[0]),
|
RouteTarget(name='65000:1001', tenant=tenants[0], description='foobar1'),
|
||||||
RouteTarget(name='65000:1002', tenant=tenants[0]),
|
RouteTarget(name='65000:1002', tenant=tenants[0], description='foobar2'),
|
||||||
RouteTarget(name='65000:1003', tenant=tenants[0]),
|
RouteTarget(name='65000:1003', tenant=tenants[0]),
|
||||||
RouteTarget(name='65000:1004', tenant=tenants[0]),
|
RouteTarget(name='65000:1004', tenant=tenants[0]),
|
||||||
RouteTarget(name='65000:2001', tenant=tenants[1]),
|
RouteTarget(name='65000:2001', tenant=tenants[1]),
|
||||||
@ -256,6 +264,10 @@ class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class RIRTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class RIRTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = RIR.objects.all()
|
queryset = RIR.objects.all()
|
||||||
@ -323,8 +335,8 @@ class AggregateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
aggregates = (
|
aggregates = (
|
||||||
Aggregate(prefix='10.1.0.0/16', rir=rirs[0], tenant=tenants[0], date_added='2020-01-01'),
|
Aggregate(prefix='10.1.0.0/16', rir=rirs[0], tenant=tenants[0], date_added='2020-01-01', description='foobar1'),
|
||||||
Aggregate(prefix='10.2.0.0/16', rir=rirs[0], tenant=tenants[1], date_added='2020-01-02'),
|
Aggregate(prefix='10.2.0.0/16', rir=rirs[0], tenant=tenants[1], date_added='2020-01-02', description='foobar2'),
|
||||||
Aggregate(prefix='10.3.0.0/16', rir=rirs[1], tenant=tenants[2], date_added='2020-01-03'),
|
Aggregate(prefix='10.3.0.0/16', rir=rirs[1], tenant=tenants[2], date_added='2020-01-03'),
|
||||||
Aggregate(prefix='2001:db8:1::/48', rir=rirs[1], tenant=tenants[0], date_added='2020-01-04'),
|
Aggregate(prefix='2001:db8:1::/48', rir=rirs[1], tenant=tenants[0], date_added='2020-01-04'),
|
||||||
Aggregate(prefix='2001:db8:2::/48', rir=rirs[2], tenant=tenants[1], date_added='2020-01-05'),
|
Aggregate(prefix='2001:db8:2::/48', rir=rirs[2], tenant=tenants[1], date_added='2020-01-05'),
|
||||||
@ -340,6 +352,10 @@ class AggregateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'date_added': ['2020-01-01', '2020-01-02']}
|
params = {'date_added': ['2020-01-01', '2020-01-02']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
# TODO: Test for multiple values
|
# TODO: Test for multiple values
|
||||||
def test_prefix(self):
|
def test_prefix(self):
|
||||||
params = {'prefix': '10.1.0.0/16'}
|
params = {'prefix': '10.1.0.0/16'}
|
||||||
@ -375,8 +391,8 @@ class RoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
roles = (
|
roles = (
|
||||||
Role(name='Role 1', slug='role-1'),
|
Role(name='Role 1', slug='role-1', description='foobar1'),
|
||||||
Role(name='Role 2', slug='role-2'),
|
Role(name='Role 2', slug='role-2', description='foobar2'),
|
||||||
Role(name='Role 3', slug='role-3'),
|
Role(name='Role 3', slug='role-3'),
|
||||||
)
|
)
|
||||||
Role.objects.bulk_create(roles)
|
Role.objects.bulk_create(roles)
|
||||||
@ -389,6 +405,10 @@ class RoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'slug': ['role-1', 'role-2']}
|
params = {'slug': ['role-1', 'role-2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = Prefix.objects.all()
|
queryset = Prefix.objects.all()
|
||||||
@ -467,8 +487,8 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
prefixes = (
|
prefixes = (
|
||||||
Prefix(prefix='10.0.0.0/24', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True),
|
Prefix(prefix='10.0.0.0/24', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True, description='foobar1'),
|
||||||
Prefix(prefix='10.0.1.0/24', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]),
|
Prefix(prefix='10.0.1.0/24', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0], description='foobar2'),
|
||||||
Prefix(prefix='10.0.2.0/24', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED),
|
Prefix(prefix='10.0.2.0/24', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED),
|
||||||
Prefix(prefix='10.0.3.0/24', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED),
|
Prefix(prefix='10.0.3.0/24', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED),
|
||||||
Prefix(prefix='2001:db8::/64', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True),
|
Prefix(prefix='2001:db8::/64', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True),
|
||||||
@ -601,6 +621,10 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = IPRange.objects.all()
|
queryset = IPRange.objects.all()
|
||||||
@ -639,8 +663,8 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
ip_ranges = (
|
ip_ranges = (
|
||||||
IPRange(start_address='10.0.1.100/24', end_address='10.0.1.199/24', size=100, vrf=None, tenant=None, role=None, status=IPRangeStatusChoices.STATUS_ACTIVE),
|
IPRange(start_address='10.0.1.100/24', end_address='10.0.1.199/24', size=100, vrf=None, tenant=None, role=None, status=IPRangeStatusChoices.STATUS_ACTIVE, description='foobar1'),
|
||||||
IPRange(start_address='10.0.2.100/24', end_address='10.0.2.199/24', size=100, vrf=vrfs[0], tenant=tenants[0], role=roles[0], status=IPRangeStatusChoices.STATUS_ACTIVE),
|
IPRange(start_address='10.0.2.100/24', end_address='10.0.2.199/24', size=100, vrf=vrfs[0], tenant=tenants[0], role=roles[0], status=IPRangeStatusChoices.STATUS_ACTIVE, description='foobar2'),
|
||||||
IPRange(start_address='10.0.3.100/24', end_address='10.0.3.199/24', size=100, vrf=vrfs[1], tenant=tenants[1], role=roles[1], status=IPRangeStatusChoices.STATUS_DEPRECATED),
|
IPRange(start_address='10.0.3.100/24', end_address='10.0.3.199/24', size=100, vrf=vrfs[1], tenant=tenants[1], role=roles[1], status=IPRangeStatusChoices.STATUS_DEPRECATED),
|
||||||
IPRange(start_address='10.0.4.100/24', end_address='10.0.4.199/24', size=100, vrf=vrfs[2], tenant=tenants[2], role=roles[2], status=IPRangeStatusChoices.STATUS_RESERVED),
|
IPRange(start_address='10.0.4.100/24', end_address='10.0.4.199/24', size=100, vrf=vrfs[2], tenant=tenants[2], role=roles[2], status=IPRangeStatusChoices.STATUS_RESERVED),
|
||||||
IPRange(start_address='2001:db8:0:1::1/64', end_address='2001:db8:0:1::100/64', size=100, vrf=None, tenant=None, role=None, status=IPRangeStatusChoices.STATUS_ACTIVE),
|
IPRange(start_address='2001:db8:0:1::1/64', end_address='2001:db8:0:1::100/64', size=100, vrf=None, tenant=None, role=None, status=IPRangeStatusChoices.STATUS_ACTIVE),
|
||||||
@ -692,6 +716,10 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = IPAddress.objects.all()
|
queryset = IPAddress.objects.all()
|
||||||
@ -1201,8 +1229,8 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
|
|
||||||
vlans = (
|
vlans = (
|
||||||
# Create one VLAN per VLANGroup
|
# Create one VLAN per VLANGroup
|
||||||
VLAN(vid=1, name='Region 1', group=groups[0]),
|
VLAN(vid=1, name='Region 1', group=groups[0], description='foobar1'),
|
||||||
VLAN(vid=2, name='Region 2', group=groups[1]),
|
VLAN(vid=2, name='Region 2', group=groups[1], description='foobar2'),
|
||||||
VLAN(vid=3, name='Region 3', group=groups[2]),
|
VLAN(vid=3, name='Region 3', group=groups[2]),
|
||||||
VLAN(vid=4, name='Site Group 1', group=groups[3]),
|
VLAN(vid=4, name='Site Group 1', group=groups[3]),
|
||||||
VLAN(vid=5, name='Site Group 2', group=groups[4]),
|
VLAN(vid=5, name='Site Group 2', group=groups[4]),
|
||||||
@ -1271,6 +1299,10 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'group': [groups[0].slug, groups[1].slug]}
|
params = {'group': [groups[0].slug, groups[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_role(self):
|
def test_role(self):
|
||||||
roles = Role.objects.all()[:2]
|
roles = Role.objects.all()[:2]
|
||||||
params = {'role_id': [roles[0].pk, roles[1].pk]}
|
params = {'role_id': [roles[0].pk, roles[1].pk]}
|
||||||
@ -1337,8 +1369,8 @@ class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
VirtualMachine.objects.bulk_create(virtual_machines)
|
VirtualMachine.objects.bulk_create(virtual_machines)
|
||||||
|
|
||||||
services = (
|
services = (
|
||||||
Service(device=devices[0], name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1001]),
|
Service(device=devices[0], name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1001], description='foobar1'),
|
||||||
Service(device=devices[1], name='Service 2', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1002]),
|
Service(device=devices[1], name='Service 2', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1002], description='foobar2'),
|
||||||
Service(device=devices[2], name='Service 3', protocol=ServiceProtocolChoices.PROTOCOL_UDP, ports=[1003]),
|
Service(device=devices[2], name='Service 3', protocol=ServiceProtocolChoices.PROTOCOL_UDP, ports=[1003]),
|
||||||
Service(virtual_machine=virtual_machines[0], name='Service 4', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[2001]),
|
Service(virtual_machine=virtual_machines[0], name='Service 4', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[2001]),
|
||||||
Service(virtual_machine=virtual_machines[1], name='Service 5', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[2002]),
|
Service(virtual_machine=virtual_machines[1], name='Service 5', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[2002]),
|
||||||
@ -1354,6 +1386,10 @@ class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'protocol': ServiceProtocolChoices.PROTOCOL_TCP}
|
params = {'protocol': ServiceProtocolChoices.PROTOCOL_TCP}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_port(self):
|
def test_port(self):
|
||||||
params = {'port': '1001'}
|
params = {'port': '1001'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
@ -19,7 +19,7 @@ from netbox.config import PARAMS
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '3.1.9-dev'
|
VERSION = '3.1.10-dev'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
|
BIN
netbox/project-static/dist/netbox-dark.css
vendored
BIN
netbox/project-static/dist/netbox-dark.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -6,7 +6,16 @@ type ShowHideMap = {
|
|||||||
*
|
*
|
||||||
* @example vlangroup_edit
|
* @example vlangroup_edit
|
||||||
*/
|
*/
|
||||||
[view: string]: {
|
[view: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ShowHideLayout = {
|
||||||
|
/**
|
||||||
|
* Name of layout config
|
||||||
|
*
|
||||||
|
* @example vlangroup
|
||||||
|
*/
|
||||||
|
[config: string]: {
|
||||||
/**
|
/**
|
||||||
* Default layout.
|
* Default layout.
|
||||||
*/
|
*/
|
||||||
@ -19,15 +28,15 @@ type ShowHideMap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping of scope names to arrays of object types whose fields should be hidden or shown when
|
* Mapping of layout names to arrays of object types whose fields should be hidden or shown when
|
||||||
* the scope type (key) is selected.
|
* the scope type (key) is selected.
|
||||||
*
|
*
|
||||||
* For example, if `region` is the scope type, the fields with IDs listed in
|
* For example, if `region` is the scope type, the fields with IDs listed in
|
||||||
* showHideMap.region.hide should be hidden, and the fields with IDs listed in
|
* showHideMap.region.hide should be hidden, and the fields with IDs listed in
|
||||||
* showHideMap.region.show should be shown.
|
* showHideMap.region.show should be shown.
|
||||||
*/
|
*/
|
||||||
const showHideMap: ShowHideMap = {
|
const showHideLayout: ShowHideLayout = {
|
||||||
vlangroup_edit: {
|
vlangroup: {
|
||||||
region: {
|
region: {
|
||||||
hide: ['id_sitegroup', 'id_site', 'id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
|
hide: ['id_sitegroup', 'id_site', 'id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
|
||||||
show: ['id_region'],
|
show: ['id_region'],
|
||||||
@ -70,6 +79,17 @@ const showHideMap: ShowHideMap = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of view names to layout configurations
|
||||||
|
*
|
||||||
|
* For example, if `vlangroup_add` is the view, use the layout configuration `vlangroup`.
|
||||||
|
*/
|
||||||
|
const showHideMap: ShowHideMap = {
|
||||||
|
vlangroup_add: 'vlangroup',
|
||||||
|
vlangroup_edit: 'vlangroup',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle visibility of a given element's parent.
|
* Toggle visibility of a given element's parent.
|
||||||
* @param query CSS Query.
|
* @param query CSS Query.
|
||||||
@ -94,8 +114,9 @@ function toggleParentVisibility(query: string, action: 'show' | 'hide') {
|
|||||||
function handleScopeChange<P extends keyof ShowHideMap>(view: P, element: HTMLSelectElement) {
|
function handleScopeChange<P extends keyof ShowHideMap>(view: P, element: HTMLSelectElement) {
|
||||||
// Scope type's innerText looks something like `DCIM > region`.
|
// Scope type's innerText looks something like `DCIM > region`.
|
||||||
const scopeType = element.options[element.selectedIndex].innerText.toLowerCase();
|
const scopeType = element.options[element.selectedIndex].innerText.toLowerCase();
|
||||||
|
const layoutConfig = showHideMap[view];
|
||||||
|
|
||||||
for (const [scope, fields] of Object.entries(showHideMap[view])) {
|
for (const [scope, fields] of Object.entries(showHideLayout[layoutConfig])) {
|
||||||
// If the scope type ends with the specified scope, toggle its field visibility according to
|
// If the scope type ends with the specified scope, toggle its field visibility according to
|
||||||
// the show/hide values.
|
// the show/hide values.
|
||||||
if (scopeType.endsWith(scope)) {
|
if (scopeType.endsWith(scope)) {
|
||||||
@ -109,7 +130,7 @@ function handleScopeChange<P extends keyof ShowHideMap>(view: P, element: HTMLSe
|
|||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, hide all fields.
|
// Otherwise, hide all fields.
|
||||||
for (const field of showHideMap[view].default.hide) {
|
for (const field of showHideLayout[layoutConfig].default.hide) {
|
||||||
toggleParentVisibility(`#${field}`, 'hide');
|
toggleParentVisibility(`#${field}`, 'hide');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@ $theme-colors: (
|
|||||||
'danger': $danger,
|
'danger': $danger,
|
||||||
'light': $light,
|
'light': $light,
|
||||||
'dark': $dark,
|
'dark': $dark,
|
||||||
|
|
||||||
// General-purpose palette
|
// General-purpose palette
|
||||||
'blue': $blue-300,
|
'blue': $blue-300,
|
||||||
'indigo': $indigo-300,
|
'indigo': $indigo-300,
|
||||||
@ -37,7 +36,7 @@ $theme-colors: (
|
|||||||
'cyan': $cyan-300,
|
'cyan': $cyan-300,
|
||||||
'gray': $gray-300,
|
'gray': $gray-300,
|
||||||
'black': $black,
|
'black': $black,
|
||||||
'white': $white,
|
'white': $white
|
||||||
);
|
);
|
||||||
|
|
||||||
// Gradient
|
// Gradient
|
||||||
@ -146,9 +145,9 @@ $nav-tabs-link-active-border-color: $gray-800 $gray-800 $nav-tabs-link-active-bg
|
|||||||
$nav-pills-link-active-color: $component-active-color;
|
$nav-pills-link-active-color: $component-active-color;
|
||||||
$nav-pills-link-active-bg: $component-active-bg;
|
$nav-pills-link-active-bg: $component-active-bg;
|
||||||
|
|
||||||
$navbar-light-color: $darkest;
|
$navbar-light-color: $darker;
|
||||||
$navbar-light-toggler-icon-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'><path stroke='#{$navbar-light-color}' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/></svg>");
|
|
||||||
$navbar-light-toggler-border-color: $gray-700;
|
$navbar-light-toggler-border-color: $gray-700;
|
||||||
|
$navbar-light-toggler-icon-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'><path stroke='#{$navbar-light-toggler-border-color}' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/></svg>");
|
||||||
|
|
||||||
// Dropdowns
|
// Dropdowns
|
||||||
$dropdown-color: $body-color;
|
$dropdown-color: $body-color;
|
||||||
|
@ -139,7 +139,7 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
(function() {
|
function checkSideNav() {
|
||||||
// Check localStorage to see if the sidebar should be pinned.
|
// Check localStorage to see if the sidebar should be pinned.
|
||||||
var sideNavRaw = localStorage.getItem('netbox-sidenav');
|
var sideNavRaw = localStorage.getItem('netbox-sidenav');
|
||||||
// Determine if the device has a small screeen. This media query is equivalent to
|
// Determine if the device has a small screeen. This media query is equivalent to
|
||||||
@ -154,11 +154,15 @@
|
|||||||
// jumpy/glitchy behavior on page reloads.
|
// jumpy/glitchy behavior on page reloads.
|
||||||
document.body.setAttribute('data-sidenav-pinned', '');
|
document.body.setAttribute('data-sidenav-pinned', '');
|
||||||
document.body.setAttribute('data-sidenav-show', '');
|
document.body.setAttribute('data-sidenav-show', '');
|
||||||
|
document.body.removeAttribute('data-sidenav-hidden');
|
||||||
} else {
|
} else {
|
||||||
|
document.body.removeAttribute('data-sidenav-pinned');
|
||||||
document.body.setAttribute('data-sidenav-hidden', '');
|
document.body.setAttribute('data-sidenav-hidden', '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
}
|
||||||
|
window.addEventListener('resize', function(){ checkSideNav() });
|
||||||
|
checkSideNav();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{# Page layout #}
|
{# Page layout #}
|
||||||
|
@ -108,11 +108,12 @@
|
|||||||
|
|
||||||
{# Page footer #}
|
{# Page footer #}
|
||||||
<footer class="footer container-fluid">
|
<footer class="footer container-fluid">
|
||||||
|
{% block footer %}
|
||||||
<div class="row align-items-center justify-content-between mx-0">
|
<div class="row align-items-center justify-content-between mx-0">
|
||||||
|
|
||||||
{# Docs & Community Links #}
|
|
||||||
<div class="col-sm-12 col-md-auto fs-4 noprint">
|
<div class="col-sm-12 col-md-auto fs-4 noprint">
|
||||||
<nav class="nav justify-content-center justify-content-lg-start">
|
<nav class="nav justify-content-center justify-content-lg-start">
|
||||||
|
{% block footer_links %}
|
||||||
{# Documentation #}
|
{# Documentation #}
|
||||||
<a type="button" class="nav-link" href="{% static 'docs/' %}" target="_blank">
|
<a type="button" class="nav-link" href="{% static 'docs/' %}" target="_blank">
|
||||||
<i title="Docs" class="mdi mdi-book-open-variant text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
<i title="Docs" class="mdi mdi-book-open-variant text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
||||||
@ -144,16 +145,17 @@
|
|||||||
<a type="button" class="nav-link" href="https://netdev.chat/" target="_blank">
|
<a type="button" class="nav-link" href="https://netdev.chat/" target="_blank">
|
||||||
<i title="Community" class="mdi mdi-slack text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
<i title="Community" class="mdi mdi-slack text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
||||||
</a>
|
</a>
|
||||||
|
{% endblock footer_links %}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# System Info #}
|
|
||||||
<div class="col-sm-12 col-md-auto text-center text-lg-end text-muted">
|
<div class="col-sm-12 col-md-auto text-center text-lg-end text-muted">
|
||||||
<span class="d-block d-md-inline">{% annotated_now %} {% now 'T' %}</span>
|
<span class="d-block d-md-inline">{% annotated_now %} {% now 'T' %}</span>
|
||||||
<span class="ms-md-3 d-block d-md-inline">{{ settings.HOSTNAME }} (v{{ settings.VERSION }})</span>
|
<span class="ms-md-3 d-block d-md-inline">{{ settings.HOSTNAME }} (v{{ settings.VERSION }})</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock footer %}
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -183,42 +183,98 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">Stats</h5>
|
<h5 class="card-header">Related Objects</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<table class="table table-hover attr-table">
|
||||||
<div class="col col-md-4 text-center">
|
<tr>
|
||||||
<h2><a href="{% url 'dcim:location_list' %}?site_id={{ object.pk }}" class="btn {% if stats.location_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.location_count }}</a></h2>
|
<th scope="row">Locations</th>
|
||||||
<p>Locations</p>
|
<td class="text-end">
|
||||||
</div>
|
{% if stats.location_count %}
|
||||||
<div class="col col-md-4 text-center">
|
<a href="{% url 'dcim:location_list' %}?site_id={{ object.pk }}">{{ stats.location_count }}</a>
|
||||||
<h2><a href="{% url 'dcim:rack_list' %}?site_id={{ object.pk }}" class="btn {% if stats.rack_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.rack_count }}</a></h2>
|
{% else %}
|
||||||
<p>Racks</p>
|
{{ ''|placeholder }}
|
||||||
</div>
|
{% endif %}
|
||||||
<div class="col col-md-4 text-center">
|
</td>
|
||||||
<h2><a href="{% url 'dcim:device_list' %}?site_id={{ object.pk }}" class="btn {% if stats.device_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.device_count }}</a></h2>
|
</tr>
|
||||||
<p>Devices</p>
|
<tr>
|
||||||
</div>
|
<th scope="row">Racks</th>
|
||||||
<div class="col col-md-4 text-center">
|
<td class="text-end">
|
||||||
<h2><a href="{% url 'ipam:prefix_list' %}?site_id={{ object.pk }}" class="btn {% if stats.prefix_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.prefix_count }}</a></h2>
|
{% if stats.rack_count %}
|
||||||
<p>Prefixes</p>
|
<div class="dropdown">
|
||||||
</div>
|
<button class="btn btn-sm btn-light dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<div class="col col-md-4 text-center">
|
{{ stats.rack_count }}
|
||||||
<h2><a href="{% url 'ipam:vlan_list' %}?site_id={{ object.pk }}" class="btn {% if stats.vlan_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.vlan_count }}</a></h2>
|
</button>
|
||||||
<p>VLANs</p>
|
<ul class="dropdown-menu">
|
||||||
</div>
|
<li><a class="dropdown-item" href="{% url 'dcim:rack_list' %}?site_id={{ object.pk }}">View Racks</a></li>
|
||||||
<div class="col col-md-4 text-center">
|
<li><a class="dropdown-item" href="{% url 'dcim:rack_elevation_list' %}?site_id={{ object.pk }}">View Elevations</a></li>
|
||||||
<h2><a href="{% url 'circuits:circuit_list' %}?site_id={{ object.pk }}" class="btn {% if stats.circuit_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2>
|
</ul>
|
||||||
<p>Circuits</p>
|
|
||||||
</div>
|
|
||||||
<div class="col col-md-4 text-center">
|
|
||||||
<h2><a href="{% url 'virtualization:virtualmachine_list' %}?site_id={{ object.pk }}" class="btn {% if stats.vm_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.vm_count }}</a></h2>
|
|
||||||
<p>Virtual Machines</p>
|
|
||||||
</div>
|
|
||||||
<div class="col col-md-4 text-center">
|
|
||||||
<h2><a href="{% url 'ipam:asn_list' %}?site_id={{ object.pk }}" class="btn {% if stats.asn_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.asn_count }}</a></h2>
|
|
||||||
<p>ASNs</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Devices</th>
|
||||||
|
<td class="text-end">
|
||||||
|
{% if stats.device_count %}
|
||||||
|
<a href="{% url 'dcim:device_list' %}?site_id={{ object.pk }}">{{ stats.device_count }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Virtual Machines</th>
|
||||||
|
<td class="text-end">
|
||||||
|
{% if stats.vm_count %}
|
||||||
|
<a href="{% url 'virtualization:virtualmachine_list' %}?site_id={{ object.pk }}">{{ stats.vm_count }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Prefixes</th>
|
||||||
|
<td class="text-end">
|
||||||
|
{% if stats.prefix_count %}
|
||||||
|
<a href="{% url 'ipam:prefix_list' %}?site_id={{ object.pk }}">{{ stats.prefix_count }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">VLANs</th>
|
||||||
|
<td class="text-end">
|
||||||
|
{% if stats.vlan_count %}
|
||||||
|
<a href="{% url 'ipam:vlan_list' %}?site_id={{ object.pk }}">{{ stats.vlan_count }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">ASNs</th>
|
||||||
|
<td class="text-end">
|
||||||
|
{% if stats.asn_count %}
|
||||||
|
<a href="{% url 'ipam:asn_list' %}?site_id={{ object.pk }}">{{ stats.asn_count }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Circuits</th>
|
||||||
|
<td class="text-end">
|
||||||
|
{% if stats.circuit_count %}
|
||||||
|
<a href="{% url 'circuits:circuit_list' %}?site_id={{ object.pk }}">{{ stats.circuit_count }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/panels/contacts.html' %}
|
{% include 'inc/panels/contacts.html' %}
|
||||||
|
@ -5,7 +5,18 @@
|
|||||||
|
|
||||||
{% block title %}Search{% endblock %}
|
{% block title %}Search{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block tabs %}
|
||||||
|
<ul class="nav nav-tabs px-3">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" type="button" role="tab">
|
||||||
|
Results
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endblock tabs %}
|
||||||
|
|
||||||
|
{% block content-wrapper %}
|
||||||
|
<div class="tab-content">
|
||||||
{% if request.GET.q %}
|
{% if request.GET.q %}
|
||||||
{% if results %}
|
{% if results %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -73,4 +84,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock content %}
|
</div>
|
||||||
|
{% endblock content-wrapper %}
|
||||||
|
@ -46,7 +46,7 @@ class ContactRoleFilterSet(OrganizationalModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContactRole
|
model = ContactRole
|
||||||
fields = ['id', 'name', 'slug']
|
fields = ['id', 'name', 'slug', 'description']
|
||||||
|
|
||||||
|
|
||||||
class ContactFilterSet(PrimaryModelFilterSet):
|
class ContactFilterSet(PrimaryModelFilterSet):
|
||||||
|
@ -64,8 +64,8 @@ class TenantTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
tenantgroup.save()
|
tenantgroup.save()
|
||||||
|
|
||||||
tenants = (
|
tenants = (
|
||||||
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
|
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0], description='foobar1'),
|
||||||
Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]),
|
Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1], description='foobar2'),
|
||||||
Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
|
Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
|
||||||
)
|
)
|
||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
@ -85,6 +85,10 @@ class TenantTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'group': [group[0].slug, group[1].slug]}
|
params = {'group': [group[0].slug, group[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class ContactGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class ContactGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = ContactGroup.objects.all()
|
queryset = ContactGroup.objects.all()
|
||||||
@ -137,8 +141,8 @@ class ContactRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
contact_roles = (
|
contact_roles = (
|
||||||
ContactRole(name='Contact Role 1', slug='contact-role-1'),
|
ContactRole(name='Contact Role 1', slug='contact-role-1', description='foobar1'),
|
||||||
ContactRole(name='Contact Role 2', slug='contact-role-2'),
|
ContactRole(name='Contact Role 2', slug='contact-role-2', description='foobar2'),
|
||||||
ContactRole(name='Contact Role 3', slug='contact-role-3'),
|
ContactRole(name='Contact Role 3', slug='contact-role-3'),
|
||||||
)
|
)
|
||||||
ContactRole.objects.bulk_create(contact_roles)
|
ContactRole.objects.bulk_create(contact_roles)
|
||||||
@ -151,6 +155,10 @@ class ContactRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'slug': ['contact-role-1', 'contact-role-2']}
|
params = {'slug': ['contact-role-1', 'contact-role-2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class ContactTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class ContactTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = Contact.objects.all()
|
queryset = Contact.objects.all()
|
||||||
|
@ -97,7 +97,7 @@ class TokenFilterSet(BaseFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Token
|
model = Token
|
||||||
fields = ['id', 'key', 'write_enabled']
|
fields = ['id', 'key', 'write_enabled', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -138,7 +138,7 @@ class ObjectPermissionFilterSet(BaseFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ObjectPermission
|
model = ObjectPermission
|
||||||
fields = ['id', 'name', 'enabled', 'object_types']
|
fields = ['id', 'name', 'enabled', 'object_types', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -142,8 +142,8 @@ class ObjectPermissionTestCase(TestCase, BaseFilterSetTests):
|
|||||||
)
|
)
|
||||||
|
|
||||||
permissions = (
|
permissions = (
|
||||||
ObjectPermission(name='Permission 1', actions=['view', 'add', 'change', 'delete']),
|
ObjectPermission(name='Permission 1', actions=['view', 'add', 'change', 'delete'], description='foobar1'),
|
||||||
ObjectPermission(name='Permission 2', actions=['view', 'add', 'change', 'delete']),
|
ObjectPermission(name='Permission 2', actions=['view', 'add', 'change', 'delete'], description='foobar2'),
|
||||||
ObjectPermission(name='Permission 3', actions=['view', 'add', 'change', 'delete']),
|
ObjectPermission(name='Permission 3', actions=['view', 'add', 'change', 'delete']),
|
||||||
ObjectPermission(name='Permission 4', actions=['view'], enabled=False),
|
ObjectPermission(name='Permission 4', actions=['view'], enabled=False),
|
||||||
ObjectPermission(name='Permission 5', actions=['add'], enabled=False),
|
ObjectPermission(name='Permission 5', actions=['add'], enabled=False),
|
||||||
@ -183,6 +183,10 @@ class ObjectPermissionTestCase(TestCase, BaseFilterSetTests):
|
|||||||
params = {'object_types': [object_types[0].pk, object_types[1].pk]}
|
params = {'object_types': [object_types[0].pk, object_types[1].pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class TokenTestCase(TestCase, BaseFilterSetTests):
|
class TokenTestCase(TestCase, BaseFilterSetTests):
|
||||||
queryset = Token.objects.all()
|
queryset = Token.objects.all()
|
||||||
@ -201,8 +205,8 @@ class TokenTestCase(TestCase, BaseFilterSetTests):
|
|||||||
future_date = make_aware(datetime.datetime(3000, 1, 1))
|
future_date = make_aware(datetime.datetime(3000, 1, 1))
|
||||||
past_date = make_aware(datetime.datetime(2000, 1, 1))
|
past_date = make_aware(datetime.datetime(2000, 1, 1))
|
||||||
tokens = (
|
tokens = (
|
||||||
Token(user=users[0], key=Token.generate_key(), expires=future_date, write_enabled=True),
|
Token(user=users[0], key=Token.generate_key(), expires=future_date, write_enabled=True, description='foobar1'),
|
||||||
Token(user=users[1], key=Token.generate_key(), expires=future_date, write_enabled=True),
|
Token(user=users[1], key=Token.generate_key(), expires=future_date, write_enabled=True, description='foobar2'),
|
||||||
Token(user=users[2], key=Token.generate_key(), expires=past_date, write_enabled=False),
|
Token(user=users[2], key=Token.generate_key(), expires=past_date, write_enabled=False),
|
||||||
)
|
)
|
||||||
Token.objects.bulk_create(tokens)
|
Token.objects.bulk_create(tokens)
|
||||||
@ -232,3 +236,7 @@ class TokenTestCase(TestCase, BaseFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
params = {'write_enabled': False}
|
params = {'write_enabled': False}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
@ -98,7 +98,9 @@ class BulkRenameForm(BootstrapMixin, forms.Form):
|
|||||||
An extendable form to be used for renaming objects in bulk.
|
An extendable form to be used for renaming objects in bulk.
|
||||||
"""
|
"""
|
||||||
find = forms.CharField()
|
find = forms.CharField()
|
||||||
replace = forms.CharField()
|
replace = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
use_regex = forms.BooleanField(
|
use_regex = forms.BooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
initial=True,
|
initial=True,
|
||||||
|
@ -39,6 +39,12 @@ class RestrictedQuerySet(QuerySet):
|
|||||||
# Any permission with null constraints grants access to _all_ instances
|
# Any permission with null constraints grants access to _all_ instances
|
||||||
attrs = Q()
|
attrs = Q()
|
||||||
break
|
break
|
||||||
|
else:
|
||||||
|
# for else, when no break
|
||||||
|
# avoid duplicates when JOIN on many-to-many fields without using DISTINCT.
|
||||||
|
# DISTINCT acts globally on the entire request, which may not be desirable.
|
||||||
|
allowed_objects = self.model.objects.filter(attrs)
|
||||||
|
attrs = Q(pk__in=allowed_objects)
|
||||||
qs = self.filter(attrs)
|
qs = self.filter(attrs)
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
@ -282,7 +282,7 @@ class VMInterfaceFilterSet(PrimaryModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VMInterface
|
model = VMInterface
|
||||||
fields = ['id', 'name', 'enabled', 'mtu']
|
fields = ['id', 'name', 'enabled', 'mtu', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -422,8 +422,8 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
VirtualMachine.objects.bulk_create(vms)
|
VirtualMachine.objects.bulk_create(vms)
|
||||||
|
|
||||||
interfaces = (
|
interfaces = (
|
||||||
VMInterface(virtual_machine=vms[0], name='Interface 1', enabled=True, mtu=100, mac_address='00-00-00-00-00-01'),
|
VMInterface(virtual_machine=vms[0], name='Interface 1', enabled=True, mtu=100, mac_address='00-00-00-00-00-01', description='foobar1'),
|
||||||
VMInterface(virtual_machine=vms[1], name='Interface 2', enabled=True, mtu=200, mac_address='00-00-00-00-00-02'),
|
VMInterface(virtual_machine=vms[1], name='Interface 2', enabled=True, mtu=200, mac_address='00-00-00-00-00-02', description='foobar2'),
|
||||||
VMInterface(virtual_machine=vms[2], name='Interface 3', enabled=False, mtu=300, mac_address='00-00-00-00-00-03'),
|
VMInterface(virtual_machine=vms[2], name='Interface 3', enabled=False, mtu=300, mac_address='00-00-00-00-00-03'),
|
||||||
)
|
)
|
||||||
VMInterface.objects.bulk_create(interfaces)
|
VMInterface.objects.bulk_create(interfaces)
|
||||||
@ -478,3 +478,7 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
def test_mac_address(self):
|
def test_mac_address(self):
|
||||||
params = {'mac_address': ['00-00-00-00-00-01', '00-00-00-00-00-02']}
|
params = {'mac_address': ['00-00-00-00-00-01', '00-00-00-00-00-02']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
@ -61,7 +61,7 @@ class WirelessLANFilterSet(PrimaryModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WirelessLAN
|
model = WirelessLAN
|
||||||
fields = ['id', 'ssid', 'auth_psk']
|
fields = ['id', 'ssid', 'auth_psk', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -93,7 +93,7 @@ class WirelessLinkFilterSet(PrimaryModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WirelessLink
|
model = WirelessLink
|
||||||
fields = ['id', 'ssid', 'auth_psk']
|
fields = ['id', 'ssid', 'auth_psk', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -25,8 +25,8 @@ class WirelessLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
group.save()
|
group.save()
|
||||||
|
|
||||||
child_groups = (
|
child_groups = (
|
||||||
WirelessLANGroup(name='Wireless LAN Group 1A', slug='wireless-lan-group-1a', parent=groups[0]),
|
WirelessLANGroup(name='Wireless LAN Group 1A', slug='wireless-lan-group-1a', parent=groups[0], description='foobar1'),
|
||||||
WirelessLANGroup(name='Wireless LAN Group 1B', slug='wireless-lan-group-1b', parent=groups[0]),
|
WirelessLANGroup(name='Wireless LAN Group 1B', slug='wireless-lan-group-1b', parent=groups[0], description='foobar2'),
|
||||||
WirelessLANGroup(name='Wireless LAN Group 2A', slug='wireless-lan-group-2a', parent=groups[1]),
|
WirelessLANGroup(name='Wireless LAN Group 2A', slug='wireless-lan-group-2a', parent=groups[1]),
|
||||||
WirelessLANGroup(name='Wireless LAN Group 2B', slug='wireless-lan-group-2b', parent=groups[1]),
|
WirelessLANGroup(name='Wireless LAN Group 2B', slug='wireless-lan-group-2b', parent=groups[1]),
|
||||||
WirelessLANGroup(name='Wireless LAN Group 3A', slug='wireless-lan-group-3a', parent=groups[2]),
|
WirelessLANGroup(name='Wireless LAN Group 3A', slug='wireless-lan-group-3a', parent=groups[2]),
|
||||||
@ -54,6 +54,10 @@ class WirelessLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'parent': [parent_groups[0].slug, parent_groups[1].slug]}
|
params = {'parent': [parent_groups[0].slug, parent_groups[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = WirelessLAN.objects.all()
|
queryset = WirelessLAN.objects.all()
|
||||||
@ -147,7 +151,8 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
status=LinkStatusChoices.STATUS_CONNECTED,
|
status=LinkStatusChoices.STATUS_CONNECTED,
|
||||||
auth_type=WirelessAuthTypeChoices.TYPE_OPEN,
|
auth_type=WirelessAuthTypeChoices.TYPE_OPEN,
|
||||||
auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO,
|
auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO,
|
||||||
auth_psk='PSK1'
|
auth_psk='PSK1',
|
||||||
|
description='foobar1'
|
||||||
).save()
|
).save()
|
||||||
WirelessLink(
|
WirelessLink(
|
||||||
interface_a=interfaces[1],
|
interface_a=interfaces[1],
|
||||||
@ -156,7 +161,8 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
status=LinkStatusChoices.STATUS_PLANNED,
|
status=LinkStatusChoices.STATUS_PLANNED,
|
||||||
auth_type=WirelessAuthTypeChoices.TYPE_WEP,
|
auth_type=WirelessAuthTypeChoices.TYPE_WEP,
|
||||||
auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP,
|
auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP,
|
||||||
auth_psk='PSK2'
|
auth_psk='PSK2',
|
||||||
|
description='foobar2'
|
||||||
).save()
|
).save()
|
||||||
WirelessLink(
|
WirelessLink(
|
||||||
interface_a=interfaces[4],
|
interface_a=interfaces[4],
|
||||||
@ -192,3 +198,7 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
def test_auth_psk(self):
|
def test_auth_psk(self):
|
||||||
params = {'auth_psk': ['PSK1', 'PSK2']}
|
params = {'auth_psk': ['PSK1', 'PSK2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
@ -10,7 +10,7 @@ django-redis==5.2.0
|
|||||||
django-rq==2.5.1
|
django-rq==2.5.1
|
||||||
django-tables2==2.4.1
|
django-tables2==2.4.1
|
||||||
django-taggit==2.1.0
|
django-taggit==2.1.0
|
||||||
django-timezone-field==4.2.3
|
django-timezone-field==5.0
|
||||||
djangorestframework==3.12.4
|
djangorestframework==3.12.4
|
||||||
drf-yasg[validation]==1.20.0
|
drf-yasg[validation]==1.20.0
|
||||||
graphene_django==2.15.0
|
graphene_django==2.15.0
|
||||||
@ -18,7 +18,7 @@ gunicorn==20.1.0
|
|||||||
Jinja2==3.0.3
|
Jinja2==3.0.3
|
||||||
Markdown==3.3.6
|
Markdown==3.3.6
|
||||||
markdown-include==0.6.0
|
markdown-include==0.6.0
|
||||||
mkdocs-material==8.1.11
|
mkdocs-material==8.2.5
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==9.0.1
|
Pillow==9.0.1
|
||||||
psycopg2-binary==2.9.3
|
psycopg2-binary==2.9.3
|
||||||
@ -27,6 +27,7 @@ social-auth-app-django==5.0.0
|
|||||||
social-auth-core==4.2.0
|
social-auth-core==4.2.0
|
||||||
svgwrite==1.4.1
|
svgwrite==1.4.1
|
||||||
tablib==3.2.0
|
tablib==3.2.0
|
||||||
|
tzdata==2021.5
|
||||||
|
|
||||||
# Workaround for #7401
|
# Workaround for #7401
|
||||||
jsonschema==3.2.0
|
jsonschema==3.2.0
|
||||||
|
Loading…
Reference in New Issue
Block a user