mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-10 09:38:15 -06:00
Merge branch 'develop' into 16117-allow_filtering_by_vlan_in_prefixes
This commit is contained in:
commit
82814e3347
@ -4,9 +4,19 @@
|
|||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
|
* [#12984](https://github.com/netbox-community/netbox/issues/12984) - Add Molex Micro-Fit power port & outlet types
|
||||||
* [#14639](https://github.com/netbox-community/netbox/issues/14639) - Add Ukrainian translation support
|
* [#14639](https://github.com/netbox-community/netbox/issues/14639) - Add Ukrainian translation support
|
||||||
* [#14686](https://github.com/netbox-community/netbox/issues/14686) - Add German translation support
|
* [#14686](https://github.com/netbox-community/netbox/issues/14686) - Add German translation support
|
||||||
* [#14855](https://github.com/netbox-community/netbox/issues/14855) - Add Chinese translation support
|
* [#14855](https://github.com/netbox-community/netbox/issues/14855) - Add Chinese translation support
|
||||||
|
* [#15353](https://github.com/netbox-community/netbox/issues/15353) - Improve error reporting when custom scripts fail to load
|
||||||
|
* [#15496](https://github.com/netbox-community/netbox/issues/15496) - Implement dedicated views for management of circuit terminations
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#13293](https://github.com/netbox-community/netbox/issues/13293) - Limit interface selector for IP address to current device/VM
|
||||||
|
* [#14953](https://github.com/netbox-community/netbox/issues/14953) - Ensure annotated count fields are present in REST API response data when creating new objects
|
||||||
|
* [#14982](https://github.com/netbox-community/netbox/issues/14982) - Fix OpenAPI schema definition for SerializedPKRelatedFields
|
||||||
|
* [#16138](https://github.com/netbox-community/netbox/issues/16138) - Fix support for referencing users & groups in object permissions
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -275,6 +275,17 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
|||||||
queryset=ProviderNetwork.objects.all(),
|
queryset=ProviderNetwork.objects.all(),
|
||||||
label=_('ProviderNetwork (ID)'),
|
label=_('ProviderNetwork (ID)'),
|
||||||
)
|
)
|
||||||
|
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='circuit__provider_id',
|
||||||
|
queryset=Provider.objects.all(),
|
||||||
|
label=_('Provider (ID)'),
|
||||||
|
)
|
||||||
|
provider = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='circuit__provider__slug',
|
||||||
|
queryset=Provider.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label=_('Provider (slug)'),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
|
@ -3,16 +3,18 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
|
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
|
from dcim.models import Site
|
||||||
from ipam.models import ASN
|
from ipam.models import ASN
|
||||||
from netbox.forms import NetBoxModelBulkEditForm
|
from netbox.forms import NetBoxModelBulkEditForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import add_blank_choice
|
from utilities.forms import add_blank_choice
|
||||||
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||||
from utilities.forms.rendering import FieldSet
|
from utilities.forms.rendering import FieldSet, TabbedGroups
|
||||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, NumberWithOptions
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CircuitBulkEditForm',
|
'CircuitBulkEditForm',
|
||||||
|
'CircuitTerminationBulkEditForm',
|
||||||
'CircuitTypeBulkEditForm',
|
'CircuitTypeBulkEditForm',
|
||||||
'ProviderBulkEditForm',
|
'ProviderBulkEditForm',
|
||||||
'ProviderAccountBulkEditForm',
|
'ProviderAccountBulkEditForm',
|
||||||
@ -172,3 +174,48 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
nullable_fields = (
|
nullable_fields = (
|
||||||
'tenant', 'commit_rate', 'description', 'comments',
|
'tenant', 'commit_rate', 'description', 'comments',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
|
description = forms.CharField(
|
||||||
|
label=_('Description'),
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
site = DynamicModelChoiceField(
|
||||||
|
label=_('Site'),
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
provider_network = DynamicModelChoiceField(
|
||||||
|
label=_('Provider Network'),
|
||||||
|
queryset=ProviderNetwork.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
port_speed = forms.IntegerField(
|
||||||
|
required=False,
|
||||||
|
label=_('Port speed (Kbps)'),
|
||||||
|
)
|
||||||
|
upstream_speed = forms.IntegerField(
|
||||||
|
required=False,
|
||||||
|
label=_('Upstream speed (Kbps)'),
|
||||||
|
)
|
||||||
|
mark_connected = forms.NullBooleanField(
|
||||||
|
label=_('Mark connected'),
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect
|
||||||
|
)
|
||||||
|
|
||||||
|
model = CircuitTermination
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet(
|
||||||
|
'description',
|
||||||
|
TabbedGroups(
|
||||||
|
FieldSet('site', name=_('Site')),
|
||||||
|
FieldSet('provider_network', name=_('Provider Network')),
|
||||||
|
),
|
||||||
|
'mark_connected', name=_('Circuit Termination')
|
||||||
|
),
|
||||||
|
FieldSet('port_speed', 'upstream_speed', name=_('Termination Details')),
|
||||||
|
)
|
||||||
|
nullable_fields = ('description')
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from circuits.choices import CircuitStatusChoices
|
|
||||||
from circuits.models import *
|
|
||||||
from dcim.models import Site
|
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from circuits.choices import *
|
||||||
|
from circuits.models import *
|
||||||
|
from dcim.models import Site
|
||||||
from netbox.forms import NetBoxModelImportForm
|
from netbox.forms import NetBoxModelImportForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
|
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
|
||||||
@ -12,6 +12,7 @@ from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugFiel
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
'CircuitImportForm',
|
'CircuitImportForm',
|
||||||
'CircuitTerminationImportForm',
|
'CircuitTerminationImportForm',
|
||||||
|
'CircuitTerminationImportRelatedForm',
|
||||||
'CircuitTypeImportForm',
|
'CircuitTypeImportForm',
|
||||||
'ProviderImportForm',
|
'ProviderImportForm',
|
||||||
'ProviderAccountImportForm',
|
'ProviderAccountImportForm',
|
||||||
@ -111,7 +112,16 @@ class CircuitImportForm(NetBoxModelImportForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CircuitTerminationImportForm(forms.ModelForm):
|
class BaseCircuitTerminationImportForm(forms.ModelForm):
|
||||||
|
circuit = CSVModelChoiceField(
|
||||||
|
label=_('Circuit'),
|
||||||
|
queryset=Circuit.objects.all(),
|
||||||
|
to_field_name='cid',
|
||||||
|
)
|
||||||
|
term_side = CSVChoiceField(
|
||||||
|
label=_('Termination'),
|
||||||
|
choices=CircuitTerminationSideChoices,
|
||||||
|
)
|
||||||
site = CSVModelChoiceField(
|
site = CSVModelChoiceField(
|
||||||
label=_('Site'),
|
label=_('Site'),
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
@ -125,9 +135,21 @@ class CircuitTerminationImportForm(forms.ModelForm):
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTerminationImportRelatedForm(BaseCircuitTerminationImportForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
fields = [
|
fields = [
|
||||||
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||||
'pp_info', 'description',
|
'pp_info', 'description'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTerminationImportForm(NetBoxModelImportForm, BaseCircuitTerminationImportForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CircuitTermination
|
||||||
|
fields = [
|
||||||
|
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||||
|
'pp_info', 'description', 'tags'
|
||||||
]
|
]
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
|
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices, CircuitTerminationSideChoices
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from dcim.models import Region, Site, SiteGroup
|
from dcim.models import Region, Site, SiteGroup
|
||||||
from ipam.models import ASN
|
from ipam.models import ASN
|
||||||
@ -13,6 +13,7 @@ from utilities.forms.widgets import DatePicker, NumberWithOptions
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CircuitFilterForm',
|
'CircuitFilterForm',
|
||||||
|
'CircuitTerminationFilterForm',
|
||||||
'CircuitTypeFilterForm',
|
'CircuitTypeFilterForm',
|
||||||
'ProviderFilterForm',
|
'ProviderFilterForm',
|
||||||
'ProviderAccountFilterForm',
|
'ProviderAccountFilterForm',
|
||||||
@ -186,3 +187,46 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||||
|
model = CircuitTermination
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
|
FieldSet('circuit_id', 'term_side', name=_('Circuit')),
|
||||||
|
FieldSet('provider_id', 'provider_network_id', name=_('Provider')),
|
||||||
|
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||||
|
)
|
||||||
|
site_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region_id',
|
||||||
|
'site_group_id': '$site_group_id',
|
||||||
|
},
|
||||||
|
label=_('Site')
|
||||||
|
)
|
||||||
|
circuit_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Circuit.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Circuit')
|
||||||
|
)
|
||||||
|
term_side = forms.MultipleChoiceField(
|
||||||
|
label=_('Term Side'),
|
||||||
|
choices=CircuitTerminationSideChoices,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
provider_network_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=ProviderNetwork.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'provider_id': '$provider_id'
|
||||||
|
},
|
||||||
|
label=_('Provider network')
|
||||||
|
)
|
||||||
|
provider_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Provider.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Provider')
|
||||||
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
@ -227,7 +227,7 @@ class CircuitTermination(
|
|||||||
return f'{self.circuit}: Termination {self.term_side}'
|
return f'{self.circuit}: Termination {self.term_side}'
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return self.circuit.get_absolute_url()
|
return reverse('circuits:circuittermination', args=[self.pk])
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
@ -10,6 +10,7 @@ from .columns import CommitRateColumn
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CircuitTable',
|
'CircuitTable',
|
||||||
|
'CircuitTerminationTable',
|
||||||
'CircuitTypeTable',
|
'CircuitTypeTable',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -88,3 +89,31 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
|||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
|
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTerminationTable(NetBoxTable):
|
||||||
|
circuit = tables.Column(
|
||||||
|
verbose_name=_('Circuit'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
provider = tables.Column(
|
||||||
|
verbose_name=_('Provider'),
|
||||||
|
linkify=True,
|
||||||
|
accessor='circuit.provider'
|
||||||
|
)
|
||||||
|
site = tables.Column(
|
||||||
|
verbose_name=_('Site'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
provider_network = tables.Column(
|
||||||
|
verbose_name=_('Provider Network'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(NetBoxTable.Meta):
|
||||||
|
model = CircuitTermination
|
||||||
|
fields = (
|
||||||
|
'pk', 'id', 'circuit', 'provider', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||||
|
'xconnect_id', 'pp_info', 'description', 'created', 'last_updated', 'actions',
|
||||||
|
)
|
||||||
|
default_columns = ('pk', 'id', 'circuit', 'provider', 'term_side', 'description')
|
||||||
|
@ -351,24 +351,26 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
|
|
||||||
providers = (
|
providers = (
|
||||||
Provider(name='Provider 1', slug='provider-1'),
|
Provider(name='Provider 1', slug='provider-1'),
|
||||||
|
Provider(name='Provider 2', slug='provider-2'),
|
||||||
|
Provider(name='Provider 3', slug='provider-3'),
|
||||||
)
|
)
|
||||||
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]),
|
||||||
ProviderNetwork(name='Provider Network 2', provider=providers[0]),
|
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
|
||||||
ProviderNetwork(name='Provider Network 3', provider=providers[0]),
|
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
|
||||||
)
|
)
|
||||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||||
|
|
||||||
circuits = (
|
circuits = (
|
||||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 1'),
|
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 1'),
|
||||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 2'),
|
Circuit(provider=providers[1], type=circuit_types[0], cid='Circuit 2'),
|
||||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 3'),
|
Circuit(provider=providers[2], type=circuit_types[0], cid='Circuit 3'),
|
||||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 4'),
|
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 4'),
|
||||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 5'),
|
Circuit(provider=providers[1], type=circuit_types[0], cid='Circuit 5'),
|
||||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 6'),
|
Circuit(provider=providers[2], type=circuit_types[0], cid='Circuit 6'),
|
||||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 7'),
|
Circuit(provider=providers[2], type=circuit_types[0], cid='Circuit 7'),
|
||||||
)
|
)
|
||||||
Circuit.objects.bulk_create(circuits)
|
Circuit.objects.bulk_create(circuits)
|
||||||
|
|
||||||
@ -413,10 +415,17 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
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.filter(cid__in=['Circuit 1', 'Circuit 2'])
|
||||||
params = {'circuit_id': [circuits[0].pk, circuits[1].pk]}
|
params = {'circuit_id': [circuits[0].pk, circuits[1].pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_provider(self):
|
||||||
|
providers = Provider.objects.all()[:2]
|
||||||
|
params = {'provider_id': [providers[0].pk, providers[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||||
|
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||||
|
|
||||||
def test_site(self):
|
def test_site(self):
|
||||||
sites = Site.objects.all()[:2]
|
sites = Site.objects.all()[:2]
|
||||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||||
|
@ -5,8 +5,11 @@ from django.urls import reverse
|
|||||||
|
|
||||||
from circuits.choices import *
|
from circuits.choices import *
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
|
from core.models import ObjectType
|
||||||
from dcim.models import Cable, Interface, Site
|
from dcim.models import Cable, Interface, Site
|
||||||
from ipam.models import ASN, RIR
|
from ipam.models import ASN, RIR
|
||||||
|
from netbox.choices import ImportFormatChoices
|
||||||
|
from users.models import ObjectPermission
|
||||||
from utilities.testing import ViewTestCases, create_tags, create_test_device
|
from utilities.testing import ViewTestCases, create_tags, create_test_device
|
||||||
|
|
||||||
|
|
||||||
@ -115,6 +118,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
Site.objects.create(name='Site 1', slug='site-1')
|
||||||
|
|
||||||
providers = (
|
providers = (
|
||||||
Provider(name='Provider 1', slug='provider-1'),
|
Provider(name='Provider 1', slug='provider-1'),
|
||||||
@ -184,6 +188,51 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'comments': 'New comments',
|
'comments': 'New comments',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
|
||||||
|
def test_bulk_import_objects_with_terminations(self):
|
||||||
|
json_data = """
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"cid": "Circuit 7",
|
||||||
|
"provider": "Provider 1",
|
||||||
|
"type": "Circuit Type 1",
|
||||||
|
"status": "active",
|
||||||
|
"description": "Testing Import",
|
||||||
|
"terminations": [
|
||||||
|
{
|
||||||
|
"term_side": "A",
|
||||||
|
"site": "Site 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"term_side": "Z",
|
||||||
|
"site": "Site 1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
initial_count = self._get_queryset().count()
|
||||||
|
data = {
|
||||||
|
'data': json_data,
|
||||||
|
'format': ImportFormatChoices.JSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Assign model-level permission
|
||||||
|
obj_perm = ObjectPermission(
|
||||||
|
name='Test permission',
|
||||||
|
actions=['add']
|
||||||
|
)
|
||||||
|
obj_perm.save()
|
||||||
|
obj_perm.users.add(self.user)
|
||||||
|
obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
|
||||||
|
|
||||||
|
# Try GET with model-level permission
|
||||||
|
self.assertHttpStatus(self.client.get(self._get_url('import')), 200)
|
||||||
|
|
||||||
|
# Test POST with permission
|
||||||
|
self.assertHttpStatus(self.client.post(self._get_url('import'), data), 302)
|
||||||
|
self.assertEqual(self._get_queryset().count(), initial_count + 1)
|
||||||
|
|
||||||
|
|
||||||
class ProviderAccountTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class ProviderAccountTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = ProviderAccount
|
model = ProviderAccount
|
||||||
@ -287,10 +336,7 @@ class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CircuitTerminationTestCase(
|
class CircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
ViewTestCases.EditObjectViewTestCase,
|
|
||||||
ViewTestCases.DeleteObjectViewTestCase,
|
|
||||||
):
|
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -327,6 +373,24 @@ class CircuitTerminationTestCase(
|
|||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
"circuit,term_side,site,description",
|
||||||
|
"Circuit 3,A,Site 1,Foo",
|
||||||
|
"Circuit 3,Z,Site 1,Bar",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.csv_update_data = (
|
||||||
|
"id,port_speed,description",
|
||||||
|
f"{circuit_terminations[0].pk},100,New description7",
|
||||||
|
f"{circuit_terminations[1].pk},200,New description8",
|
||||||
|
f"{circuit_terminations[2].pk},300,New description9",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'port_speed': 400,
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
def test_trace(self):
|
def test_trace(self):
|
||||||
device = create_test_device('Device 1')
|
device = create_test_device('Device 1')
|
||||||
|
@ -48,7 +48,11 @@ urlpatterns = [
|
|||||||
path('circuits/<int:pk>/', include(get_model_urls('circuits', 'circuit'))),
|
path('circuits/<int:pk>/', include(get_model_urls('circuits', 'circuit'))),
|
||||||
|
|
||||||
# Circuit terminations
|
# Circuit terminations
|
||||||
|
path('circuit-terminations/', views.CircuitTerminationListView.as_view(), name='circuittermination_list'),
|
||||||
path('circuit-terminations/add/', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
|
path('circuit-terminations/add/', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
|
||||||
|
path('circuit-terminations/import/', views.CircuitTerminationBulkImportView.as_view(), name='circuittermination_import'),
|
||||||
|
path('circuit-terminations/edit/', views.CircuitTerminationBulkEditView.as_view(), name='circuittermination_bulk_edit'),
|
||||||
|
path('circuit-terminations/delete/', views.CircuitTerminationBulkDeleteView.as_view(), name='circuittermination_bulk_delete'),
|
||||||
path('circuit-terminations/<int:pk>/', include(get_model_urls('circuits', 'circuittermination'))),
|
path('circuit-terminations/<int:pk>/', include(get_model_urls('circuits', 'circuittermination'))),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -298,7 +298,7 @@ class CircuitBulkImportView(generic.BulkImportView):
|
|||||||
'circuits.add_circuittermination',
|
'circuits.add_circuittermination',
|
||||||
]
|
]
|
||||||
related_object_forms = {
|
related_object_forms = {
|
||||||
'terminations': forms.CircuitTerminationImportForm,
|
'terminations': forms.CircuitTerminationImportRelatedForm,
|
||||||
}
|
}
|
||||||
|
|
||||||
def prep_related_object_data(self, parent, data):
|
def prep_related_object_data(self, parent, data):
|
||||||
@ -408,6 +408,18 @@ class CircuitContactsView(ObjectContactsView):
|
|||||||
# Circuit terminations
|
# Circuit terminations
|
||||||
#
|
#
|
||||||
|
|
||||||
|
class CircuitTerminationListView(generic.ObjectListView):
|
||||||
|
queryset = CircuitTermination.objects.all()
|
||||||
|
filterset = filtersets.CircuitTerminationFilterSet
|
||||||
|
filterset_form = forms.CircuitTerminationFilterForm
|
||||||
|
table = tables.CircuitTerminationTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(CircuitTermination)
|
||||||
|
class CircuitTerminationView(generic.ObjectView):
|
||||||
|
queryset = CircuitTermination.objects.all()
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(CircuitTermination, 'edit')
|
@register_model_view(CircuitTermination, 'edit')
|
||||||
class CircuitTerminationEditView(generic.ObjectEditView):
|
class CircuitTerminationEditView(generic.ObjectEditView):
|
||||||
queryset = CircuitTermination.objects.all()
|
queryset = CircuitTermination.objects.all()
|
||||||
@ -419,5 +431,23 @@ class CircuitTerminationDeleteView(generic.ObjectDeleteView):
|
|||||||
queryset = CircuitTermination.objects.all()
|
queryset = CircuitTermination.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTerminationBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = CircuitTermination.objects.all()
|
||||||
|
model_form = forms.CircuitTerminationImportForm
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTerminationBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = CircuitTermination.objects.all()
|
||||||
|
filterset = filtersets.CircuitTerminationFilterSet
|
||||||
|
table = tables.CircuitTerminationTable
|
||||||
|
form = forms.CircuitTerminationBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTerminationBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
queryset = CircuitTermination.objects.all()
|
||||||
|
filterset = filtersets.CircuitTerminationFilterSet
|
||||||
|
table = tables.CircuitTerminationTable
|
||||||
|
|
||||||
|
|
||||||
# Trace view
|
# Trace view
|
||||||
register_model_view(CircuitTermination, 'trace', kwargs={'model': CircuitTermination})(PathTraceView)
|
register_model_view(CircuitTermination, 'trace', kwargs={'model': CircuitTermination})(PathTraceView)
|
||||||
|
@ -255,3 +255,14 @@ class NetBoxAutoSchema(AutoSchema):
|
|||||||
if '{id}' in self.path:
|
if '{id}' in self.path:
|
||||||
return f"{self.method.capitalize()} a {model_name} object."
|
return f"{self.method.capitalize()} a {model_name} object."
|
||||||
return f"{self.method.capitalize()} a list of {model_name} objects."
|
return f"{self.method.capitalize()} a list of {model_name} objects."
|
||||||
|
|
||||||
|
|
||||||
|
class FixSerializedPKRelatedField(OpenApiSerializerFieldExtension):
|
||||||
|
target_class = 'netbox.api.fields.SerializedPKRelatedField'
|
||||||
|
|
||||||
|
def map_serializer_field(self, auto_schema, direction):
|
||||||
|
if direction == "response":
|
||||||
|
component = auto_schema.resolve_serializer(self.target.serializer, direction)
|
||||||
|
return component.ref if component else None
|
||||||
|
else:
|
||||||
|
return build_basic_type(OpenApiTypes.INT)
|
||||||
|
@ -21,7 +21,7 @@ __all__ = (
|
|||||||
class RegionSerializer(NestedGroupModelSerializer):
|
class RegionSerializer(NestedGroupModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
||||||
parent = NestedRegionSerializer(required=False, allow_null=True, default=None)
|
parent = NestedRegionSerializer(required=False, allow_null=True, default=None)
|
||||||
site_count = serializers.IntegerField(read_only=True)
|
site_count = serializers.IntegerField(read_only=True, default=0)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Region
|
model = Region
|
||||||
@ -35,7 +35,7 @@ class RegionSerializer(NestedGroupModelSerializer):
|
|||||||
class SiteGroupSerializer(NestedGroupModelSerializer):
|
class SiteGroupSerializer(NestedGroupModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
|
||||||
parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None)
|
parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None)
|
||||||
site_count = serializers.IntegerField(read_only=True)
|
site_count = serializers.IntegerField(read_only=True, default=0)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SiteGroup
|
model = SiteGroup
|
||||||
@ -86,8 +86,8 @@ class LocationSerializer(NestedGroupModelSerializer):
|
|||||||
parent = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
parent = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
||||||
status = ChoiceField(choices=LocationStatusChoices, required=False)
|
status = ChoiceField(choices=LocationStatusChoices, required=False)
|
||||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||||
rack_count = serializers.IntegerField(read_only=True)
|
rack_count = serializers.IntegerField(read_only=True, default=0)
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True, default=0)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Location
|
model = Location
|
||||||
|
@ -399,6 +399,10 @@ class PowerPortTypeChoices(ChoiceSet):
|
|||||||
TYPE_USB_MICRO_AB = 'usb-micro-ab'
|
TYPE_USB_MICRO_AB = 'usb-micro-ab'
|
||||||
TYPE_USB_3_B = 'usb-3-b'
|
TYPE_USB_3_B = 'usb-3-b'
|
||||||
TYPE_USB_3_MICROB = 'usb-3-micro-b'
|
TYPE_USB_3_MICROB = 'usb-3-micro-b'
|
||||||
|
# Molex
|
||||||
|
TYPE_MOLEX_MICRO_FIT_1X2 = 'molex-micro-fit-1x2'
|
||||||
|
TYPE_MOLEX_MICRO_FIT_2X2 = 'molex-micro-fit-2x2'
|
||||||
|
TYPE_MOLEX_MICRO_FIT_2X4 = 'molex-micro-fit-2x4'
|
||||||
# Direct current (DC)
|
# Direct current (DC)
|
||||||
TYPE_DC = 'dc-terminal'
|
TYPE_DC = 'dc-terminal'
|
||||||
# Proprietary
|
# Proprietary
|
||||||
@ -520,6 +524,11 @@ class PowerPortTypeChoices(ChoiceSet):
|
|||||||
(TYPE_USB_3_B, 'USB 3.0 Type B'),
|
(TYPE_USB_3_B, 'USB 3.0 Type B'),
|
||||||
(TYPE_USB_3_MICROB, 'USB 3.0 Micro B'),
|
(TYPE_USB_3_MICROB, 'USB 3.0 Micro B'),
|
||||||
)),
|
)),
|
||||||
|
('Molex', (
|
||||||
|
(TYPE_MOLEX_MICRO_FIT_1X2, 'Molex Micro-Fit 1x2'),
|
||||||
|
(TYPE_MOLEX_MICRO_FIT_2X2, 'Molex Micro-Fit 2x2'),
|
||||||
|
(TYPE_MOLEX_MICRO_FIT_2X4, 'Molex Micro-Fit 2x4'),
|
||||||
|
)),
|
||||||
('DC', (
|
('DC', (
|
||||||
(TYPE_DC, 'DC Terminal'),
|
(TYPE_DC, 'DC Terminal'),
|
||||||
)),
|
)),
|
||||||
@ -635,6 +644,10 @@ class PowerOutletTypeChoices(ChoiceSet):
|
|||||||
TYPE_USB_A = 'usb-a'
|
TYPE_USB_A = 'usb-a'
|
||||||
TYPE_USB_MICROB = 'usb-micro-b'
|
TYPE_USB_MICROB = 'usb-micro-b'
|
||||||
TYPE_USB_C = 'usb-c'
|
TYPE_USB_C = 'usb-c'
|
||||||
|
# Molex
|
||||||
|
TYPE_MOLEX_MICRO_FIT_1X2 = 'molex-micro-fit-1x2'
|
||||||
|
TYPE_MOLEX_MICRO_FIT_2X2 = 'molex-micro-fit-2x2'
|
||||||
|
TYPE_MOLEX_MICRO_FIT_2X4 = 'molex-micro-fit-2x4'
|
||||||
# Direct current (DC)
|
# Direct current (DC)
|
||||||
TYPE_DC = 'dc-terminal'
|
TYPE_DC = 'dc-terminal'
|
||||||
# Proprietary
|
# Proprietary
|
||||||
@ -749,6 +762,11 @@ class PowerOutletTypeChoices(ChoiceSet):
|
|||||||
(TYPE_USB_MICROB, 'USB Micro B'),
|
(TYPE_USB_MICROB, 'USB Micro B'),
|
||||||
(TYPE_USB_C, 'USB Type C'),
|
(TYPE_USB_C, 'USB Type C'),
|
||||||
)),
|
)),
|
||||||
|
('Molex', (
|
||||||
|
(TYPE_MOLEX_MICRO_FIT_1X2, 'Molex Micro-Fit 1x2'),
|
||||||
|
(TYPE_MOLEX_MICRO_FIT_2X2, 'Molex Micro-Fit 2x2'),
|
||||||
|
(TYPE_MOLEX_MICRO_FIT_2X4, 'Molex Micro-Fit 2x4'),
|
||||||
|
)),
|
||||||
('DC', (
|
('DC', (
|
||||||
(TYPE_DC, 'DC Terminal'),
|
(TYPE_DC, 'DC Terminal'),
|
||||||
)),
|
)),
|
||||||
|
@ -96,6 +96,7 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
|
|||||||
Proxy model for script module files.
|
Proxy model for script module files.
|
||||||
"""
|
"""
|
||||||
objects = ScriptModuleManager()
|
objects = ScriptModuleManager()
|
||||||
|
error = None
|
||||||
|
|
||||||
event_rules = GenericRelation(
|
event_rules = GenericRelation(
|
||||||
to='extras.EventRule',
|
to='extras.EventRule',
|
||||||
@ -126,6 +127,7 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
|
|||||||
try:
|
try:
|
||||||
module = self.get_module()
|
module = self.get_module()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self.error = e
|
||||||
logger.debug(f"Failed to load script: {self.python_name} error: {e}")
|
logger.debug(f"Failed to load script: {self.python_name} error: {e}")
|
||||||
module = None
|
module = None
|
||||||
|
|
||||||
|
@ -1052,12 +1052,27 @@ class ScriptListView(ContentTypePermissionRequiredMixin, View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class ScriptView(generic.ObjectView):
|
class BaseScriptView(generic.ObjectView):
|
||||||
queryset = Script.objects.all()
|
queryset = Script.objects.all()
|
||||||
|
|
||||||
|
def _get_script_class(self, script):
|
||||||
|
"""
|
||||||
|
Return an instance of the Script's Python class
|
||||||
|
"""
|
||||||
|
if script_class := script.python_class:
|
||||||
|
return script_class()
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptView(BaseScriptView):
|
||||||
|
|
||||||
def get(self, request, **kwargs):
|
def get(self, request, **kwargs):
|
||||||
script = self.get_object(**kwargs)
|
script = self.get_object(**kwargs)
|
||||||
script_class = script.python_class()
|
script_class = self._get_script_class(script)
|
||||||
|
if not script_class:
|
||||||
|
return render(request, 'extras/script.html', {
|
||||||
|
'script': script,
|
||||||
|
})
|
||||||
|
|
||||||
form = script_class.as_form(initial=normalize_querydict(request.GET))
|
form = script_class.as_form(initial=normalize_querydict(request.GET))
|
||||||
|
|
||||||
return render(request, 'extras/script.html', {
|
return render(request, 'extras/script.html', {
|
||||||
@ -1069,11 +1084,16 @@ class ScriptView(generic.ObjectView):
|
|||||||
|
|
||||||
def post(self, request, **kwargs):
|
def post(self, request, **kwargs):
|
||||||
script = self.get_object(**kwargs)
|
script = self.get_object(**kwargs)
|
||||||
script_class = script.python_class()
|
|
||||||
|
|
||||||
if not request.user.has_perm('extras.run_script', obj=script):
|
if not request.user.has_perm('extras.run_script', obj=script):
|
||||||
return HttpResponseForbidden()
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
|
script_class = self._get_script_class(script)
|
||||||
|
if not script_class:
|
||||||
|
return render(request, 'extras/script.html', {
|
||||||
|
'script': script,
|
||||||
|
})
|
||||||
|
|
||||||
form = script_class.as_form(request.POST, request.FILES)
|
form = script_class.as_form(request.POST, request.FILES)
|
||||||
|
|
||||||
# Allow execution only if RQ worker process is running
|
# Allow execution only if RQ worker process is running
|
||||||
@ -1103,21 +1123,22 @@ class ScriptView(generic.ObjectView):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class ScriptSourceView(generic.ObjectView):
|
class ScriptSourceView(BaseScriptView):
|
||||||
queryset = Script.objects.all()
|
queryset = Script.objects.all()
|
||||||
|
|
||||||
def get(self, request, **kwargs):
|
def get(self, request, **kwargs):
|
||||||
script = self.get_object(**kwargs)
|
script = self.get_object(**kwargs)
|
||||||
|
script_class = self._get_script_class(script)
|
||||||
|
|
||||||
return render(request, 'extras/script/source.html', {
|
return render(request, 'extras/script/source.html', {
|
||||||
'script': script,
|
'script': script,
|
||||||
'script_class': script.python_class(),
|
'script_class': script_class,
|
||||||
'job_count': script.jobs.count(),
|
'job_count': script.jobs.count(),
|
||||||
'tab': 'source',
|
'tab': 'source',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class ScriptJobsView(generic.ObjectView):
|
class ScriptJobsView(BaseScriptView):
|
||||||
queryset = Script.objects.all()
|
queryset = Script.objects.all()
|
||||||
|
|
||||||
def get(self, request, **kwargs):
|
def get(self, request, **kwargs):
|
||||||
|
@ -258,6 +258,7 @@ CIRCUITS_MENU = Menu(
|
|||||||
items=(
|
items=(
|
||||||
get_model_item('circuits', 'circuit', _('Circuits')),
|
get_model_item('circuits', 'circuit', _('Circuits')),
|
||||||
get_model_item('circuits', 'circuittype', _('Circuit Types')),
|
get_model_item('circuits', 'circuittype', _('Circuit Types')),
|
||||||
|
get_model_item('circuits', 'circuittermination', _('Circuit Terminations')),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MenuGroup(
|
MenuGroup(
|
||||||
@ -372,19 +373,19 @@ ADMIN_MENU = Menu(
|
|||||||
link=f'users:user_list',
|
link=f'users:user_list',
|
||||||
link_text=_('Users'),
|
link_text=_('Users'),
|
||||||
auth_required=True,
|
auth_required=True,
|
||||||
permissions=[f'auth.view_user'],
|
permissions=[f'users.view_user'],
|
||||||
buttons=(
|
buttons=(
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
link=f'users:user_add',
|
link=f'users:user_add',
|
||||||
title='Add',
|
title='Add',
|
||||||
icon_class='mdi mdi-plus-thick',
|
icon_class='mdi mdi-plus-thick',
|
||||||
permissions=[f'auth.add_user']
|
permissions=[f'users.add_user']
|
||||||
),
|
),
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
link=f'users:user_import',
|
link=f'users:user_import',
|
||||||
title='Import',
|
title='Import',
|
||||||
icon_class='mdi mdi-upload',
|
icon_class='mdi mdi-upload',
|
||||||
permissions=[f'auth.add_user']
|
permissions=[f'users.add_user']
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -392,19 +393,19 @@ ADMIN_MENU = Menu(
|
|||||||
link=f'users:group_list',
|
link=f'users:group_list',
|
||||||
link_text=_('Groups'),
|
link_text=_('Groups'),
|
||||||
auth_required=True,
|
auth_required=True,
|
||||||
permissions=[f'auth.view_group'],
|
permissions=[f'users.view_group'],
|
||||||
buttons=(
|
buttons=(
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
link=f'users:group_add',
|
link=f'users:group_add',
|
||||||
title='Add',
|
title='Add',
|
||||||
icon_class='mdi mdi-plus-thick',
|
icon_class='mdi mdi-plus-thick',
|
||||||
permissions=[f'auth.add_group']
|
permissions=[f'users.add_group']
|
||||||
),
|
),
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
link=f'users:group_import',
|
link=f'users:group_import',
|
||||||
title='Import',
|
title='Import',
|
||||||
icon_class='mdi mdi-upload',
|
icon_class='mdi mdi-upload',
|
||||||
permissions=[f'auth.add_group']
|
permissions=[f'users.add_group']
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
51
netbox/templates/circuits/circuittermination.html
Normal file
51
netbox/templates/circuits/circuittermination.html
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
{{ block.super }}
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'circuits:circuit_list' %}?provider_id={{ object.circuit.provider.pk }}">{{ object.circuit.provider }}</a></li>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
{% if object %}
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Circuit" %}</th>
|
||||||
|
<td>
|
||||||
|
{{ object.circuit|linkify }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Provider" %}</th>
|
||||||
|
<td>
|
||||||
|
{{ object.circuit.provider|linkify }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% include 'circuits/inc/circuit_termination_fields.html' with termination=object %}
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="card-body">
|
||||||
|
<span class="text-muted">{% trans "None" %}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
|
{% include 'inc/panels/tags.html' %}
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
{% plugin_full_width_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -27,93 +27,7 @@
|
|||||||
</h5>
|
</h5>
|
||||||
{% if termination %}
|
{% if termination %}
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
{% if termination.site %}
|
{% include 'circuits/inc/circuit_termination_fields.html' with termination=termination %}
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Site" %}</th>
|
|
||||||
<td>
|
|
||||||
{% if termination.site.region %}
|
|
||||||
{{ termination.site.region|linkify }} /
|
|
||||||
{% endif %}
|
|
||||||
{{ termination.site|linkify }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Termination" %}</th>
|
|
||||||
<td>
|
|
||||||
{% if termination.mark_connected %}
|
|
||||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
|
|
||||||
<span class="text-muted">{% trans "Marked as connected" %}</span>
|
|
||||||
{% elif termination.cable %}
|
|
||||||
<a class="d-block d-md-inline" href="{{ termination.cable.get_absolute_url }}">{{ termination.cable }}</a> {% trans "to" %}
|
|
||||||
{% for peer in termination.link_peers %}
|
|
||||||
{% if peer.device %}
|
|
||||||
{{ peer.device|linkify }}<br/>
|
|
||||||
{% elif peer.circuit %}
|
|
||||||
{{ peer.circuit|linkify }}<br/>
|
|
||||||
{% endif %}
|
|
||||||
{{ peer|linkify }}{% if not forloop.last %},{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
<div class="mt-1">
|
|
||||||
<a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
|
|
||||||
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i> {% trans "Trace" %}
|
|
||||||
</a>
|
|
||||||
{% if perms.dcim.change_cable %}
|
|
||||||
<a href="{% url 'dcim:cable_edit' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="{% trans "Edit cable" %}" class="btn btn-warning lh-1">
|
|
||||||
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> {% trans "Edit" %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.dcim.delete_cable %}
|
|
||||||
<a href="{% url 'dcim:cable_delete' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="{% trans "Remove cable" %}" class="btn btn-danger lh-1">
|
|
||||||
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i> {% trans "Disconnect" %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% elif perms.dcim.add_cable %}
|
|
||||||
<div class="dropdown">
|
|
||||||
<button type="button" class="btn btn-success dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
|
||||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=dcim.interface&return_url={{ object.get_absolute_url }}">{% trans "Interface" %}</a></li>
|
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}">{% trans "Front Port" %}</a></li>
|
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}">{% trans "Rear Port" %}</a></li>
|
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}">{% trans "Circuit Termination" %}</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% else %}
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Provider Network" %}</th>
|
|
||||||
<td>{{ termination.provider_network.provider|linkify }} / {{ termination.provider_network|linkify }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Speed" %}</th>
|
|
||||||
<td>
|
|
||||||
{% if termination.port_speed and termination.upstream_speed %}
|
|
||||||
<i class="mdi mdi-arrow-down-bold" title="{% trans "Downstream" %}"></i> {{ termination.port_speed|humanize_speed }}
|
|
||||||
<i class="mdi mdi-arrow-up-bold" title="{% trans "Upstream" %}"></i> {{ termination.upstream_speed|humanize_speed }}
|
|
||||||
{% elif termination.port_speed %}
|
|
||||||
{{ termination.port_speed|humanize_speed }}
|
|
||||||
{% else %}
|
|
||||||
{{ ''|placeholder }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Cross-Connect" %}</th>
|
|
||||||
<td>{{ termination.xconnect_id|placeholder }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Patch Panel/Port" %}</th>
|
|
||||||
<td>{{ termination.pp_info|placeholder }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Description" %}</th>
|
|
||||||
<td>{{ termination.description|placeholder }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Tags" %}</th>
|
<th scope="row">{% trans "Tags" %}</th>
|
||||||
<td>
|
<td>
|
||||||
|
@ -0,0 +1,90 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% if termination.site %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Site" %}</th>
|
||||||
|
<td>
|
||||||
|
{% if termination.site.region %}
|
||||||
|
{{ termination.site.region|linkify }} /
|
||||||
|
{% endif %}
|
||||||
|
{{ termination.site|linkify }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Termination" %}</th>
|
||||||
|
<td>
|
||||||
|
{% if termination.mark_connected %}
|
||||||
|
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
|
||||||
|
<span class="text-muted">{% trans "Marked as connected" %}</span>
|
||||||
|
{% elif termination.cable %}
|
||||||
|
<a class="d-block d-md-inline" href="{{ termination.cable.get_absolute_url }}">{{ termination.cable }}</a> {% trans "to" %}
|
||||||
|
{% for peer in termination.link_peers %}
|
||||||
|
{% if peer.device %}
|
||||||
|
{{ peer.device|linkify }}<br/>
|
||||||
|
{% elif peer.circuit %}
|
||||||
|
{{ peer.circuit|linkify }}<br/>
|
||||||
|
{% endif %}
|
||||||
|
{{ peer|linkify }}{% if not forloop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="mt-1">
|
||||||
|
<a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
|
||||||
|
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i> {% trans "Trace" %}
|
||||||
|
</a>
|
||||||
|
{% if perms.dcim.change_cable %}
|
||||||
|
<a href="{% url 'dcim:cable_edit' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="{% trans "Edit cable" %}" class="btn btn-warning lh-1">
|
||||||
|
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> {% trans "Edit" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.delete_cable %}
|
||||||
|
<a href="{% url 'dcim:cable_delete' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="{% trans "Remove cable" %}" class="btn btn-danger lh-1">
|
||||||
|
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i> {% trans "Disconnect" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% elif perms.dcim.add_cable %}
|
||||||
|
<div class="dropdown">
|
||||||
|
<button type="button" class="btn btn-success dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=dcim.interface&return_url={{ object.get_absolute_url }}">{% trans "Interface" %}</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}">{% trans "Front Port" %}</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}">{% trans "Rear Port" %}</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}">{% trans "Circuit Termination" %}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Provider Network" %}</th>
|
||||||
|
<td>{{ termination.provider_network.provider|linkify }} / {{ termination.provider_network|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Speed" %}</th>
|
||||||
|
<td>
|
||||||
|
{% if termination.port_speed and termination.upstream_speed %}
|
||||||
|
<i class="mdi mdi-arrow-down-bold" title="{% trans "Downstream" %}"></i> {{ termination.port_speed|humanize_speed }}
|
||||||
|
<i class="mdi mdi-arrow-up-bold" title="{% trans "Upstream" %}"></i> {{ termination.upstream_speed|humanize_speed }}
|
||||||
|
{% elif termination.port_speed %}
|
||||||
|
{{ termination.port_speed|humanize_speed }}
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Cross-Connect" %}</th>
|
||||||
|
<td>{{ termination.xconnect_id|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Patch Panel/Port" %}</th>
|
||||||
|
<td>{{ termination.pp_info|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Description" %}</th>
|
||||||
|
<td>{{ termination.description|placeholder }}</td>
|
||||||
|
</tr>
|
@ -14,38 +14,43 @@
|
|||||||
{% trans "You do not have permission to run scripts" %}.
|
{% trans "You do not have permission to run scripts" %}.
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form action="" method="post" enctype="multipart/form-data" class="object-edit">
|
{% if form %}
|
||||||
{% csrf_token %}
|
<form action="" method="post" enctype="multipart/form-data" class="object-edit">
|
||||||
<div class="field-group my-4">
|
{% csrf_token %}
|
||||||
{# Render grouped fields according to declared fieldsets #}
|
<div class="field-group my-4">
|
||||||
{% for group, fields in script_class.get_fieldsets %}
|
{# Render grouped fields according to declared fieldsets #}
|
||||||
{% if fields %}
|
{% for group, fields in script_class.get_fieldsets %}
|
||||||
<div class="field-group mb-5">
|
{% if fields %}
|
||||||
<div class="row">
|
<div class="field-group mb-5">
|
||||||
<h5 class="col-9 offset-3">{{ group }}</h5>
|
<div class="row">
|
||||||
|
<h5 class="col-9 offset-3">{{ group }}</h5>
|
||||||
|
</div>
|
||||||
|
{% for name in fields %}
|
||||||
|
{% with field=form|getfield:name %}
|
||||||
|
{% render_field field %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% for name in fields %}
|
{% endif %}
|
||||||
{% with field=form|getfield:name %}
|
{% endfor %}
|
||||||
{% render_field field %}
|
</div>
|
||||||
{% endwith %}
|
<div class="text-end">
|
||||||
{% endfor %}
|
<a href="{% url 'extras:script_list' %}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
|
||||||
</div>
|
{% if not request.user|can_run:script or not script.is_executable %}
|
||||||
|
<button class="btn btn-primary" disabled>
|
||||||
|
<i class="mdi mdi-play"></i> {% trans "Run Script" %}
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<button type="submit" name="_run" class="btn btn-primary">
|
||||||
|
<i class="mdi mdi-play"></i> {% trans "Run Script" %}
|
||||||
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
</div>
|
||||||
</div>
|
</form>
|
||||||
<div class="text-end">
|
{% else %}
|
||||||
<a href="{% url 'extras:script_list' %}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
|
<p>{% trans "Error loading script" %}.</p>
|
||||||
{% if not request.user|can_run:script or not script.is_executable %}
|
<pre class="block">{{ script.module.error }}</pre>
|
||||||
<button class="btn btn-primary" disabled>
|
{% endif %}
|
||||||
<i class="mdi mdi-play"></i> {% trans "Run Script" %}
|
|
||||||
</button>
|
|
||||||
{% else %}
|
|
||||||
<button type="submit" name="_run" class="btn btn-primary">
|
|
||||||
<i class="mdi mdi-play"></i> {% trans "Run Script" %}
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
{% extends 'extras/script/base.html' %}
|
{% extends 'extras/script/base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<code class="h6 my-3 d-block">{{ script_class.filename }}</code>
|
|
||||||
<pre class="block">{{ script_class.source }}</pre>
|
{% if script_class %}
|
||||||
|
<code class="h6 my-3 d-block">{{ script_class.filename }}</code>
|
||||||
|
<pre class="block">{{ script_class.source }}</pre>
|
||||||
|
{% else %}
|
||||||
|
<p>{% trans "Error loading script" %}.</p>
|
||||||
|
<pre class="block">{{ script.module.error }}</pre>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -21,7 +21,7 @@ __all__ = (
|
|||||||
class ContactGroupSerializer(NestedGroupModelSerializer):
|
class ContactGroupSerializer(NestedGroupModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactgroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactgroup-detail')
|
||||||
parent = NestedContactGroupSerializer(required=False, allow_null=True, default=None)
|
parent = NestedContactGroupSerializer(required=False, allow_null=True, default=None)
|
||||||
contact_count = serializers.IntegerField(read_only=True)
|
contact_count = serializers.IntegerField(read_only=True, default=0)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContactGroup
|
model = ContactGroup
|
||||||
|
@ -14,7 +14,7 @@ __all__ = (
|
|||||||
class TenantGroupSerializer(NestedGroupModelSerializer):
|
class TenantGroupSerializer(NestedGroupModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
|
||||||
parent = NestedTenantGroupSerializer(required=False, allow_null=True)
|
parent = NestedTenantGroupSerializer(required=False, allow_null=True)
|
||||||
tenant_count = serializers.IntegerField(read_only=True)
|
tenant_count = serializers.IntegerField(read_only=True, default=0)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TenantGroup
|
model = TenantGroup
|
||||||
|
@ -3,8 +3,7 @@ from django.db.models import Q
|
|||||||
|
|
||||||
OBJECTPERMISSION_OBJECT_TYPES = Q(
|
OBJECTPERMISSION_OBJECT_TYPES = Q(
|
||||||
~Q(app_label__in=['account', 'admin', 'auth', 'contenttypes', 'sessions', 'taggit', 'users']) |
|
~Q(app_label__in=['account', 'admin', 'auth', 'contenttypes', 'sessions', 'taggit', 'users']) |
|
||||||
Q(app_label='auth', model__in=['group', 'user']) |
|
Q(app_label='users', model__in=['objectpermission', 'token', 'group', 'user'])
|
||||||
Q(app_label='users', model__in=['objectpermission', 'token'])
|
|
||||||
)
|
)
|
||||||
|
|
||||||
CONSTRAINT_TOKEN_USER = '$user'
|
CONSTRAINT_TOKEN_USER = '$user'
|
||||||
|
53
netbox/users/migrations/0009_update_group_perms.py
Normal file
53
netbox/users/migrations/0009_update_group_perms.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Generated by Django 5.0.5 on 2024-05-15 18:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def update_content_types(apps, schema_editor):
|
||||||
|
ObjectType = apps.get_model('core', 'ObjectType')
|
||||||
|
ObjectPermission = apps.get_model('users', 'ObjectPermission')
|
||||||
|
|
||||||
|
auth_group_ct = ObjectType.objects.filter(app_label='auth', model='group').first()
|
||||||
|
users_group_ct = ObjectType.objects.filter(app_label='users', model='group').first()
|
||||||
|
if auth_group_ct and users_group_ct:
|
||||||
|
perms = ObjectPermission.objects.filter(object_types__in=[auth_group_ct])
|
||||||
|
for perm in perms:
|
||||||
|
perm.object_types.remove(auth_group_ct)
|
||||||
|
perm.object_types.add(users_group_ct)
|
||||||
|
perm.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0008_flip_objectpermission_assignments'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Update ContentTypes
|
||||||
|
migrations.RunPython(
|
||||||
|
code=update_content_types,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='objectpermission',
|
||||||
|
name='object_types',
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
limit_choices_to=models.Q(
|
||||||
|
models.Q(
|
||||||
|
models.Q(
|
||||||
|
(
|
||||||
|
'app_label__in',
|
||||||
|
['account', 'admin', 'auth', 'contenttypes', 'sessions', 'taggit', 'users'],
|
||||||
|
),
|
||||||
|
_negated=True,
|
||||||
|
),
|
||||||
|
models.Q(('app_label', 'users'), ('model__in', ['objectpermission', 'token', 'group', 'user'])),
|
||||||
|
_connector='OR',
|
||||||
|
)
|
||||||
|
),
|
||||||
|
related_name='object_permissions',
|
||||||
|
to='core.objecttype',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -17,7 +17,7 @@ __all__ = (
|
|||||||
class WirelessLANGroupSerializer(NestedGroupModelSerializer):
|
class WirelessLANGroupSerializer(NestedGroupModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail')
|
||||||
parent = NestedWirelessLANGroupSerializer(required=False, allow_null=True, default=None)
|
parent = NestedWirelessLANGroupSerializer(required=False, allow_null=True, default=None)
|
||||||
wirelesslan_count = serializers.IntegerField(read_only=True)
|
wirelesslan_count = serializers.IntegerField(read_only=True, default=0)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WirelessLANGroup
|
model = WirelessLANGroup
|
||||||
|
Loading…
Reference in New Issue
Block a user