mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-25 08:46:10 -06:00
7961 fix merge
This commit is contained in:
commit
969c6dbae8
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.3.5
|
placeholder: v3.3.6
|
||||||
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.3.5
|
placeholder: v3.3.6
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,13 +1,14 @@
|
|||||||
<!--
|
<!--
|
||||||
Thank you for your interest in contributing to NetBox! Please note that
|
Thank you for your interest in contributing to NetBox! Please note that
|
||||||
our contribution policy requires that a feature request or bug report be
|
our contribution policy requires that a feature request or bug report be
|
||||||
approved and assigned prior to filing a pull request. This helps avoid
|
approved and assigned prior to opening a pull request. This helps avoid
|
||||||
wasting time and effort on something that we might not be able to accept.
|
waste time and effort on a proposed change that we might not be able to
|
||||||
|
accept.
|
||||||
|
|
||||||
IF YOUR PULL REQUEST DOES NOT REFERENCE AN ISSUE WHICH HAS BEEN ASSIGNED
|
IF YOUR PULL REQUEST DOES NOT REFERENCE AN ISSUE WHICH HAS BEEN ASSIGNED
|
||||||
TO YOU, IT WE BE CLOSED AUTOMATICALLY.
|
TO YOU, IT WILL BE CLOSED AUTOMATICALLY.
|
||||||
|
|
||||||
Specify your assigned issue number on the line below.
|
Please specify your assigned issue number on the line below.
|
||||||
-->
|
-->
|
||||||
### Fixes: #1234
|
### Fixes: #1234
|
||||||
|
|
||||||
|
@ -82,23 +82,25 @@ class ThingEditView(ObjectEditView):
|
|||||||
Below are the class definitions for NetBox's object views. These views handle CRUD actions for individual objects. The view, add/edit, and delete views each inherit from `BaseObjectView`, which is not intended to be used directly.
|
Below are the class definitions for NetBox's object views. These views handle CRUD actions for individual objects. The view, add/edit, and delete views each inherit from `BaseObjectView`, which is not intended to be used directly.
|
||||||
|
|
||||||
::: netbox.views.generic.base.BaseObjectView
|
::: netbox.views.generic.base.BaseObjectView
|
||||||
|
options:
|
||||||
|
members:
|
||||||
|
- get_queryset
|
||||||
|
- get_object
|
||||||
|
- get_extra_context
|
||||||
|
|
||||||
::: netbox.views.generic.ObjectView
|
::: netbox.views.generic.ObjectView
|
||||||
options:
|
options:
|
||||||
members:
|
members:
|
||||||
- get_object
|
|
||||||
- get_template_name
|
- get_template_name
|
||||||
|
|
||||||
::: netbox.views.generic.ObjectEditView
|
::: netbox.views.generic.ObjectEditView
|
||||||
options:
|
options:
|
||||||
members:
|
members:
|
||||||
- get_object
|
|
||||||
- alter_object
|
- alter_object
|
||||||
|
|
||||||
::: netbox.views.generic.ObjectDeleteView
|
::: netbox.views.generic.ObjectDeleteView
|
||||||
options:
|
options:
|
||||||
members:
|
members: false
|
||||||
- get_object
|
|
||||||
|
|
||||||
::: netbox.views.generic.ObjectChildrenView
|
::: netbox.views.generic.ObjectChildrenView
|
||||||
options:
|
options:
|
||||||
@ -111,6 +113,10 @@ Below are the class definitions for NetBox's object views. These views handle CR
|
|||||||
Below are the class definitions for NetBox's multi-object views. These views handle simultaneous actions for sets objects. The list, import, edit, and delete views each inherit from `BaseMultiObjectView`, which is not intended to be used directly.
|
Below are the class definitions for NetBox's multi-object views. These views handle simultaneous actions for sets objects. The list, import, edit, and delete views each inherit from `BaseMultiObjectView`, which is not intended to be used directly.
|
||||||
|
|
||||||
::: netbox.views.generic.base.BaseMultiObjectView
|
::: netbox.views.generic.base.BaseMultiObjectView
|
||||||
|
options:
|
||||||
|
members:
|
||||||
|
- get_queryset
|
||||||
|
- get_extra_context
|
||||||
|
|
||||||
::: netbox.views.generic.ObjectListView
|
::: netbox.views.generic.ObjectListView
|
||||||
options:
|
options:
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
# NetBox v3.3
|
# NetBox v3.3
|
||||||
|
|
||||||
## v3.3.6 (FUTURE)
|
## v3.3.7 (FUTURE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.3.6 (2022-10-26)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
|
* [#9584](https://github.com/netbox-community/netbox/issues/9584) - Enable filtering devices by device type slug
|
||||||
* [#9722](https://github.com/netbox-community/netbox/issues/9722) - Add LDAP configuration parameters to specify certificates
|
* [#9722](https://github.com/netbox-community/netbox/issues/9722) - Add LDAP configuration parameters to specify certificates
|
||||||
|
* [#10580](https://github.com/netbox-community/netbox/issues/10580) - Link "assigned" checkbox in IP address table to assigned interface
|
||||||
|
* [#10639](https://github.com/netbox-community/netbox/issues/10639) - Set cookie paths according to configured `BASE_PATH`
|
||||||
* [#10685](https://github.com/netbox-community/netbox/issues/10685) - Position A/Z termination cards above the fold under circuit view
|
* [#10685](https://github.com/netbox-community/netbox/issues/10685) - Position A/Z termination cards above the fold under circuit view
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
@ -12,10 +19,17 @@
|
|||||||
* [#9669](https://github.com/netbox-community/netbox/issues/9669) - Strip colons from usernames when using remote authentication
|
* [#9669](https://github.com/netbox-community/netbox/issues/9669) - Strip colons from usernames when using remote authentication
|
||||||
* [#10575](https://github.com/netbox-community/netbox/issues/10575) - Include OIDC dependencies for python-social-auth
|
* [#10575](https://github.com/netbox-community/netbox/issues/10575) - Include OIDC dependencies for python-social-auth
|
||||||
* [#10584](https://github.com/netbox-community/netbox/issues/10584) - Fix service clone link
|
* [#10584](https://github.com/netbox-community/netbox/issues/10584) - Fix service clone link
|
||||||
|
* [#10610](https://github.com/netbox-community/netbox/issues/10610) - Allow assignment of VC member to LAG on non-master peer
|
||||||
* [#10643](https://github.com/netbox-community/netbox/issues/10643) - Ensure consistent display of custom fields for all model forms
|
* [#10643](https://github.com/netbox-community/netbox/issues/10643) - Ensure consistent display of custom fields for all model forms
|
||||||
* [#10646](https://github.com/netbox-community/netbox/issues/10646) - Fix filtering of power feed by power panel when connecting a cable
|
* [#10646](https://github.com/netbox-community/netbox/issues/10646) - Fix filtering of power feed by power panel when connecting a cable
|
||||||
* [#10655](https://github.com/netbox-community/netbox/issues/10655) - Correct display of assigned contacts in object tables
|
* [#10655](https://github.com/netbox-community/netbox/issues/10655) - Correct display of assigned contacts in object tables
|
||||||
|
* [#10682](https://github.com/netbox-community/netbox/issues/10682) - Correct home view links to connection lists
|
||||||
* [#10712](https://github.com/netbox-community/netbox/issues/10712) - Fix ModuleNotFoundError exception when generating API schema under Python 3.9+
|
* [#10712](https://github.com/netbox-community/netbox/issues/10712) - Fix ModuleNotFoundError exception when generating API schema under Python 3.9+
|
||||||
|
* [#10716](https://github.com/netbox-community/netbox/issues/10716) - Add left/right page plugin content embeds for tag view
|
||||||
|
* [#10719](https://github.com/netbox-community/netbox/issues/10719) - Prevent user without sufficient permission from creating an IP address via FHRP group creation
|
||||||
|
* [#10723](https://github.com/netbox-community/netbox/issues/10723) - Distinguish between inside/outside NAT assignments for device/VM primary IPs
|
||||||
|
* [#10745](https://github.com/netbox-community/netbox/issues/10745) - Correct display of status field in clusters list
|
||||||
|
* [#10746](https://github.com/netbox-community/netbox/issues/10746) - Add missing status attribute to cluster view
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
* Device and virtual machine names are no longer case-sensitive. Attempting to create e.g. "device1" and "DEVICE1" will raise a validation error.
|
* Device and virtual machine names are no longer case-sensitive. Attempting to create e.g. "device1" and "DEVICE1" will raise a validation error.
|
||||||
* The `asn` field has been removed from the provider model. Please replicate any provider ASN assignments to the ASN model introduced in NetBox v3.1 prior to upgrading.
|
* The `asn` field has been removed from the provider model. Please replicate any provider ASN assignments to the ASN model introduced in NetBox v3.1 prior to upgrading.
|
||||||
* The `noc_contact`, `admin_contact`, and `portal_url` fields have been removed from the provider model. Please replicate any data remaining in these fields to the contact model introduced in NetBox v3.1 prior to upgrading.
|
* The `noc_contact`, `admin_contact`, and `portal_url` fields have been removed from the provider model. Please replicate any data remaining in these fields to the contact model introduced in NetBox v3.1 prior to upgrading.
|
||||||
|
* The `content_type` field on the CustomLink and ExportTemplate models have been renamed to `content_types` and now supports the assignment of multiple content types.
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
|
|||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* [#8245](https://github.com/netbox-community/netbox/issues/8245) - Enable GraphQL filtering of related objects
|
* [#8245](https://github.com/netbox-community/netbox/issues/8245) - Enable GraphQL filtering of related objects
|
||||||
|
* [#8274](https://github.com/netbox-community/netbox/issues/8274) - Enable associating a custom link with multiple object types
|
||||||
* [#9249](https://github.com/netbox-community/netbox/issues/9249) - Device and virtual machine names are no longer case-sensitive
|
* [#9249](https://github.com/netbox-community/netbox/issues/9249) - Device and virtual machine names are no longer case-sensitive
|
||||||
* [#9478](https://github.com/netbox-community/netbox/issues/9478) - Add `link_peers` field to GraphQL types for cabled objects
|
* [#9478](https://github.com/netbox-community/netbox/issues/9478) - Add `link_peers` field to GraphQL types for cabled objects
|
||||||
* [#9654](https://github.com/netbox-community/netbox/issues/9654) - Add `weight` field to racks, device types, and module types
|
* [#9654](https://github.com/netbox-community/netbox/issues/9654) - Add `weight` field to racks, device types, and module types
|
||||||
@ -30,6 +32,7 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
|
|||||||
* [#10348](https://github.com/netbox-community/netbox/issues/10348) - Add decimal custom field type
|
* [#10348](https://github.com/netbox-community/netbox/issues/10348) - Add decimal custom field type
|
||||||
* [#10556](https://github.com/netbox-community/netbox/issues/10556) - Include a `display` field in all GraphQL object types
|
* [#10556](https://github.com/netbox-community/netbox/issues/10556) - Include a `display` field in all GraphQL object types
|
||||||
* [#10595](https://github.com/netbox-community/netbox/issues/10595) - Add GraphQL relationships for additional generic foreign key fields
|
* [#10595](https://github.com/netbox-community/netbox/issues/10595) - Add GraphQL relationships for additional generic foreign key fields
|
||||||
|
* [#10761](https://github.com/netbox-community/netbox/issues/10761) - Enable associating an export template with multiple object types
|
||||||
|
|
||||||
### Plugins API
|
### Plugins API
|
||||||
|
|
||||||
@ -38,6 +41,7 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
|
|||||||
* [#9072](https://github.com/netbox-community/netbox/issues/9072) - Enable registration of tabbed plugin views for core NetBox models
|
* [#9072](https://github.com/netbox-community/netbox/issues/9072) - Enable registration of tabbed plugin views for core NetBox models
|
||||||
* [#9880](https://github.com/netbox-community/netbox/issues/9880) - Introduce `django_apps` plugin configuration parameter
|
* [#9880](https://github.com/netbox-community/netbox/issues/9880) - Introduce `django_apps` plugin configuration parameter
|
||||||
* [#10314](https://github.com/netbox-community/netbox/issues/10314) - Move `clone()` method from NetBoxModel to CloningMixin
|
* [#10314](https://github.com/netbox-community/netbox/issues/10314) - Move `clone()` method from NetBoxModel to CloningMixin
|
||||||
|
* [#10739](https://github.com/netbox-community/netbox/issues/10739) - Introduce `get_queryset()` method on generic views
|
||||||
|
|
||||||
### Other Changes
|
### Other Changes
|
||||||
|
|
||||||
@ -56,6 +60,10 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
|
|||||||
* Added optional `weight` and `weight_unit` fields
|
* Added optional `weight` and `weight_unit` fields
|
||||||
* dcim.Rack
|
* dcim.Rack
|
||||||
* Added optional `weight` and `weight_unit` fields
|
* Added optional `weight` and `weight_unit` fields
|
||||||
|
* extras.CustomLink
|
||||||
|
* Renamed `content_type` field to `content_types`
|
||||||
|
* extras.ExportTemplate
|
||||||
|
* Renamed `content_type` field to `content_types`
|
||||||
* ipam.FHRPGroup
|
* ipam.FHRPGroup
|
||||||
* Added optional `name` field
|
* Added optional `name` field
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from .bulk_edit import *
|
from .bulk_edit import *
|
||||||
from .bulk_import import *
|
from .bulk_import import *
|
||||||
from .filtersets import *
|
from .filtersets import *
|
||||||
from .models import *
|
from .model_forms import *
|
||||||
|
@ -800,6 +800,12 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Manufacturer (slug)',
|
label='Manufacturer (slug)',
|
||||||
)
|
)
|
||||||
|
device_type = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='device_type__slug',
|
||||||
|
queryset=DeviceType.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Device type (slug)',
|
||||||
|
)
|
||||||
device_type_id = django_filters.ModelMultipleChoiceFilter(
|
device_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
label='Device type (ID)',
|
label='Device type (ID)',
|
||||||
@ -1360,7 +1366,7 @@ class InterfaceFilterSet(
|
|||||||
try:
|
try:
|
||||||
devices = Device.objects.filter(pk__in=id_list)
|
devices = Device.objects.filter(pk__in=id_list)
|
||||||
for device in devices:
|
for device in devices:
|
||||||
vc_interface_ids += device.vc_interfaces().values_list('id', flat=True)
|
vc_interface_ids += device.vc_interfaces(if_master=False).values_list('id', flat=True)
|
||||||
return queryset.filter(pk__in=vc_interface_ids)
|
return queryset.filter(pk__in=vc_interface_ids)
|
||||||
except Device.DoesNotExist:
|
except Device.DoesNotExist:
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from .models import *
|
from .model_forms import *
|
||||||
from .filtersets import *
|
from .filtersets import *
|
||||||
from .object_create import *
|
from .object_create import *
|
||||||
from .object_import import *
|
from .object_import import *
|
||||||
|
@ -3,7 +3,7 @@ from django import forms
|
|||||||
from circuits.models import Circuit, CircuitTermination, Provider
|
from circuits.models import Circuit, CircuitTermination, Provider
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||||
from .models import CableForm
|
from .model_forms import CableForm
|
||||||
|
|
||||||
|
|
||||||
def get_cable_form(a_type, b_type):
|
def get_cable_form(a_type, b_type):
|
||||||
|
@ -3,7 +3,7 @@ from django import forms
|
|||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
|
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
|
||||||
from . import models as model_forms
|
from . import model_forms
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ComponentCreateForm',
|
'ComponentCreateForm',
|
||||||
|
@ -1670,6 +1670,8 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
device_types = DeviceType.objects.all()[:2]
|
device_types = DeviceType.objects.all()[:2]
|
||||||
params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
|
params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'device_type': [device_types[0].slug, device_types[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_devicerole(self):
|
def test_devicerole(self):
|
||||||
device_roles = DeviceRole.objects.all()[:2]
|
device_roles = DeviceRole.objects.all()[:2]
|
||||||
|
@ -117,14 +117,15 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
class CustomLinkSerializer(ValidatedModelSerializer):
|
class CustomLinkSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
|
||||||
content_type = ContentTypeField(
|
content_types = ContentTypeField(
|
||||||
queryset=ContentType.objects.filter(FeatureQuery('custom_links').get_query())
|
queryset=ContentType.objects.filter(FeatureQuery('custom_links').get_query()),
|
||||||
|
many=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomLink
|
model = CustomLink
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'content_type', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name',
|
'id', 'url', 'display', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name',
|
||||||
'button_class', 'new_window', 'created', 'last_updated',
|
'button_class', 'new_window', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -135,14 +136,15 @@ class CustomLinkSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
class ExportTemplateSerializer(ValidatedModelSerializer):
|
class ExportTemplateSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
|
||||||
content_type = ContentTypeField(
|
content_types = ContentTypeField(
|
||||||
queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
|
queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
|
||||||
|
many=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ExportTemplate
|
model = ExportTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'content_type', 'name', 'description', 'template_code', 'mime_type',
|
'id', 'url', 'display', 'content_types', 'name', 'description', 'template_code', 'mime_type',
|
||||||
'file_extension', 'as_attachment', 'created', 'last_updated',
|
'file_extension', 'as_attachment', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -93,11 +93,15 @@ class CustomLinkFilterSet(BaseFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
|
content_type_id = MultiValueNumberFilter(
|
||||||
|
field_name='content_types__id'
|
||||||
|
)
|
||||||
|
content_types = ContentTypeFilter()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomLink
|
model = CustomLink
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'content_type', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window',
|
'id', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window',
|
||||||
]
|
]
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
@ -116,10 +120,14 @@ class ExportTemplateFilterSet(BaseFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
|
content_type_id = MultiValueNumberFilter(
|
||||||
|
field_name='content_types__id'
|
||||||
|
)
|
||||||
|
content_types = ContentTypeFilter()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ExportTemplate
|
model = ExportTemplate
|
||||||
fields = ['id', 'content_type', 'name', 'description']
|
fields = ['id', 'content_types', 'name', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from .models import *
|
from .model_forms import *
|
||||||
from .filtersets import *
|
from .filtersets import *
|
||||||
from .bulk_edit import *
|
from .bulk_edit import *
|
||||||
from .bulk_import import *
|
from .bulk_import import *
|
||||||
|
@ -53,11 +53,6 @@ class CustomLinkBulkEditForm(BulkEditForm):
|
|||||||
queryset=CustomLink.objects.all(),
|
queryset=CustomLink.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
)
|
)
|
||||||
content_type = ContentTypeChoiceField(
|
|
||||||
queryset=ContentType.objects.all(),
|
|
||||||
limit_choices_to=FeatureQuery('custom_links'),
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
enabled = forms.NullBooleanField(
|
enabled = forms.NullBooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
widget=BulkEditNullBooleanSelect()
|
widget=BulkEditNullBooleanSelect()
|
||||||
@ -81,11 +76,6 @@ class ExportTemplateBulkEditForm(BulkEditForm):
|
|||||||
queryset=ExportTemplate.objects.all(),
|
queryset=ExportTemplate.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
)
|
)
|
||||||
content_type = ContentTypeChoiceField(
|
|
||||||
queryset=ContentType.objects.all(),
|
|
||||||
limit_choices_to=FeatureQuery('export_templates'),
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
description = forms.CharField(
|
description = forms.CharField(
|
||||||
max_length=200,
|
max_length=200,
|
||||||
required=False
|
required=False
|
||||||
|
@ -53,31 +53,31 @@ class CustomFieldCSVForm(CSVModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class CustomLinkCSVForm(CSVModelForm):
|
class CustomLinkCSVForm(CSVModelForm):
|
||||||
content_type = CSVContentTypeField(
|
content_types = CSVMultipleContentTypeField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
limit_choices_to=FeatureQuery('custom_links'),
|
limit_choices_to=FeatureQuery('custom_links'),
|
||||||
help_text="Assigned object type"
|
help_text="One or more assigned object types"
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomLink
|
model = CustomLink
|
||||||
fields = (
|
fields = (
|
||||||
'name', 'content_type', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', 'link_text',
|
'name', 'content_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', 'link_text',
|
||||||
'link_url',
|
'link_url',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExportTemplateCSVForm(CSVModelForm):
|
class ExportTemplateCSVForm(CSVModelForm):
|
||||||
content_type = CSVContentTypeField(
|
content_types = CSVMultipleContentTypeField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
limit_choices_to=FeatureQuery('export_templates'),
|
limit_choices_to=FeatureQuery('export_templates'),
|
||||||
help_text="Assigned object type"
|
help_text="One or more assigned object types"
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ExportTemplate
|
model = ExportTemplate
|
||||||
fields = (
|
fields = (
|
||||||
'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment', 'template_code',
|
'name', 'content_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'template_code',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -121,9 +121,9 @@ class JobResultFilterForm(FilterForm):
|
|||||||
class CustomLinkFilterForm(FilterForm):
|
class CustomLinkFilterForm(FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q',)),
|
(None, ('q',)),
|
||||||
('Attributes', ('content_type', 'enabled', 'new_window', 'weight')),
|
('Attributes', ('content_types', 'enabled', 'new_window', 'weight')),
|
||||||
)
|
)
|
||||||
content_type = ContentTypeChoiceField(
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
limit_choices_to=FeatureQuery('custom_links'),
|
limit_choices_to=FeatureQuery('custom_links'),
|
||||||
required=False
|
required=False
|
||||||
@ -148,9 +148,9 @@ class CustomLinkFilterForm(FilterForm):
|
|||||||
class ExportTemplateFilterForm(FilterForm):
|
class ExportTemplateFilterForm(FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q',)),
|
(None, ('q',)),
|
||||||
('Attributes', ('content_type', 'mime_type', 'file_extension', 'as_attachment')),
|
('Attributes', ('content_types', 'mime_type', 'file_extension', 'as_attachment')),
|
||||||
)
|
)
|
||||||
content_type = ContentTypeChoiceField(
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
limit_choices_to=FeatureQuery('export_templates'),
|
limit_choices_to=FeatureQuery('export_templates'),
|
||||||
required=False
|
required=False
|
||||||
|
@ -63,13 +63,13 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
||||||
content_type = ContentTypeChoiceField(
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
limit_choices_to=FeatureQuery('custom_links')
|
limit_choices_to=FeatureQuery('custom_links')
|
||||||
)
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Custom Link', ('name', 'content_type', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')),
|
('Custom Link', ('name', 'content_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')),
|
||||||
('Templates', ('link_text', 'link_url')),
|
('Templates', ('link_text', 'link_url')),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -89,13 +89,13 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class ExportTemplateForm(BootstrapMixin, forms.ModelForm):
|
class ExportTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
content_type = ContentTypeChoiceField(
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
limit_choices_to=FeatureQuery('export_templates')
|
limit_choices_to=FeatureQuery('export_templates')
|
||||||
)
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Export Template', ('name', 'content_type', 'description')),
|
('Export Template', ('name', 'content_types', 'description')),
|
||||||
('Template', ('template_code',)),
|
('Template', ('template_code',)),
|
||||||
('Rendering', ('mime_type', 'file_extension', 'as_attachment')),
|
('Rendering', ('mime_type', 'file_extension', 'as_attachment')),
|
||||||
)
|
)
|
@ -35,7 +35,7 @@ class CustomLinkType(ObjectType):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.CustomLink
|
model = models.CustomLink
|
||||||
fields = '__all__'
|
exclude = ('content_types', )
|
||||||
filterset_class = filtersets.CustomLinkFilterSet
|
filterset_class = filtersets.CustomLinkFilterSet
|
||||||
|
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ class ExportTemplateType(ObjectType):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.ExportTemplate
|
model = models.ExportTemplate
|
||||||
fields = '__all__'
|
exclude = ('content_types', )
|
||||||
filterset_class = filtersets.ExportTemplateFilterSet
|
filterset_class = filtersets.ExportTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
32
netbox/extras/migrations/0081_customlink_content_types.py
Normal file
32
netbox/extras/migrations/0081_customlink_content_types.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def copy_content_types(apps, schema_editor):
|
||||||
|
CustomLink = apps.get_model('extras', 'CustomLink')
|
||||||
|
|
||||||
|
for customlink in CustomLink.objects.all():
|
||||||
|
customlink.content_types.set([customlink.content_type])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('extras', '0080_search'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customlink',
|
||||||
|
name='content_types',
|
||||||
|
field=models.ManyToManyField(related_name='custom_links', to='contenttypes.contenttype'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=copy_content_types,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='customlink',
|
||||||
|
name='content_type',
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,40 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def copy_content_types(apps, schema_editor):
|
||||||
|
ExportTemplate = apps.get_model('extras', 'ExportTemplate')
|
||||||
|
|
||||||
|
for et in ExportTemplate.objects.all():
|
||||||
|
et.content_types.set([et.content_type])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('extras', '0081_customlink_content_types'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='exporttemplate',
|
||||||
|
name='content_types',
|
||||||
|
field=models.ManyToManyField(related_name='export_templates', to='contenttypes.contenttype'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=copy_content_types,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name='exporttemplate',
|
||||||
|
name='extras_exporttemplate_unique_content_type_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='exporttemplate',
|
||||||
|
name='content_type',
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='exporttemplate',
|
||||||
|
options={'ordering': ('name',)},
|
||||||
|
),
|
||||||
|
]
|
@ -197,10 +197,10 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogged
|
|||||||
A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
|
A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
|
||||||
code to be rendered with an object as context.
|
code to be rendered with an object as context.
|
||||||
"""
|
"""
|
||||||
content_type = models.ForeignKey(
|
content_types = models.ManyToManyField(
|
||||||
to=ContentType,
|
to=ContentType,
|
||||||
on_delete=models.CASCADE,
|
related_name='custom_links',
|
||||||
limit_choices_to=FeatureQuery('custom_links')
|
help_text='The object type(s) to which this link applies.'
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
@ -236,7 +236,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogged
|
|||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'content_type', 'enabled', 'weight', 'group_name', 'button_class', 'new_window',
|
'enabled', 'weight', 'group_name', 'button_class', 'new_window',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -268,10 +268,10 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogged
|
|||||||
|
|
||||||
|
|
||||||
class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||||
content_type = models.ForeignKey(
|
content_types = models.ManyToManyField(
|
||||||
to=ContentType,
|
to=ContentType,
|
||||||
on_delete=models.CASCADE,
|
related_name='export_templates',
|
||||||
limit_choices_to=FeatureQuery('export_templates')
|
help_text='The object type(s) to which this template applies.'
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=100
|
max_length=100
|
||||||
@ -301,16 +301,10 @@ class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['content_type', 'name']
|
ordering = ('name',)
|
||||||
constraints = (
|
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=('content_type', 'name'),
|
|
||||||
name='%(app_label)s_%(class)s_unique_content_type_name'
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.content_type}: {self.name}"
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('extras:exporttemplate', args=[self.pk])
|
return reverse('extras:exporttemplate', args=[self.pk])
|
||||||
|
@ -3,7 +3,6 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from extras.models import CustomLink
|
from extras.models import CustomLink
|
||||||
from utilities.utils import render_jinja2
|
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
@ -34,7 +33,7 @@ def custom_links(context, obj):
|
|||||||
Render all applicable links for the given object.
|
Render all applicable links for the given object.
|
||||||
"""
|
"""
|
||||||
content_type = ContentType.objects.get_for_model(obj)
|
content_type = ContentType.objects.get_for_model(obj)
|
||||||
custom_links = CustomLink.objects.filter(content_type=content_type, enabled=True)
|
custom_links = CustomLink.objects.filter(content_types=content_type, enabled=True)
|
||||||
if not custom_links:
|
if not custom_links:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
@ -137,21 +137,21 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
|
|||||||
brief_fields = ['display', 'id', 'name', 'url']
|
brief_fields = ['display', 'id', 'name', 'url']
|
||||||
create_data = [
|
create_data = [
|
||||||
{
|
{
|
||||||
'content_type': 'dcim.site',
|
'content_types': ['dcim.site'],
|
||||||
'name': 'Custom Link 4',
|
'name': 'Custom Link 4',
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
'link_text': 'Link 4',
|
'link_text': 'Link 4',
|
||||||
'link_url': 'http://example.com/?4',
|
'link_url': 'http://example.com/?4',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content_type': 'dcim.site',
|
'content_types': ['dcim.site'],
|
||||||
'name': 'Custom Link 5',
|
'name': 'Custom Link 5',
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
'link_text': 'Link 5',
|
'link_text': 'Link 5',
|
||||||
'link_url': 'http://example.com/?5',
|
'link_url': 'http://example.com/?5',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content_type': 'dcim.site',
|
'content_types': ['dcim.site'],
|
||||||
'name': 'Custom Link 6',
|
'name': 'Custom Link 6',
|
||||||
'enabled': False,
|
'enabled': False,
|
||||||
'link_text': 'Link 6',
|
'link_text': 'Link 6',
|
||||||
@ -169,21 +169,18 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
custom_links = (
|
custom_links = (
|
||||||
CustomLink(
|
CustomLink(
|
||||||
content_type=site_ct,
|
|
||||||
name='Custom Link 1',
|
name='Custom Link 1',
|
||||||
enabled=True,
|
enabled=True,
|
||||||
link_text='Link 1',
|
link_text='Link 1',
|
||||||
link_url='http://example.com/?1',
|
link_url='http://example.com/?1',
|
||||||
),
|
),
|
||||||
CustomLink(
|
CustomLink(
|
||||||
content_type=site_ct,
|
|
||||||
name='Custom Link 2',
|
name='Custom Link 2',
|
||||||
enabled=True,
|
enabled=True,
|
||||||
link_text='Link 2',
|
link_text='Link 2',
|
||||||
link_url='http://example.com/?2',
|
link_url='http://example.com/?2',
|
||||||
),
|
),
|
||||||
CustomLink(
|
CustomLink(
|
||||||
content_type=site_ct,
|
|
||||||
name='Custom Link 3',
|
name='Custom Link 3',
|
||||||
enabled=False,
|
enabled=False,
|
||||||
link_text='Link 3',
|
link_text='Link 3',
|
||||||
@ -191,6 +188,8 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
CustomLink.objects.bulk_create(custom_links)
|
CustomLink.objects.bulk_create(custom_links)
|
||||||
|
for i, custom_link in enumerate(custom_links):
|
||||||
|
custom_link.content_types.set([site_ct])
|
||||||
|
|
||||||
|
|
||||||
class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||||
@ -198,17 +197,17 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
brief_fields = ['display', 'id', 'name', 'url']
|
brief_fields = ['display', 'id', 'name', 'url']
|
||||||
create_data = [
|
create_data = [
|
||||||
{
|
{
|
||||||
'content_type': 'dcim.device',
|
'content_types': ['dcim.device'],
|
||||||
'name': 'Test Export Template 4',
|
'name': 'Test Export Template 4',
|
||||||
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content_type': 'dcim.device',
|
'content_types': ['dcim.device'],
|
||||||
'name': 'Test Export Template 5',
|
'name': 'Test Export Template 5',
|
||||||
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content_type': 'dcim.device',
|
'content_types': ['dcim.device'],
|
||||||
'name': 'Test Export Template 6',
|
'name': 'Test Export Template 6',
|
||||||
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
||||||
},
|
},
|
||||||
@ -219,26 +218,23 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
ct = ContentType.objects.get_for_model(Device)
|
|
||||||
|
|
||||||
export_templates = (
|
export_templates = (
|
||||||
ExportTemplate(
|
ExportTemplate(
|
||||||
content_type=ct,
|
|
||||||
name='Export Template 1',
|
name='Export Template 1',
|
||||||
template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}'
|
template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}'
|
||||||
),
|
),
|
||||||
ExportTemplate(
|
ExportTemplate(
|
||||||
content_type=ct,
|
|
||||||
name='Export Template 2',
|
name='Export Template 2',
|
||||||
template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}'
|
template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}'
|
||||||
),
|
),
|
||||||
ExportTemplate(
|
ExportTemplate(
|
||||||
content_type=ct,
|
|
||||||
name='Export Template 3',
|
name='Export Template 3',
|
||||||
template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}'
|
template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
ExportTemplate.objects.bulk_create(export_templates)
|
ExportTemplate.objects.bulk_create(export_templates)
|
||||||
|
for et in export_templates:
|
||||||
|
et.content_types.set([ContentType.objects.get_for_model(Device)])
|
||||||
|
|
||||||
|
|
||||||
class TagTest(APIViewTestCases.APIViewTestCase):
|
class TagTest(APIViewTestCases.APIViewTestCase):
|
||||||
|
@ -168,7 +168,6 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
|||||||
custom_links = (
|
custom_links = (
|
||||||
CustomLink(
|
CustomLink(
|
||||||
name='Custom Link 1',
|
name='Custom Link 1',
|
||||||
content_type=content_types[0],
|
|
||||||
enabled=True,
|
enabled=True,
|
||||||
weight=100,
|
weight=100,
|
||||||
new_window=False,
|
new_window=False,
|
||||||
@ -177,7 +176,6 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
|||||||
),
|
),
|
||||||
CustomLink(
|
CustomLink(
|
||||||
name='Custom Link 2',
|
name='Custom Link 2',
|
||||||
content_type=content_types[1],
|
|
||||||
enabled=True,
|
enabled=True,
|
||||||
weight=200,
|
weight=200,
|
||||||
new_window=False,
|
new_window=False,
|
||||||
@ -186,7 +184,6 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
|||||||
),
|
),
|
||||||
CustomLink(
|
CustomLink(
|
||||||
name='Custom Link 3',
|
name='Custom Link 3',
|
||||||
content_type=content_types[2],
|
|
||||||
enabled=False,
|
enabled=False,
|
||||||
weight=300,
|
weight=300,
|
||||||
new_window=True,
|
new_window=True,
|
||||||
@ -195,13 +192,17 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
CustomLink.objects.bulk_create(custom_links)
|
CustomLink.objects.bulk_create(custom_links)
|
||||||
|
for i, custom_link in enumerate(custom_links):
|
||||||
|
custom_link.content_types.set([content_types[i]])
|
||||||
|
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
params = {'name': ['Custom Link 1', 'Custom Link 2']}
|
params = {'name': ['Custom Link 1', 'Custom Link 2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_content_type(self):
|
def test_content_types(self):
|
||||||
params = {'content_type': ContentType.objects.get(model='site').pk}
|
params = {'content_types': 'dcim.site'}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
params = {'content_type_id': [ContentType.objects.get_for_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_weight(self):
|
def test_weight(self):
|
||||||
@ -227,22 +228,25 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
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', description='foobar1'),
|
ExportTemplate(name='Export Template 1', template_code='TESTING', description='foobar1'),
|
||||||
ExportTemplate(name='Export Template 2', content_type=content_types[1], template_code='TESTING', description='foobar2'),
|
ExportTemplate(name='Export Template 2', template_code='TESTING', description='foobar2'),
|
||||||
ExportTemplate(name='Export Template 3', content_type=content_types[2], template_code='TESTING'),
|
ExportTemplate(name='Export Template 3', template_code='TESTING'),
|
||||||
)
|
)
|
||||||
ExportTemplate.objects.bulk_create(export_templates)
|
ExportTemplate.objects.bulk_create(export_templates)
|
||||||
|
for i, et in enumerate(export_templates):
|
||||||
|
et.content_types.set([content_types[i]])
|
||||||
|
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
params = {'name': ['Export Template 1', 'Export Template 2']}
|
params = {'name': ['Export Template 1', 'Export Template 2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_content_type(self):
|
def test_content_types(self):
|
||||||
params = {'content_type': ContentType.objects.get(model='site').pk}
|
params = {'content_types': 'dcim.site'}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
params = {'content_type_id': [ContentType.objects.get_for_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):
|
def test_description(self):
|
||||||
|
@ -66,18 +66,19 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
site_ct = ContentType.objects.get_for_model(Site)
|
site_ct = ContentType.objects.get_for_model(Site)
|
||||||
custom_links = (
|
custom_links = (
|
||||||
CustomLink(name='Custom Link 1', content_type=site_ct, enabled=True, link_text='Link 1', link_url='http://example.com/?1'),
|
CustomLink(name='Custom Link 1', enabled=True, link_text='Link 1', link_url='http://example.com/?1'),
|
||||||
CustomLink(name='Custom Link 2', content_type=site_ct, enabled=True, link_text='Link 2', link_url='http://example.com/?2'),
|
CustomLink(name='Custom Link 2', enabled=True, link_text='Link 2', link_url='http://example.com/?2'),
|
||||||
CustomLink(name='Custom Link 3', content_type=site_ct, enabled=False, link_text='Link 3', link_url='http://example.com/?3'),
|
CustomLink(name='Custom Link 3', enabled=False, link_text='Link 3', link_url='http://example.com/?3'),
|
||||||
)
|
)
|
||||||
CustomLink.objects.bulk_create(custom_links)
|
CustomLink.objects.bulk_create(custom_links)
|
||||||
|
for i, custom_link in enumerate(custom_links):
|
||||||
|
custom_link.content_types.set([site_ct])
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Custom Link X',
|
'name': 'Custom Link X',
|
||||||
'content_type': site_ct.pk,
|
'content_types': [site_ct.pk],
|
||||||
'enabled': False,
|
'enabled': False,
|
||||||
'weight': 100,
|
'weight': 100,
|
||||||
'button_class': CustomLinkButtonClassChoices.DEFAULT,
|
'button_class': CustomLinkButtonClassChoices.DEFAULT,
|
||||||
@ -86,7 +87,7 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"name,content_type,enabled,weight,button_class,link_text,link_url",
|
"name,content_types,enabled,weight,button_class,link_text,link_url",
|
||||||
"Custom Link 4,dcim.site,True,100,blue,Link 4,http://exmaple.com/?4",
|
"Custom Link 4,dcim.site,True,100,blue,Link 4,http://exmaple.com/?4",
|
||||||
"Custom Link 5,dcim.site,True,100,blue,Link 5,http://exmaple.com/?5",
|
"Custom Link 5,dcim.site,True,100,blue,Link 5,http://exmaple.com/?5",
|
||||||
"Custom Link 6,dcim.site,False,100,blue,Link 6,http://exmaple.com/?6",
|
"Custom Link 6,dcim.site,False,100,blue,Link 6,http://exmaple.com/?6",
|
||||||
@ -111,25 +112,26 @@ class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
site_ct = ContentType.objects.get_for_model(Site)
|
site_ct = ContentType.objects.get_for_model(Site)
|
||||||
TEMPLATE_CODE = """{% for object in queryset %}{{ object }}{% endfor %}"""
|
TEMPLATE_CODE = """{% for object in queryset %}{{ object }}{% endfor %}"""
|
||||||
|
|
||||||
export_templates = (
|
export_templates = (
|
||||||
ExportTemplate(name='Export Template 1', content_type=site_ct, template_code=TEMPLATE_CODE),
|
ExportTemplate(name='Export Template 1', template_code=TEMPLATE_CODE),
|
||||||
ExportTemplate(name='Export Template 2', content_type=site_ct, template_code=TEMPLATE_CODE),
|
ExportTemplate(name='Export Template 2', template_code=TEMPLATE_CODE),
|
||||||
ExportTemplate(name='Export Template 3', content_type=site_ct, template_code=TEMPLATE_CODE),
|
ExportTemplate(name='Export Template 3', template_code=TEMPLATE_CODE),
|
||||||
)
|
)
|
||||||
ExportTemplate.objects.bulk_create(export_templates)
|
ExportTemplate.objects.bulk_create(export_templates)
|
||||||
|
for et in export_templates:
|
||||||
|
et.content_types.set([site_ct])
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Export Template X',
|
'name': 'Export Template X',
|
||||||
'content_type': site_ct.pk,
|
'content_types': [site_ct.pk],
|
||||||
'template_code': TEMPLATE_CODE,
|
'template_code': TEMPLATE_CODE,
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"name,content_type,template_code",
|
"name,content_types,template_code",
|
||||||
f"Export Template 4,dcim.site,{TEMPLATE_CODE}",
|
f"Export Template 4,dcim.site,{TEMPLATE_CODE}",
|
||||||
f"Export Template 5,dcim.site,{TEMPLATE_CODE}",
|
f"Export Template 5,dcim.site,{TEMPLATE_CODE}",
|
||||||
f"Export Template 6,dcim.site,{TEMPLATE_CODE}",
|
f"Export Template 6,dcim.site,{TEMPLATE_CODE}",
|
||||||
@ -366,13 +368,13 @@ class CustomLinkTest(TestCase):
|
|||||||
|
|
||||||
def test_view_object_with_custom_link(self):
|
def test_view_object_with_custom_link(self):
|
||||||
customlink = CustomLink(
|
customlink = CustomLink(
|
||||||
content_type=ContentType.objects.get_for_model(Site),
|
|
||||||
name='Test',
|
name='Test',
|
||||||
link_text='FOO {{ obj.name }} BAR',
|
link_text='FOO {{ obj.name }} BAR',
|
||||||
link_url='http://example.com/?site={{ obj.slug }}',
|
link_url='http://example.com/?site={{ obj.slug }}',
|
||||||
new_window=False
|
new_window=False
|
||||||
)
|
)
|
||||||
customlink.save()
|
customlink.save()
|
||||||
|
customlink.content_types.set([ContentType.objects.get_for_model(Site)])
|
||||||
|
|
||||||
site = Site(name='Test Site', slug='test-site')
|
site = Site(name='Test Site', slug='test-site')
|
||||||
site.save()
|
site.save()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from .models import *
|
from .model_forms import *
|
||||||
from .filtersets import *
|
from .filtersets import *
|
||||||
from .bulk_create import *
|
from .bulk_create import *
|
||||||
from .bulk_edit import *
|
from .bulk_edit import *
|
||||||
|
@ -3,14 +3,12 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup
|
from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup
|
||||||
from extras.models import Tag
|
|
||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.constants import *
|
from ipam.constants import *
|
||||||
from ipam.formfields import IPNetworkFormField
|
from ipam.formfields import IPNetworkFormField
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
from tenancy.models import Tenant
|
|
||||||
from utilities.exceptions import PermissionsViolation
|
from utilities.exceptions import PermissionsViolation
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, BootstrapMixin, ContentTypeChoiceField, DatePicker, DynamicModelChoiceField,
|
add_blank_choice, BootstrapMixin, ContentTypeChoiceField, DatePicker, DynamicModelChoiceField,
|
||||||
@ -552,6 +550,7 @@ class FHRPGroupForm(NetBoxModelForm):
|
|||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
instance = super().save(*args, **kwargs)
|
instance = super().save(*args, **kwargs)
|
||||||
|
user = getattr(instance, '_user', None) # Set under FHRPGroupEditView.alter_object()
|
||||||
|
|
||||||
# Check if we need to create a new IPAddress for the group
|
# Check if we need to create a new IPAddress for the group
|
||||||
if self.cleaned_data.get('ip_address'):
|
if self.cleaned_data.get('ip_address'):
|
||||||
@ -565,7 +564,7 @@ class FHRPGroupForm(NetBoxModelForm):
|
|||||||
ipaddress.save()
|
ipaddress.save()
|
||||||
|
|
||||||
# Check that the new IPAddress conforms with any assigned object-level permissions
|
# Check that the new IPAddress conforms with any assigned object-level permissions
|
||||||
if not IPAddress.objects.filter(pk=ipaddress.pk).first():
|
if not IPAddress.objects.restrict(user, 'add').filter(pk=ipaddress.pk).first():
|
||||||
raise PermissionsViolation()
|
raise PermissionsViolation()
|
||||||
|
|
||||||
return instance
|
return instance
|
@ -375,7 +375,7 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
)
|
)
|
||||||
assigned = columns.BooleanColumn(
|
assigned = columns.BooleanColumn(
|
||||||
accessor='assigned_object_id',
|
accessor='assigned_object_id',
|
||||||
linkify=True,
|
linkify=lambda record: record.assigned_object.get_absolute_url(),
|
||||||
verbose_name='Assigned'
|
verbose_name='Assigned'
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
|
@ -985,6 +985,12 @@ class FHRPGroupEditView(generic.ObjectEditView):
|
|||||||
|
|
||||||
return return_url
|
return return_url
|
||||||
|
|
||||||
|
def alter_object(self, obj, request, url_args, url_kwargs):
|
||||||
|
# Workaround to solve #10719. Capture the current user on the FHRPGroup instance so that
|
||||||
|
# we can evaluate permissions during the creation of a new IPAddress within the form.
|
||||||
|
obj._user = request.user
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(FHRPGroup, 'delete')
|
@register_model_view(FHRPGroup, 'delete')
|
||||||
class FHRPGroupDeleteView(generic.ObjectDeleteView):
|
class FHRPGroupDeleteView(generic.ObjectDeleteView):
|
||||||
|
@ -58,22 +58,24 @@ class TokenAuthentication(authentication.TokenAuthentication):
|
|||||||
if token.is_expired:
|
if token.is_expired:
|
||||||
raise exceptions.AuthenticationFailed("Token expired")
|
raise exceptions.AuthenticationFailed("Token expired")
|
||||||
|
|
||||||
if not token.user.is_active:
|
user = token.user
|
||||||
raise exceptions.AuthenticationFailed("User inactive")
|
|
||||||
|
|
||||||
# When LDAP authentication is active try to load user data from LDAP directory
|
# When LDAP authentication is active try to load user data from LDAP directory
|
||||||
if settings.REMOTE_AUTH_BACKEND == 'netbox.authentication.LDAPBackend':
|
if settings.REMOTE_AUTH_BACKEND == 'netbox.authentication.LDAPBackend':
|
||||||
from netbox.authentication import LDAPBackend
|
from netbox.authentication import LDAPBackend
|
||||||
ldap_backend = LDAPBackend()
|
ldap_backend = LDAPBackend()
|
||||||
|
|
||||||
# Load from LDAP if FIND_GROUP_PERMS is active
|
# Load from LDAP if FIND_GROUP_PERMS is active
|
||||||
if ldap_backend.settings.FIND_GROUP_PERMS:
|
# Always query LDAP when user is not active, otherwise it is never activated again
|
||||||
user = ldap_backend.populate_user(token.user.username)
|
if ldap_backend.settings.FIND_GROUP_PERMS or not token.user.is_active:
|
||||||
|
ldap_user = ldap_backend.populate_user(token.user.username)
|
||||||
# If the user is found in the LDAP directory use it, if not fallback to the local user
|
# If the user is found in the LDAP directory use it, if not fallback to the local user
|
||||||
if user:
|
if ldap_user:
|
||||||
return user, token
|
user = ldap_user
|
||||||
|
|
||||||
return token.user, token
|
if not user.is_active:
|
||||||
|
raise exceptions.AuthenticationFailed("User inactive")
|
||||||
|
|
||||||
|
return user, token
|
||||||
|
|
||||||
|
|
||||||
class TokenPermissions(DjangoObjectPermissions):
|
class TokenPermissions(DjangoObjectPermissions):
|
||||||
|
@ -108,6 +108,5 @@ class ObjectValidationMixin:
|
|||||||
conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count()
|
conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count()
|
||||||
if conforming_count != len(instance):
|
if conforming_count != len(instance):
|
||||||
raise ObjectDoesNotExist
|
raise ObjectDoesNotExist
|
||||||
else:
|
elif not self.queryset.filter(pk=instance.pk).exists():
|
||||||
# Check that the instance is matched by the view's queryset
|
raise ObjectDoesNotExist
|
||||||
self.queryset.get(pk=instance.pk)
|
|
||||||
|
@ -79,6 +79,7 @@ CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
|
|||||||
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
|
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
|
||||||
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
|
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
|
||||||
CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken')
|
CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken')
|
||||||
|
CSRF_COOKIE_PATH = BASE_PATH or '/'
|
||||||
CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', [])
|
CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', [])
|
||||||
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
|
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
|
||||||
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
|
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
|
||||||
@ -124,6 +125,8 @@ SENTRY_TRACES_SAMPLE_RATE = getattr(configuration, 'SENTRY_TRACES_SAMPLE_RATE',
|
|||||||
SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {})
|
SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {})
|
||||||
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
|
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
|
||||||
SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid')
|
SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid')
|
||||||
|
SESSION_COOKIE_PATH = BASE_PATH or '/'
|
||||||
|
LANGUAGE_COOKIE_PATH = BASE_PATH or '/'
|
||||||
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
|
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
|
||||||
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
||||||
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
|
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
|
||||||
|
@ -191,7 +191,7 @@ class NetBoxTable(BaseTable):
|
|||||||
extra_columns.extend([
|
extra_columns.extend([
|
||||||
(f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields
|
(f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields
|
||||||
])
|
])
|
||||||
custom_links = CustomLink.objects.filter(content_type=content_type, enabled=True)
|
custom_links = CustomLink.objects.filter(content_types=content_type, enabled=True)
|
||||||
extra_columns.extend([
|
extra_columns.extend([
|
||||||
(f'cl_{cl.name}', columns.CustomLinkColumn(cl)) for cl in custom_links
|
(f'cl_{cl.name}', columns.CustomLinkColumn(cl)) for cl in custom_links
|
||||||
])
|
])
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
@ -8,6 +9,7 @@ from django.http import HttpResponseServerError
|
|||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.template import loader
|
from django.template import loader
|
||||||
from django.template.exceptions import TemplateDoesNotExist
|
from django.template.exceptions import TemplateDoesNotExist
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
from django.views.decorators.csrf import requires_csrf_token
|
from django.views.decorators.csrf import requires_csrf_token
|
||||||
from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found
|
from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
@ -26,102 +28,91 @@ from netbox.forms import SearchForm
|
|||||||
from netbox.search import LookupTypes
|
from netbox.search import LookupTypes
|
||||||
from netbox.search.backends import search_backend
|
from netbox.search.backends import search_backend
|
||||||
from netbox.tables import SearchTable
|
from netbox.tables import SearchTable
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Contact, Tenant
|
||||||
from utilities.htmx import is_htmx
|
from utilities.htmx import is_htmx
|
||||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||||
from virtualization.models import Cluster, VirtualMachine
|
from virtualization.models import Cluster, VirtualMachine
|
||||||
from wireless.models import WirelessLAN, WirelessLink
|
from wireless.models import WirelessLAN, WirelessLink
|
||||||
|
|
||||||
|
Link = namedtuple('Link', ('label', 'viewname', 'permission', 'count'))
|
||||||
|
|
||||||
|
|
||||||
class HomeView(View):
|
class HomeView(View):
|
||||||
template_name = 'home.html'
|
template_name = 'home.html'
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
if settings.LOGIN_REQUIRED and not request.user.is_authenticated:
|
if settings.LOGIN_REQUIRED and not request.user.is_authenticated:
|
||||||
return redirect("login")
|
return redirect('login')
|
||||||
|
|
||||||
connected_consoleports = ConsolePort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
|
console_connections = ConsolePort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
|
||||||
_path__is_complete=True
|
_path__is_complete=True
|
||||||
)
|
).count
|
||||||
connected_powerports = PowerPort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
|
power_connections = PowerPort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
|
||||||
_path__is_complete=True
|
_path__is_complete=True
|
||||||
)
|
).count
|
||||||
connected_interfaces = Interface.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
|
interface_connections = Interface.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
|
||||||
_path__is_complete=True
|
_path__is_complete=True
|
||||||
)
|
).count
|
||||||
|
|
||||||
|
def get_count_queryset(model):
|
||||||
|
return model.objects.restrict(request.user, 'view').count
|
||||||
|
|
||||||
def build_stats():
|
def build_stats():
|
||||||
org = (
|
org = (
|
||||||
("dcim.view_site", "Sites", Site.objects.restrict(request.user, 'view').count),
|
Link(_('Sites'), 'dcim:site_list', 'dcim.view_site', get_count_queryset(Site)),
|
||||||
("tenancy.view_tenant", "Tenants", Tenant.objects.restrict(request.user, 'view').count),
|
Link(_('Tenants'), 'tenancy:tenant_list', 'tenancy.view_tenant', get_count_queryset(Tenant)),
|
||||||
|
Link(_('Contacts'), 'tenancy:contact_list', 'tenancy.view_contact', get_count_queryset(Contact)),
|
||||||
)
|
)
|
||||||
dcim = (
|
dcim = (
|
||||||
("dcim.view_rack", "Racks", Rack.objects.restrict(request.user, 'view').count),
|
Link(_('Racks'), 'dcim:rack_list', 'dcim.view_rack', get_count_queryset(Rack)),
|
||||||
("dcim.view_devicetype", "Device Types", DeviceType.objects.restrict(request.user, 'view').count),
|
Link(_('Device Types'), 'dcim:devicetype_list', 'dcim.view_devicetype', get_count_queryset(DeviceType)),
|
||||||
("dcim.view_device", "Devices", Device.objects.restrict(request.user, 'view').count),
|
Link(_('Devices'), 'dcim:device_list', 'dcim.view_device', get_count_queryset(Device)),
|
||||||
)
|
)
|
||||||
ipam = (
|
ipam = (
|
||||||
("ipam.view_vrf", "VRFs", VRF.objects.restrict(request.user, 'view').count),
|
Link(_('VRFs'), 'ipam:vrf_list', 'ipam.view_vrf', get_count_queryset(VRF)),
|
||||||
("ipam.view_aggregate", "Aggregates", Aggregate.objects.restrict(request.user, 'view').count),
|
Link(_('Aggregates'), 'ipam:aggregate_list', 'ipam.view_aggregate', get_count_queryset(Aggregate)),
|
||||||
("ipam.view_prefix", "Prefixes", Prefix.objects.restrict(request.user, 'view').count),
|
Link(_('Prefixes'), 'ipam:prefix_list', 'ipam.view_prefix', get_count_queryset(Prefix)),
|
||||||
("ipam.view_iprange", "IP Ranges", IPRange.objects.restrict(request.user, 'view').count),
|
Link(_('IP Ranges'), 'ipam:iprange_list', 'ipam.view_iprange', get_count_queryset(IPRange)),
|
||||||
("ipam.view_ipaddress", "IP Addresses", IPAddress.objects.restrict(request.user, 'view').count),
|
Link(_('IP Addresses'), 'ipam:ipaddress_list', 'ipam.view_ipaddress', get_count_queryset(IPAddress)),
|
||||||
("ipam.view_vlan", "VLANs", VLAN.objects.restrict(request.user, 'view').count)
|
Link(_('VLANs'), 'ipam:vlan_list', 'ipam.view_vlan', get_count_queryset(VLAN)),
|
||||||
|
|
||||||
)
|
)
|
||||||
circuits = (
|
circuits = (
|
||||||
("circuits.view_provider", "Providers", Provider.objects.restrict(request.user, 'view').count),
|
Link(_('Providers'), 'circuits:provider_list', 'circuits.view_provider', get_count_queryset(Provider)),
|
||||||
("circuits.view_circuit", "Circuits", Circuit.objects.restrict(request.user, 'view').count),
|
Link(_('Circuits'), 'circuits:circuit_list', 'circuits.view_circuit', get_count_queryset(Circuit))
|
||||||
)
|
)
|
||||||
virtualization = (
|
virtualization = (
|
||||||
("virtualization.view_cluster", "Clusters", Cluster.objects.restrict(request.user, 'view').count),
|
Link(_('Clusters'), 'virtualization:cluster_list', 'virtualization.view_cluster',
|
||||||
("virtualization.view_virtualmachine", "Virtual Machines", VirtualMachine.objects.restrict(request.user, 'view').count),
|
get_count_queryset(Cluster)),
|
||||||
|
Link(_('Virtual Machines'), 'virtualization:virtualmachine_list', 'virtualization.view_virtualmachine',
|
||||||
|
get_count_queryset(VirtualMachine)),
|
||||||
)
|
)
|
||||||
connections = (
|
connections = (
|
||||||
("dcim.view_cable", "Cables", Cable.objects.restrict(request.user, 'view').count),
|
Link(_('Cables'), 'dcim:cable_list', 'dcim.view_cable', get_count_queryset(Cable)),
|
||||||
("dcim.view_consoleport", "Console", connected_consoleports.count),
|
Link(_('Interfaces'), 'dcim:interface_connections_list', 'dcim.view_interface', interface_connections),
|
||||||
("dcim.view_interface", "Interfaces", connected_interfaces.count),
|
Link(_('Console'), 'dcim:console_connections_list', 'dcim.view_consoleport', console_connections),
|
||||||
("dcim.view_powerport", "Power Connections", connected_powerports.count),
|
Link(_('Power'), 'dcim:power_connections_list', 'dcim.view_powerport', power_connections),
|
||||||
)
|
)
|
||||||
power = (
|
power = (
|
||||||
("dcim.view_powerpanel", "Power Panels", PowerPanel.objects.restrict(request.user, 'view').count),
|
Link(_('Power Panels'), 'dcim:powerpanel_list', 'dcim.view_powerpanel', get_count_queryset(PowerPanel)),
|
||||||
("dcim.view_powerfeed", "Power Feeds", PowerFeed.objects.restrict(request.user, 'view').count),
|
Link(_('Power Feeds'), 'dcim:powerfeed_list', 'dcim.view_powerfeed', get_count_queryset(PowerFeed)),
|
||||||
)
|
)
|
||||||
wireless = (
|
wireless = (
|
||||||
("wireless.view_wirelesslan", "Wireless LANs", WirelessLAN.objects.restrict(request.user, 'view').count),
|
Link(_('Wireless LANs'), 'wireless:wirelesslan_list', 'wireless.view_wirelesslan',
|
||||||
("wireless.view_wirelesslink", "Wireless Links", WirelessLink.objects.restrict(request.user, 'view').count),
|
get_count_queryset(WirelessLAN)),
|
||||||
|
Link(_('Wireless Links'), 'wireless:wirelesslink_list', 'wireless.view_wirelesslink',
|
||||||
|
get_count_queryset(WirelessLink)),
|
||||||
)
|
)
|
||||||
sections = (
|
stats = (
|
||||||
("Organization", org, "domain"),
|
(_('Organization'), org, 'domain'),
|
||||||
("IPAM", ipam, "counter"),
|
(_('IPAM'), ipam, 'counter'),
|
||||||
("Virtualization", virtualization, "monitor"),
|
(_('Virtualization'), virtualization, 'monitor'),
|
||||||
("Inventory", dcim, "server"),
|
(_('Inventory'), dcim, 'server'),
|
||||||
("Circuits", circuits, "transit-connection-variant"),
|
(_('Circuits'), circuits, 'transit-connection-variant'),
|
||||||
("Connections", connections, "cable-data"),
|
(_('Connections'), connections, 'cable-data'),
|
||||||
("Power", power, "flash"),
|
(_('Power'), power, 'flash'),
|
||||||
("Wireless", wireless, "wifi"),
|
(_('Wireless'), wireless, 'wifi'),
|
||||||
)
|
)
|
||||||
|
|
||||||
stats = []
|
|
||||||
for section_label, section_items, icon_class in sections:
|
|
||||||
items = []
|
|
||||||
for perm, item_label, get_count in section_items:
|
|
||||||
app, scope = perm.split(".")
|
|
||||||
url = ":".join((app, scope.replace("view_", "") + "_list"))
|
|
||||||
item = {
|
|
||||||
"label": item_label,
|
|
||||||
"count": None,
|
|
||||||
"url": url,
|
|
||||||
"disabled": True,
|
|
||||||
"icon": icon_class,
|
|
||||||
}
|
|
||||||
if request.user.has_perm(perm):
|
|
||||||
item["count"] = get_count()
|
|
||||||
item["disabled"] = False
|
|
||||||
items.append(item)
|
|
||||||
stats.append((section_label, items, icon_class))
|
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
# Compile changelog table
|
# Compile changelog table
|
||||||
|
@ -1,18 +1,40 @@
|
|||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from utilities.views import ObjectPermissionRequiredMixin
|
from utilities.views import ObjectPermissionRequiredMixin
|
||||||
|
|
||||||
|
|
||||||
class BaseObjectView(ObjectPermissionRequiredMixin, View):
|
class BaseView(ObjectPermissionRequiredMixin, View):
|
||||||
|
queryset = None
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.queryset = self.get_queryset(request)
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
"""
|
"""
|
||||||
Base view class for reusable generic views.
|
Return the base queryset for the view. By default, this returns self.queryset.all().
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The current request
|
||||||
|
"""
|
||||||
|
if self.queryset is None:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
f"{self.__class__.__name__} does not define a queryset. Set queryset on the class or "
|
||||||
|
f"override its get_queryset() method."
|
||||||
|
)
|
||||||
|
return self.queryset.all()
|
||||||
|
|
||||||
|
|
||||||
|
class BaseObjectView(BaseView):
|
||||||
|
"""
|
||||||
|
Base class for generic views which display or manipulate a single object.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
queryset: Django QuerySet from which the object(s) will be fetched
|
queryset: Django QuerySet from which the object(s) will be fetched
|
||||||
template_name: The name of the HTML template file to render
|
template_name: The name of the HTML template file to render
|
||||||
"""
|
"""
|
||||||
queryset = None
|
|
||||||
template_name = None
|
template_name = None
|
||||||
|
|
||||||
def get_object(self, **kwargs):
|
def get_object(self, **kwargs):
|
||||||
@ -35,16 +57,15 @@ class BaseObjectView(ObjectPermissionRequiredMixin, View):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class BaseMultiObjectView(ObjectPermissionRequiredMixin, View):
|
class BaseMultiObjectView(BaseView):
|
||||||
"""
|
"""
|
||||||
Base view class for reusable generic views.
|
Base class for generic views which display or manipulate multiple objects.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
queryset: Django QuerySet from which the object(s) will be fetched
|
queryset: Django QuerySet from which the object(s) will be fetched
|
||||||
table: The django-tables2 Table class used to render the objects list
|
table: The django-tables2 Table class used to render the objects list
|
||||||
template_name: The name of the HTML template file to render
|
template_name: The name of the HTML template file to render
|
||||||
"""
|
"""
|
||||||
queryset = None
|
|
||||||
table = None
|
table = None
|
||||||
template_name = None
|
template_name = None
|
||||||
|
|
||||||
|
@ -142,7 +142,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
|||||||
|
|
||||||
# Render an ExportTemplate
|
# Render an ExportTemplate
|
||||||
elif request.GET['export']:
|
elif request.GET['export']:
|
||||||
template = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export'])
|
template = get_object_or_404(ExportTemplate, content_types=content_type, name=request.GET['export'])
|
||||||
return self.export_template(template, request)
|
return self.export_template(template, request)
|
||||||
|
|
||||||
# Check for YAML export support on the model
|
# Check for YAML export support on the model
|
||||||
@ -335,7 +335,6 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
|
|
||||||
ids = [int(record["id"]) for record in records]
|
ids = [int(record["id"]) for record in records]
|
||||||
qs = self.queryset.model.objects.filter(id__in=ids)
|
qs = self.queryset.model.objects.filter(id__in=ids)
|
||||||
print(qs)
|
|
||||||
objs = {}
|
objs = {}
|
||||||
for obj in qs:
|
for obj in qs:
|
||||||
objs[obj.id] = obj
|
objs[obj.id] = obj
|
||||||
|
@ -179,7 +179,7 @@ class ObjectImportView(GetReturnURLMixin, BaseObjectView):
|
|||||||
obj = model_form.save()
|
obj = model_form.save()
|
||||||
|
|
||||||
# Enforce object-level permissions
|
# Enforce object-level permissions
|
||||||
if not self.queryset.filter(pk=obj.pk).first():
|
if not self.queryset.filter(pk=obj.pk).exists():
|
||||||
raise PermissionsViolation()
|
raise PermissionsViolation()
|
||||||
|
|
||||||
# Iterate through the related object forms (if any), validating and saving each instance.
|
# Iterate through the related object forms (if any), validating and saving each instance.
|
||||||
@ -396,7 +396,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
|||||||
obj = form.save()
|
obj = form.save()
|
||||||
|
|
||||||
# Check that the new object conforms with any assigned object-level permissions
|
# Check that the new object conforms with any assigned object-level permissions
|
||||||
if not self.queryset.filter(pk=obj.pk).first():
|
if not self.queryset.filter(pk=obj.pk).exists():
|
||||||
raise PermissionsViolation()
|
raise PermissionsViolation()
|
||||||
|
|
||||||
msg = '{} {}'.format(
|
msg = '{} {}'.format(
|
||||||
|
@ -178,7 +178,7 @@
|
|||||||
{% if object.primary_ip4.nat_inside %}
|
{% if object.primary_ip4.nat_inside %}
|
||||||
(NAT for <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>)
|
(NAT for <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>)
|
||||||
{% elif object.primary_ip4.nat_outside.exists %}
|
{% elif object.primary_ip4.nat_outside.exists %}
|
||||||
(NAT for {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
(NAT: {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ ''|placeholder }}
|
{{ ''|placeholder }}
|
||||||
@ -193,7 +193,7 @@
|
|||||||
{% if object.primary_ip6.nat_inside %}
|
{% if object.primary_ip6.nat_inside %}
|
||||||
(NAT for <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>)
|
(NAT for <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>)
|
||||||
{% elif object.primary_ip6.nat_outside.exists %}
|
{% elif object.primary_ip6.nat_outside.exists %}
|
||||||
(NAT for {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
(NAT: {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ ''|placeholder }}
|
{{ ''|placeholder }}
|
||||||
|
@ -6,19 +6,13 @@
|
|||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col col-md-5">
|
<div class="col col-md-5">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">Custom Link</h5>
|
||||||
Custom Link
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Name</th>
|
<th scope="row">Name</th>
|
||||||
<td>{{ object.name }}</td>
|
<td>{{ object.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th scope="row">Content Type</th>
|
|
||||||
<td>{{ object.content_type }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Enabled</th>
|
<th scope="row">Enabled</th>
|
||||||
<td>{% checkmark object.enabled %}</td>
|
<td>{% checkmark object.enabled %}</td>
|
||||||
@ -42,6 +36,18 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Assigned Models</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
{% for ct in object.content_types.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ ct }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-7">
|
<div class="col col-md-7">
|
||||||
|
@ -18,10 +18,6 @@
|
|||||||
</h5>
|
</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
|
||||||
<th scope="row">Content Type</th>
|
|
||||||
<td>{{ object.content_type }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Name</th>
|
<th scope="row">Name</th>
|
||||||
<td>{{ object.name }}</td>
|
<td>{{ object.name }}</td>
|
||||||
@ -45,6 +41,18 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Assigned Models</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
{% for ct in object.content_types.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ ct }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-7">
|
<div class="col col-md-7">
|
||||||
|
@ -39,6 +39,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@ -64,6 +65,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -36,8 +36,8 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
{% if not item.disabled %}
|
{% if item.permission in perms %}
|
||||||
<a href="{% url item.url %}" class="list-group-item list-group-item-action">
|
<a href="{% url item.viewname %}" class="list-group-item list-group-item-action">
|
||||||
<div class="d-flex w-100 justify-content-between align-items-center">
|
<div class="d-flex w-100 justify-content-between align-items-center">
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
<h4 class="mb-1">{{ item.count }}</h4>
|
<h4 class="mb-1">{{ item.count }}</h4>
|
||||||
|
@ -19,6 +19,10 @@
|
|||||||
<th scope="row">Type</th>
|
<th scope="row">Type</th>
|
||||||
<td>{{ object.type|linkify }}</td>
|
<td>{{ object.type|linkify }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Status</th>
|
||||||
|
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Group</th>
|
<th scope="row">Group</th>
|
||||||
<td>{{ object.group|linkify|placeholder }}</td>
|
<td>{{ object.group|linkify|placeholder }}</td>
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
{% if object.primary_ip4.nat_inside %}
|
{% if object.primary_ip4.nat_inside %}
|
||||||
(NAT for <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>)
|
(NAT for <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>)
|
||||||
{% elif object.primary_ip4.nat_outside.exists %}
|
{% elif object.primary_ip4.nat_outside.exists %}
|
||||||
(NAT for {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
(NAT: {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ ''|placeholder }}
|
{{ ''|placeholder }}
|
||||||
@ -61,7 +61,7 @@
|
|||||||
{% if object.primary_ip6.nat_inside %}
|
{% if object.primary_ip6.nat_inside %}
|
||||||
(NAT for <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>)
|
(NAT for <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>)
|
||||||
{% elif object.primary_ip6.nat_outside.exists %}
|
{% elif object.primary_ip6.nat_outside.exists %}
|
||||||
(NAT for {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
(NAT: {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ ''|placeholder }}
|
{{ ''|placeholder }}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from .forms import *
|
from .forms import *
|
||||||
from .models import *
|
from .model_forms import *
|
||||||
from .filtersets import *
|
from .filtersets import *
|
||||||
from .bulk_edit import *
|
from .bulk_edit import *
|
||||||
from .bulk_import import *
|
from .bulk_import import *
|
||||||
|
@ -83,7 +83,7 @@ def export_button(context, model):
|
|||||||
data_format = 'YAML' if hasattr(content_type.model_class(), 'to_yaml') else 'CSV'
|
data_format = 'YAML' if hasattr(content_type.model_class(), 'to_yaml') else 'CSV'
|
||||||
|
|
||||||
# Retrieve all export templates for this model
|
# Retrieve all export templates for this model
|
||||||
export_templates = ExportTemplate.objects.restrict(user, 'view').filter(content_type=content_type)
|
export_templates = ExportTemplate.objects.restrict(user, 'view').filter(content_types=content_type)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'perms': context['perms'],
|
'perms': context['perms'],
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from .models import *
|
from .model_forms import *
|
||||||
from .filtersets import *
|
from .filtersets import *
|
||||||
from .object_create import *
|
from .object_create import *
|
||||||
from .bulk_create import *
|
from .bulk_create import *
|
||||||
|
@ -3,7 +3,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from dcim.forms.common import InterfaceCommonForm
|
from dcim.forms.common import InterfaceCommonForm
|
||||||
from dcim.forms.models import INTERFACE_MODE_HELP_TEXT
|
from dcim.forms.model_forms import INTERFACE_MODE_HELP_TEXT
|
||||||
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
|
||||||
from ipam.models import IPAddress, VLAN, VLANGroup, VRF
|
from ipam.models import IPAddress, VLAN, VLANGroup, VRF
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
@ -1,5 +1,5 @@
|
|||||||
from utilities.forms import ExpandableNameField
|
from utilities.forms import ExpandableNameField
|
||||||
from .models import VMInterfaceForm
|
from .model_forms import VMInterfaceForm
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'VMInterfaceCreateForm',
|
'VMInterfaceCreateForm',
|
||||||
|
@ -64,6 +64,7 @@ class ClusterTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
|||||||
group = tables.Column(
|
group = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
status = columns.ChoiceFieldColumn()
|
||||||
site = tables.Column(
|
site = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from .models import *
|
from .model_forms import *
|
||||||
from .filtersets import *
|
from .filtersets import *
|
||||||
from .bulk_edit import *
|
from .bulk_edit import *
|
||||||
from .bulk_import import *
|
from .bulk_import import *
|
||||||
|
@ -19,18 +19,18 @@ graphene-django==3.0.0
|
|||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
Markdown==3.3.7
|
Markdown==3.3.7
|
||||||
mkdocs-material==8.5.6
|
mkdocs-material==8.5.7
|
||||||
mkdocstrings[python-legacy]==0.19.0
|
mkdocstrings[python-legacy]==0.19.0
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==9.2.0
|
Pillow==9.2.0
|
||||||
psycopg2-binary==2.9.3
|
psycopg2-binary==2.9.5
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
sentry-sdk==1.9.10
|
sentry-sdk==1.10.1
|
||||||
social-auth-app-django==5.0.0
|
social-auth-app-django==5.0.0
|
||||||
social-auth-core[openidconnect]==4.3.0
|
social-auth-core[openidconnect]==4.3.0
|
||||||
svgwrite==1.4.3
|
svgwrite==1.4.3
|
||||||
tablib==3.2.1
|
tablib==3.2.1
|
||||||
tzdata==2022.4
|
tzdata==2022.5
|
||||||
|
|
||||||
# Workaround for #7401
|
# Workaround for #7401
|
||||||
jsonschema==3.2.0
|
jsonschema==3.2.0
|
||||||
|
Loading…
Reference in New Issue
Block a user