mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-19 09:53:34 -06:00
Merge branch 'develop' into feature
This commit is contained in:
commit
2e2ff09822
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.5.1
|
placeholder: v3.5.3
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
11
.github/ISSUE_TEMPLATE/config.yml
vendored
11
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -3,10 +3,13 @@ blank_issues_enabled: false
|
|||||||
contact_links:
|
contact_links:
|
||||||
- name: 📖 Contributing Policy
|
- name: 📖 Contributing Policy
|
||||||
url: https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md
|
url: https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md
|
||||||
about: "Please read through our contributing policy before opening an issue or pull request"
|
about: "Please read through our contributing policy before opening an issue or pull request."
|
||||||
- name: ❓ Discussion
|
- name: ❓ Discussion
|
||||||
url: https://github.com/netbox-community/netbox/discussions
|
url: https://github.com/netbox-community/netbox/discussions
|
||||||
about: "If you're just looking for help, try starting a discussion instead"
|
about: "If you're just looking for help, try starting a discussion instead."
|
||||||
|
- name: 💡 Plugin Idea
|
||||||
|
url: https://plugin-ideas.netbox.dev
|
||||||
|
about: "Have an idea for a plugin? Head over to the ideas board!"
|
||||||
- name: 💬 Community Slack
|
- name: 💬 Community Slack
|
||||||
url: https://netdev.chat/
|
url: https://netdev.chat
|
||||||
about: "Join #netbox on the NetDev Community Slack for assistance with installation issues and other problems"
|
about: "Join #netbox on the NetDev Community Slack for assistance with installation issues and other problems."
|
||||||
|
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.5.1
|
placeholder: v3.5.3
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
|
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
|
||||||
|
<p>The premiere source of truth powering network automation</p>
|
||||||
The premiere source of truth powering network automation
|
<img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master" alt="CI status" />
|
||||||
|
<p></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
NetBox is the leading solution for modeling and documenting modern networks. By
|
NetBox is the leading solution for modeling and documenting modern networks. By
|
||||||
combining the traditional disciplines of IP address management (IPAM) and
|
combining the traditional disciplines of IP address management (IPAM) and
|
||||||
datacenter infrastructure management (DCIM) with powerful APIs and extensions,
|
datacenter infrastructure management (DCIM) with powerful APIs and extensions,
|
||||||
|
@ -84,7 +84,8 @@ feedparser
|
|||||||
|
|
||||||
# Django wrapper for Graphene (GraphQL support)
|
# Django wrapper for Graphene (GraphQL support)
|
||||||
# https://github.com/graphql-python/graphene-django/releases
|
# https://github.com/graphql-python/graphene-django/releases
|
||||||
graphene_django
|
# Pinned to v3.0.0 for GraphiQL UI issue (see #12762)
|
||||||
|
graphene_django==3.0.0
|
||||||
|
|
||||||
# WSGI HTTP server
|
# WSGI HTTP server
|
||||||
# https://docs.gunicorn.org/en/latest/news.html
|
# https://docs.gunicorn.org/en/latest/news.html
|
||||||
|
@ -204,3 +204,25 @@ This parameter defines the URL of the repository that will be checked for new Ne
|
|||||||
Default: `300`
|
Default: `300`
|
||||||
|
|
||||||
The maximum execution time of a background task (such as running a custom script), in seconds.
|
The maximum execution time of a background task (such as running a custom script), in seconds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RQ_RETRY_INTERVAL
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
This parameter was added in NetBox v3.5.
|
||||||
|
|
||||||
|
Default: `60`
|
||||||
|
|
||||||
|
This parameter controls how frequently a failed job is retried, up to the maximum number of times specified by `RQ_RETRY_MAX`. This must be either an integer specifying the number of seconds to wait between successive attempts, or a list of such values. For example, `[60, 300, 3600]` will retry the task after 1 minute, 5 minutes, and 1 hour.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RQ_RETRY_MAX
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
This parameter was added in NetBox v3.5.
|
||||||
|
|
||||||
|
Default: `0` (retries disabled)
|
||||||
|
|
||||||
|
The maximum number of times a background task will be retried before being marked as failed.
|
||||||
|
@ -63,7 +63,7 @@ Each attribute of the IP address is expressed as an attribute of the JSON object
|
|||||||
|
|
||||||
## Interactive Documentation
|
## Interactive Documentation
|
||||||
|
|
||||||
Comprehensive, interactive documentation of all REST API endpoints is available on a running NetBox instance at `/api/docs/`. This interface provides a convenient sandbox for researching and experimenting with specific endpoints and request types. The API itself can also be explored using a web browser by navigating to its root at `/api/`.
|
Comprehensive, interactive documentation of all REST API endpoints is available on a running NetBox instance at `/api/schema/swagger-ui/`. This interface provides a convenient sandbox for researching and experimenting with specific endpoints and request types. The API itself can also be explored using a web browser by navigating to its root at `/api/`.
|
||||||
|
|
||||||
## Endpoint Hierarchy
|
## Endpoint Hierarchy
|
||||||
|
|
||||||
|
@ -68,11 +68,12 @@ Defines how filters are evaluated against custom field values.
|
|||||||
|
|
||||||
Controls how and whether the custom field is displayed within the NetBox user interface.
|
Controls how and whether the custom field is displayed within the NetBox user interface.
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|------------|--------------------------------------|
|
|-------------------|--------------------------------------------------|
|
||||||
| Read/write | Display and permit editing (default) |
|
| Read/write | Display and permit editing (default) |
|
||||||
| Read-only | Display field but disallow editing |
|
| Read-only | Display field but disallow editing |
|
||||||
| Hidden | Do not display field in the UI |
|
| Hidden | Do not display field in the UI |
|
||||||
|
| Hidden (if unset) | Display in the UI only when a value has been set |
|
||||||
|
|
||||||
### Default
|
### Default
|
||||||
|
|
||||||
|
@ -1,10 +1,45 @@
|
|||||||
# NetBox v3.5
|
# NetBox v3.5
|
||||||
|
|
||||||
## v3.5.2 (FUTURE)
|
## v3.5.4 (FUTURE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.5.3 (2023-06-02)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#9876](https://github.com/netbox-community/netbox/issues/9876) - Improve support for matching tags in conditional rules
|
||||||
|
* [#12015](https://github.com/netbox-community/netbox/issues/12015) - Add device type & role filters for device components
|
||||||
|
* [#12470](https://github.com/netbox-community/netbox/issues/12470) - Collapse context data by default when viewing a rendered device configuration
|
||||||
|
* [#12562](https://github.com/netbox-community/netbox/issues/12562) - Record client IP address when logging authentication failures
|
||||||
|
* [#12597](https://github.com/netbox-community/netbox/issues/12597) - Add an option to hide custom fields only if unset
|
||||||
|
* [#12599](https://github.com/netbox-community/netbox/issues/12599) - Apply filter parameters to links in object count dashboard widgets
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#7503](https://github.com/netbox-community/netbox/issues/7503) - Improve rack space validation when creating multiple devices via REST API
|
||||||
|
* [#11539](https://github.com/netbox-community/netbox/issues/11539) - Fix exception when applying "empty" filter lookup with invalid value
|
||||||
|
* [#11934](https://github.com/netbox-community/netbox/issues/11934) - Prevent reassignment of an IP address designated as primary for its parent object
|
||||||
|
* [#12538](https://github.com/netbox-community/netbox/issues/12538) - Redirect user to originating view after editing/deleting an image attachment
|
||||||
|
* [#12627](https://github.com/netbox-community/netbox/issues/12627) - Restore hover preview for embedded image attachment tables
|
||||||
|
* [#12694](https://github.com/netbox-community/netbox/issues/12694) - Strip leading & trailing whitespace from custom link URL & text
|
||||||
|
* [#12702](https://github.com/netbox-community/netbox/issues/12702) - Fix sizing of rear port selection widget on front port template creation form
|
||||||
|
* [#12715](https://github.com/netbox-community/netbox/issues/12715) - Use contact assignments table to display the contacts assigned to an object
|
||||||
|
* [#12730](https://github.com/netbox-community/netbox/issues/12730) - Fix extraneous contacts listed in object contact assignments view
|
||||||
|
* [#12742](https://github.com/netbox-community/netbox/issues/12742) - Object counts dashboard widget should support URL-compatible query filters
|
||||||
|
* [#12762](https://github.com/netbox-community/netbox/issues/12762) - Fix GraphiQL UI by reverting graphene-django to earlier version
|
||||||
|
* [#12745](https://github.com/netbox-community/netbox/issues/12745) - Escape display text in API-backed selection widgets
|
||||||
|
* [#12779](https://github.com/netbox-community/netbox/issues/12779) - Correct arithmetic for converting inches to meters
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.5.2 (2023-05-22)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* [#7671](https://github.com/netbox-community/netbox/issues/7671) - Introduce `REMOTE_AUTH_AUTO_CREATE_GROUPS` config parameter to enable the automatic creation of new groups when remote authentication is in use
|
* [#7671](https://github.com/netbox-community/netbox/issues/7671) - Introduce `REMOTE_AUTH_AUTO_CREATE_GROUPS` config parameter to enable the automatic creation of new groups when remote authentication is in use
|
||||||
|
* [#9068](https://github.com/netbox-community/netbox/issues/9068) - Disallow the assignment of network/broadcast IP addresses to interfaces
|
||||||
|
* [#11017](https://github.com/netbox-community/netbox/issues/11017) - Increase the maximum values for allocated and maximum power draws
|
||||||
* [#11233](https://github.com/netbox-community/netbox/issues/11233) - Intercept and cleanly report errors upon attempted database writes when maintenance mode is enabled
|
* [#11233](https://github.com/netbox-community/netbox/issues/11233) - Intercept and cleanly report errors upon attempted database writes when maintenance mode is enabled
|
||||||
* [#11599](https://github.com/netbox-community/netbox/issues/11599) - Move contacts panels to separate tabs under object views
|
* [#11599](https://github.com/netbox-community/netbox/issues/11599) - Move contacts panels to separate tabs under object views
|
||||||
* [#11670](https://github.com/netbox-community/netbox/issues/11670) - Enable setting device type & module type weight via bulk import
|
* [#11670](https://github.com/netbox-community/netbox/issues/11670) - Enable setting device type & module type weight via bulk import
|
||||||
@ -14,14 +49,23 @@
|
|||||||
* [#12233](https://github.com/netbox-community/netbox/issues/12233) - Move related IP addresses table to a separate tab
|
* [#12233](https://github.com/netbox-community/netbox/issues/12233) - Move related IP addresses table to a separate tab
|
||||||
* [#12286](https://github.com/netbox-community/netbox/issues/12286) - Show height and total weight under device view
|
* [#12286](https://github.com/netbox-community/netbox/issues/12286) - Show height and total weight under device view
|
||||||
* [#12323](https://github.com/netbox-community/netbox/issues/12323) - Add 100GE CXP interface type
|
* [#12323](https://github.com/netbox-community/netbox/issues/12323) - Add 100GE CXP interface type
|
||||||
|
* [#12327](https://github.com/netbox-community/netbox/issues/12327) - Introduce the ability to automatically retry failed background jobs
|
||||||
* [#12498](https://github.com/netbox-community/netbox/issues/12498) - Hide map button if `MAPS_URL` is empty
|
* [#12498](https://github.com/netbox-community/netbox/issues/12498) - Hide map button if `MAPS_URL` is empty
|
||||||
|
* [#12548](https://github.com/netbox-community/netbox/issues/12548) - Optimize REST API performance when retrieving interfaces with L2VPN assignments
|
||||||
* [#12554](https://github.com/netbox-community/netbox/issues/12554) - Allow customization or disabling of the maintenance mode banner
|
* [#12554](https://github.com/netbox-community/netbox/issues/12554) - Allow customization or disabling of the maintenance mode banner
|
||||||
|
* [#12605](https://github.com/netbox-community/netbox/issues/12605) - Add LX.5 port types
|
||||||
|
* [#12629](https://github.com/netbox-community/netbox/issues/12629) - Add 400GE CDFP and CFP8 interface types
|
||||||
|
* [#12678](https://github.com/netbox-community/netbox/issues/12678) - Add 200GE QSFP-DD interface type
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* [#10686](https://github.com/netbox-community/netbox/issues/10686) - Enable specifying termination object by virtual chassis master when importing cables
|
* [#10686](https://github.com/netbox-community/netbox/issues/10686) - Enable specifying termination object by virtual chassis master when importing cables
|
||||||
|
* [#11619](https://github.com/netbox-community/netbox/issues/11619) - Enable assigning VLANs without a site to interfaces during bulk edit
|
||||||
|
* [#12468](https://github.com/netbox-community/netbox/issues/12468) - Custom field names should not permit double underscores
|
||||||
* [#12550](https://github.com/netbox-community/netbox/issues/12550) - Fix rear port selection widget under front port creation form
|
* [#12550](https://github.com/netbox-community/netbox/issues/12550) - Fix rear port selection widget under front port creation form
|
||||||
* [#12570](https://github.com/netbox-community/netbox/issues/12570) - Disable ordering of synchronized object tables by the "synced" attribute
|
* [#12570](https://github.com/netbox-community/netbox/issues/12570) - Disable ordering of synchronized object tables by the "synced" attribute
|
||||||
|
* [#12594](https://github.com/netbox-community/netbox/issues/12594) - Enable selecting config context as object type in object counts dashboard widget
|
||||||
|
* [#12642](https://github.com/netbox-community/netbox/issues/12642) - Fix bulk tenant assignment via cluster import form
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ from extras.utils import FeatureQuery
|
|||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
from netbox.constants import RQ_QUEUE_DEFAULT
|
from netbox.constants import RQ_QUEUE_DEFAULT
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.rqworker import get_queue_for_model
|
from utilities.rqworker import get_queue_for_model, get_rq_retry
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Job',
|
'Job',
|
||||||
@ -219,5 +219,6 @@ class Job(models.Model):
|
|||||||
event=event,
|
event=event,
|
||||||
data=self.data,
|
data=self.data,
|
||||||
timestamp=str(timezone.now()),
|
timestamp=str(timezone.now()),
|
||||||
username=self.user.username
|
username=self.user.username,
|
||||||
|
retry=get_rq_retry()
|
||||||
)
|
)
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter
|
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.renderers import JSONRenderer
|
from rest_framework.renderers import JSONRenderer
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.status import HTTP_400_BAD_REQUEST
|
|
||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
|
from rest_framework.status import HTTP_400_BAD_REQUEST
|
||||||
from rest_framework.viewsets import ViewSet
|
from rest_framework.viewsets import ViewSet
|
||||||
|
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
@ -14,7 +14,6 @@ from dcim import filtersets
|
|||||||
from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
|
from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from dcim.svg import CableTraceSVG
|
from dcim.svg import CableTraceSVG
|
||||||
from extras.api.nested_serializers import NestedConfigTemplateSerializer
|
|
||||||
from extras.api.mixins import ConfigContextQuerySetMixin, ConfigTemplateRenderMixin
|
from extras.api.mixins import ConfigContextQuerySetMixin, ConfigTemplateRenderMixin
|
||||||
from ipam.models import Prefix, VLAN
|
from ipam.models import Prefix, VLAN
|
||||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
||||||
@ -22,6 +21,7 @@ from netbox.api.metadata import ContentTypeMetadata
|
|||||||
from netbox.api.pagination import StripCountAnnotationsPaginator
|
from netbox.api.pagination import StripCountAnnotationsPaginator
|
||||||
from netbox.api.renderers import TextRenderer
|
from netbox.api.renderers import TextRenderer
|
||||||
from netbox.api.viewsets import NetBoxModelViewSet
|
from netbox.api.viewsets import NetBoxModelViewSet
|
||||||
|
from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin
|
||||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
@ -386,7 +386,12 @@ class PlatformViewSet(NetBoxModelViewSet):
|
|||||||
# Devices/modules
|
# Devices/modules
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceViewSet(ConfigContextQuerySetMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet):
|
class DeviceViewSet(
|
||||||
|
SequentialBulkCreatesMixin,
|
||||||
|
ConfigContextQuerySetMixin,
|
||||||
|
ConfigTemplateRenderMixin,
|
||||||
|
NetBoxModelViewSet
|
||||||
|
):
|
||||||
queryset = Device.objects.prefetch_related(
|
queryset = Device.objects.prefetch_related(
|
||||||
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
|
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
|
||||||
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'config_template', 'tags',
|
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'config_template', 'tags',
|
||||||
@ -493,7 +498,8 @@ class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
|||||||
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = Interface.objects.prefetch_related(
|
queryset = Interface.objects.prefetch_related(
|
||||||
'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path', 'cable__terminations', 'wireless_lans',
|
'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path', 'cable__terminations', 'wireless_lans',
|
||||||
'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags'
|
'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags', 'l2vpn_terminations',
|
||||||
|
'vdcs',
|
||||||
)
|
)
|
||||||
serializer_class = serializers.InterfaceSerializer
|
serializer_class = serializers.InterfaceSerializer
|
||||||
filterset_class = filtersets.InterfaceFilterSet
|
filterset_class = filtersets.InterfaceFilterSet
|
||||||
|
@ -812,8 +812,11 @@ class InterfaceTypeChoices(ChoiceSet):
|
|||||||
TYPE_100GE_QSFP28 = '100gbase-x-qsfp28'
|
TYPE_100GE_QSFP28 = '100gbase-x-qsfp28'
|
||||||
TYPE_200GE_CFP2 = '200gbase-x-cfp2'
|
TYPE_200GE_CFP2 = '200gbase-x-cfp2'
|
||||||
TYPE_200GE_QSFP56 = '200gbase-x-qsfp56'
|
TYPE_200GE_QSFP56 = '200gbase-x-qsfp56'
|
||||||
|
TYPE_200GE_QSFP_DD = '200gbase-x-qsfpdd'
|
||||||
TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd'
|
TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd'
|
||||||
TYPE_400GE_OSFP = '400gbase-x-osfp'
|
TYPE_400GE_OSFP = '400gbase-x-osfp'
|
||||||
|
TYPE_400GE_CDFP = '400gbase-x-cdfp'
|
||||||
|
TYPE_400GE_CFP8 = '400gbase-x-cfp8'
|
||||||
TYPE_800GE_QSFP_DD = '800gbase-x-qsfpdd'
|
TYPE_800GE_QSFP_DD = '800gbase-x-qsfpdd'
|
||||||
TYPE_800GE_OSFP = '800gbase-x-osfp'
|
TYPE_800GE_OSFP = '800gbase-x-osfp'
|
||||||
|
|
||||||
@ -957,8 +960,11 @@ class InterfaceTypeChoices(ChoiceSet):
|
|||||||
(TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'),
|
(TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'),
|
||||||
(TYPE_100GE_QSFP28, 'QSFP28 (100GE)'),
|
(TYPE_100GE_QSFP28, 'QSFP28 (100GE)'),
|
||||||
(TYPE_200GE_QSFP56, 'QSFP56 (200GE)'),
|
(TYPE_200GE_QSFP56, 'QSFP56 (200GE)'),
|
||||||
|
(TYPE_200GE_QSFP_DD, 'QSFP-DD (200GE)'),
|
||||||
(TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'),
|
(TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'),
|
||||||
(TYPE_400GE_OSFP, 'OSFP (400GE)'),
|
(TYPE_400GE_OSFP, 'OSFP (400GE)'),
|
||||||
|
(TYPE_400GE_CDFP, 'CDFP (400GE)'),
|
||||||
|
(TYPE_400GE_CFP8, 'CPF8 (400GE)'),
|
||||||
(TYPE_800GE_QSFP_DD, 'QSFP-DD (800GE)'),
|
(TYPE_800GE_QSFP_DD, 'QSFP-DD (800GE)'),
|
||||||
(TYPE_800GE_OSFP, 'OSFP (800GE)'),
|
(TYPE_800GE_OSFP, 'OSFP (800GE)'),
|
||||||
)
|
)
|
||||||
@ -1223,6 +1229,10 @@ class PortTypeChoices(ChoiceSet):
|
|||||||
TYPE_LSH_PC = 'lsh-pc'
|
TYPE_LSH_PC = 'lsh-pc'
|
||||||
TYPE_LSH_UPC = 'lsh-upc'
|
TYPE_LSH_UPC = 'lsh-upc'
|
||||||
TYPE_LSH_APC = 'lsh-apc'
|
TYPE_LSH_APC = 'lsh-apc'
|
||||||
|
TYPE_LX5 = 'lx5'
|
||||||
|
TYPE_LX5_PC = 'lx5-pc'
|
||||||
|
TYPE_LX5_UPC = 'lx5-upc'
|
||||||
|
TYPE_LX5_APC = 'lx5-apc'
|
||||||
TYPE_SPLICE = 'splice'
|
TYPE_SPLICE = 'splice'
|
||||||
TYPE_CS = 'cs'
|
TYPE_CS = 'cs'
|
||||||
TYPE_SN = 'sn'
|
TYPE_SN = 'sn'
|
||||||
@ -1269,6 +1279,10 @@ class PortTypeChoices(ChoiceSet):
|
|||||||
(TYPE_LSH_PC, 'LSH/PC'),
|
(TYPE_LSH_PC, 'LSH/PC'),
|
||||||
(TYPE_LSH_UPC, 'LSH/UPC'),
|
(TYPE_LSH_UPC, 'LSH/UPC'),
|
||||||
(TYPE_LSH_APC, 'LSH/APC'),
|
(TYPE_LSH_APC, 'LSH/APC'),
|
||||||
|
(TYPE_LX5, 'LX.5'),
|
||||||
|
(TYPE_LX5_PC, 'LX.5/PC'),
|
||||||
|
(TYPE_LX5_UPC, 'LX.5/UPC'),
|
||||||
|
(TYPE_LX5_APC, 'LX.5/APC'),
|
||||||
(TYPE_MPO, 'MPO'),
|
(TYPE_MPO, 'MPO'),
|
||||||
(TYPE_MTRJ, 'MTRJ'),
|
(TYPE_MTRJ, 'MTRJ'),
|
||||||
(TYPE_SC, 'SC'),
|
(TYPE_SC, 'SC'),
|
||||||
|
@ -1219,6 +1219,28 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
|||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
label=_('Device (name)'),
|
label=_('Device (name)'),
|
||||||
)
|
)
|
||||||
|
device_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='device__device_type',
|
||||||
|
queryset=DeviceType.objects.all(),
|
||||||
|
label=_('Device type (ID)'),
|
||||||
|
)
|
||||||
|
device_type = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='device__device_type__model',
|
||||||
|
queryset=DeviceType.objects.all(),
|
||||||
|
to_field_name='model',
|
||||||
|
label=_('Device type (model)'),
|
||||||
|
)
|
||||||
|
device_role_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='device__device_role',
|
||||||
|
queryset=DeviceRole.objects.all(),
|
||||||
|
label=_('Device role (ID)'),
|
||||||
|
)
|
||||||
|
device_role = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='device__device_role__slug',
|
||||||
|
queryset=DeviceRole.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label=_('Device role (slug)'),
|
||||||
|
)
|
||||||
virtual_chassis_id = django_filters.ModelMultipleChoiceFilter(
|
virtual_chassis_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='device__virtual_chassis',
|
field_name='device__virtual_chassis',
|
||||||
queryset=VirtualChassis.objects.all(),
|
queryset=VirtualChassis.objects.all(),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from timezone_field import TimeZoneFormField
|
from timezone_field import TimeZoneFormField
|
||||||
@ -1288,8 +1289,13 @@ class InterfaceBulkEditForm(
|
|||||||
break
|
break
|
||||||
|
|
||||||
if site is not None:
|
if site is not None:
|
||||||
self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
|
# Query for VLANs assigned to the same site and VLANs with no site assigned (null).
|
||||||
self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
|
self.fields['untagged_vlan'].widget.add_query_param(
|
||||||
|
'site_id', [site.pk, settings.FILTERS_NULL_CHOICE_VALUE]
|
||||||
|
)
|
||||||
|
self.fields['tagged_vlans'].widget.add_query_param(
|
||||||
|
'site_id', [site.pk, settings.FILTERS_NULL_CHOICE_VALUE]
|
||||||
|
)
|
||||||
|
|
||||||
self.fields['parent'].choices = ()
|
self.fields['parent'].choices = ()
|
||||||
self.fields['parent'].widget.attrs['disabled'] = True
|
self.fields['parent'].widget.attrs['disabled'] = True
|
||||||
|
@ -102,13 +102,25 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('Virtual Chassis')
|
label=_('Virtual Chassis')
|
||||||
)
|
)
|
||||||
|
device_type_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=DeviceType.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Device type')
|
||||||
|
)
|
||||||
|
device_role_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=DeviceRole.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Device role')
|
||||||
|
)
|
||||||
device_id = DynamicModelMultipleChoiceField(
|
device_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
query_params={
|
query_params={
|
||||||
'site_id': '$site_id',
|
'site_id': '$site_id',
|
||||||
'location_id': '$location_id',
|
'location_id': '$location_id',
|
||||||
'virtual_chassis_id': '$virtual_chassis_id'
|
'virtual_chassis_id': '$virtual_chassis_id',
|
||||||
|
'device_type_id': '$device_type_id',
|
||||||
|
'role_id': '$device_role_id'
|
||||||
},
|
},
|
||||||
label=_('Device')
|
label=_('Device')
|
||||||
)
|
)
|
||||||
@ -1070,7 +1082,8 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id', 'tag')),
|
(None, ('q', 'filter_id', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'type', 'speed')),
|
('Attributes', ('name', 'label', 'type', 'speed')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
||||||
|
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
|
||||||
('Connection', ('cabled', 'connected', 'occupied')),
|
('Connection', ('cabled', 'connected', 'occupied')),
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
@ -1089,7 +1102,8 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id', 'tag')),
|
(None, ('q', 'filter_id', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'type', 'speed')),
|
('Attributes', ('name', 'label', 'type', 'speed')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
||||||
|
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
|
||||||
('Connection', ('cabled', 'connected', 'occupied')),
|
('Connection', ('cabled', 'connected', 'occupied')),
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
@ -1108,7 +1122,8 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id', 'tag')),
|
(None, ('q', 'filter_id', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'type')),
|
('Attributes', ('name', 'label', 'type')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
||||||
|
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
|
||||||
('Connection', ('cabled', 'connected', 'occupied')),
|
('Connection', ('cabled', 'connected', 'occupied')),
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
@ -1123,7 +1138,8 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id', 'tag')),
|
(None, ('q', 'filter_id', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'type')),
|
('Attributes', ('name', 'label', 'type')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
||||||
|
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
|
||||||
('Connection', ('cabled', 'connected', 'occupied')),
|
('Connection', ('cabled', 'connected', 'occupied')),
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
@ -1141,8 +1157,8 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
('Addressing', ('vrf_id', 'l2vpn_id', 'mac_address', 'wwn')),
|
('Addressing', ('vrf_id', 'l2vpn_id', 'mac_address', 'wwn')),
|
||||||
('PoE', ('poe_mode', 'poe_type')),
|
('PoE', ('poe_mode', 'poe_type')),
|
||||||
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
|
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id',
|
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
||||||
'device_id', 'vdc_id')),
|
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')),
|
||||||
('Connection', ('cabled', 'connected', 'occupied')),
|
('Connection', ('cabled', 'connected', 'occupied')),
|
||||||
)
|
)
|
||||||
vdc_id = DynamicModelMultipleChoiceField(
|
vdc_id = DynamicModelMultipleChoiceField(
|
||||||
@ -1242,7 +1258,8 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id', 'tag')),
|
(None, ('q', 'filter_id', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'type', 'color')),
|
('Attributes', ('name', 'label', 'type', 'color')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
||||||
|
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
|
||||||
('Cable', ('cabled', 'occupied')),
|
('Cable', ('cabled', 'occupied')),
|
||||||
)
|
)
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
@ -1261,7 +1278,8 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id', 'tag')),
|
(None, ('q', 'filter_id', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'type', 'color')),
|
('Attributes', ('name', 'label', 'type', 'color')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
||||||
|
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
|
||||||
('Cable', ('cabled', 'occupied')),
|
('Cable', ('cabled', 'occupied')),
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
@ -1279,7 +1297,8 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id', 'tag')),
|
(None, ('q', 'filter_id', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'position')),
|
('Attributes', ('name', 'label', 'position')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
||||||
|
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
position = forms.CharField(
|
position = forms.CharField(
|
||||||
@ -1292,7 +1311,8 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id', 'tag')),
|
(None, ('q', 'filter_id', 'tag')),
|
||||||
('Attributes', ('name', 'label')),
|
('Attributes', ('name', 'label')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
||||||
|
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
@ -1302,7 +1322,8 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id', 'tag')),
|
(None, ('q', 'filter_id', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
|
('Attributes', ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
||||||
|
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
|
||||||
)
|
)
|
||||||
role_id = DynamicModelMultipleChoiceField(
|
role_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=InventoryItemRole.objects.all(),
|
queryset=InventoryItemRole.objects.all(),
|
||||||
|
@ -101,6 +101,7 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp
|
|||||||
choices=[],
|
choices=[],
|
||||||
label=_('Rear ports'),
|
label=_('Rear ports'),
|
||||||
help_text=_('Select one rear port assignment for each front port being created.'),
|
help_text=_('Select one rear port assignment for each front port being created.'),
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 6})
|
||||||
)
|
)
|
||||||
|
|
||||||
# Override fieldsets from FrontPortTemplateForm to omit rear_port_position
|
# Override fieldsets from FrontPortTemplateForm to omit rear_port_position
|
||||||
|
@ -1115,7 +1115,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
device_types = (
|
device_types = (
|
||||||
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||||
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2', u_height=2),
|
||||||
)
|
)
|
||||||
DeviceType.objects.bulk_create(device_types)
|
DeviceType.objects.bulk_create(device_types)
|
||||||
|
|
||||||
@ -1229,6 +1229,39 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_rack_fit(self):
|
||||||
|
"""
|
||||||
|
Check that creating multiple devices with overlapping position fails.
|
||||||
|
"""
|
||||||
|
device = Device.objects.first()
|
||||||
|
device_type = DeviceType.objects.all()[1]
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
'device_type': device_type.pk,
|
||||||
|
'device_role': device.device_role.pk,
|
||||||
|
'site': device.site.pk,
|
||||||
|
'name': 'Test Device 7',
|
||||||
|
'rack': device.rack.pk,
|
||||||
|
'face': 'front',
|
||||||
|
'position': 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'device_type': device_type.pk,
|
||||||
|
'device_role': device.device_role.pk,
|
||||||
|
'site': device.site.pk,
|
||||||
|
'name': 'Test Device 8',
|
||||||
|
'rack': device.rack.pk,
|
||||||
|
'face': 'front',
|
||||||
|
'position': 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.add_permissions('dcim.add_device')
|
||||||
|
url = reverse('dcim-api:device-list')
|
||||||
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
|
|
||||||
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
class ModuleTest(APIViewTestCases.APIViewTestCase):
|
class ModuleTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = Module
|
model = Module
|
||||||
|
@ -12,6 +12,23 @@ from virtualization.models import Cluster, ClusterType
|
|||||||
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceComponentFilterSetTests:
|
||||||
|
|
||||||
|
def test_device_type(self):
|
||||||
|
device_types = DeviceType.objects.all()[:2]
|
||||||
|
params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'device_type': [device_types[0].model, device_types[1].model]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_device_role(self):
|
||||||
|
device_role = DeviceRole.objects.all()[:2]
|
||||||
|
params = {'device_role_id': [device_role[0].pk, device_role[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'device_role': [device_role[0].slug, device_role[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = Region.objects.all()
|
queryset = Region.objects.all()
|
||||||
filterset = RegionFilterSet
|
filterset = RegionFilterSet
|
||||||
@ -1994,7 +2011,7 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||||
queryset = ConsolePort.objects.all()
|
queryset = ConsolePort.objects.all()
|
||||||
filterset = ConsolePortFilterSet
|
filterset = ConsolePortFilterSet
|
||||||
|
|
||||||
@ -2023,10 +2040,23 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||||
Site(name='Site X', slug='site-x'),
|
Site(name='Site X', slug='site-x'),
|
||||||
))
|
))
|
||||||
|
|
||||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
device_types = (
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 3', slug='device-type-3'),
|
||||||
|
)
|
||||||
|
DeviceType.objects.bulk_create(device_types)
|
||||||
|
|
||||||
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
|
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
|
||||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
|
||||||
|
device_roles = (
|
||||||
|
DeviceRole(name='Device Role 1', slug='device-role-1'),
|
||||||
|
DeviceRole(name='Device Role 2', slug='device-role-2'),
|
||||||
|
DeviceRole(name='Device Role 3', slug='device-role-3'),
|
||||||
|
)
|
||||||
|
DeviceRole.objects.bulk_create(device_roles)
|
||||||
|
|
||||||
locations = (
|
locations = (
|
||||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||||
@ -2044,10 +2074,10 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Rack.objects.bulk_create(racks)
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
Device(name=None, device_type=device_types[0], device_role=device_roles[0], site=sites[3]), # For cable connections
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
@ -2161,7 +2191,7 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||||
queryset = ConsoleServerPort.objects.all()
|
queryset = ConsoleServerPort.objects.all()
|
||||||
filterset = ConsoleServerPortFilterSet
|
filterset = ConsoleServerPortFilterSet
|
||||||
|
|
||||||
@ -2190,10 +2220,23 @@ class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||||
Site(name='Site X', slug='site-x'),
|
Site(name='Site X', slug='site-x'),
|
||||||
))
|
))
|
||||||
|
|
||||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
device_types = (
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 3', slug='device-type-3'),
|
||||||
|
)
|
||||||
|
DeviceType.objects.bulk_create(device_types)
|
||||||
|
|
||||||
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
|
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
|
||||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
|
||||||
|
device_roles = (
|
||||||
|
DeviceRole(name='Device Role 1', slug='device-role-1'),
|
||||||
|
DeviceRole(name='Device Role 2', slug='device-role-2'),
|
||||||
|
DeviceRole(name='Device Role 3', slug='device-role-3'),
|
||||||
|
)
|
||||||
|
DeviceRole.objects.bulk_create(device_roles)
|
||||||
|
|
||||||
locations = (
|
locations = (
|
||||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||||
@ -2211,10 +2254,10 @@ class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Rack.objects.bulk_create(racks)
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
Device(name=None, device_type=device_types[2], device_role=device_roles[2], site=sites[3]), # For cable connections
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
@ -2328,7 +2371,7 @@ class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||||
queryset = PowerPort.objects.all()
|
queryset = PowerPort.objects.all()
|
||||||
filterset = PowerPortFilterSet
|
filterset = PowerPortFilterSet
|
||||||
|
|
||||||
@ -2357,10 +2400,23 @@ class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||||
Site(name='Site X', slug='site-x'),
|
Site(name='Site X', slug='site-x'),
|
||||||
))
|
))
|
||||||
|
|
||||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
device_types = (
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 3', slug='device-type-3'),
|
||||||
|
)
|
||||||
|
DeviceType.objects.bulk_create(device_types)
|
||||||
|
|
||||||
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
|
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
|
||||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
|
||||||
|
device_roles = (
|
||||||
|
DeviceRole(name='Device Role 1', slug='device-role-1'),
|
||||||
|
DeviceRole(name='Device Role 2', slug='device-role-2'),
|
||||||
|
DeviceRole(name='Device Role 3', slug='device-role-3'),
|
||||||
|
)
|
||||||
|
DeviceRole.objects.bulk_create(device_roles)
|
||||||
|
|
||||||
locations = (
|
locations = (
|
||||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||||
@ -2378,10 +2434,10 @@ class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Rack.objects.bulk_create(racks)
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
Device(name=None, device_type=device_types[2], device_role=device_roles[2], site=sites[3]), # For cable connections
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
@ -2503,7 +2559,7 @@ class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||||
queryset = PowerOutlet.objects.all()
|
queryset = PowerOutlet.objects.all()
|
||||||
filterset = PowerOutletFilterSet
|
filterset = PowerOutletFilterSet
|
||||||
|
|
||||||
@ -2532,10 +2588,23 @@ class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||||
Site(name='Site X', slug='site-x'),
|
Site(name='Site X', slug='site-x'),
|
||||||
))
|
))
|
||||||
|
|
||||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
device_types = (
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 3', slug='device-type-3'),
|
||||||
|
)
|
||||||
|
DeviceType.objects.bulk_create(device_types)
|
||||||
|
|
||||||
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
|
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
|
||||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
|
||||||
|
device_roles = (
|
||||||
|
DeviceRole(name='Device Role 1', slug='device-role-1'),
|
||||||
|
DeviceRole(name='Device Role 2', slug='device-role-2'),
|
||||||
|
DeviceRole(name='Device Role 3', slug='device-role-3'),
|
||||||
|
)
|
||||||
|
DeviceRole.objects.bulk_create(device_roles)
|
||||||
|
|
||||||
locations = (
|
locations = (
|
||||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||||
@ -2553,10 +2622,10 @@ class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Rack.objects.bulk_create(racks)
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
Device(name=None, device_type=device_types[2], device_role=device_roles[2], site=sites[3]), # For cable connections
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
@ -2674,7 +2743,7 @@ class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||||
queryset = Interface.objects.all()
|
queryset = Interface.objects.all()
|
||||||
filterset = InterfaceFilterSet
|
filterset = InterfaceFilterSet
|
||||||
|
|
||||||
@ -2703,10 +2772,23 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||||
Site(name='Site X', slug='site-x'),
|
Site(name='Site X', slug='site-x'),
|
||||||
))
|
))
|
||||||
|
|
||||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
device_types = (
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 3', slug='device-type-3'),
|
||||||
|
)
|
||||||
|
DeviceType.objects.bulk_create(device_types)
|
||||||
|
|
||||||
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
|
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
|
||||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
|
||||||
|
device_roles = (
|
||||||
|
DeviceRole(name='Device Role 1', slug='device-role-1'),
|
||||||
|
DeviceRole(name='Device Role 2', slug='device-role-2'),
|
||||||
|
DeviceRole(name='Device Role 3', slug='device-role-3'),
|
||||||
|
)
|
||||||
|
DeviceRole.objects.bulk_create(device_roles)
|
||||||
|
|
||||||
locations = (
|
locations = (
|
||||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||||
@ -2724,10 +2806,10 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Rack.objects.bulk_create(racks)
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
Device(name=None, device_type=device_types[2], device_role=device_roles[2], site=sites[3]), # For cable connections
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
@ -3097,7 +3179,7 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||||
queryset = FrontPort.objects.all()
|
queryset = FrontPort.objects.all()
|
||||||
filterset = FrontPortFilterSet
|
filterset = FrontPortFilterSet
|
||||||
|
|
||||||
@ -3126,10 +3208,23 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||||
Site(name='Site X', slug='site-x'),
|
Site(name='Site X', slug='site-x'),
|
||||||
))
|
))
|
||||||
|
|
||||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
device_types = (
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 3', slug='device-type-3'),
|
||||||
|
)
|
||||||
|
DeviceType.objects.bulk_create(device_types)
|
||||||
|
|
||||||
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
|
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
|
||||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
|
||||||
|
device_roles = (
|
||||||
|
DeviceRole(name='Device Role 1', slug='device-role-1'),
|
||||||
|
DeviceRole(name='Device Role 2', slug='device-role-2'),
|
||||||
|
DeviceRole(name='Device Role 3', slug='device-role-3'),
|
||||||
|
)
|
||||||
|
DeviceRole.objects.bulk_create(device_roles)
|
||||||
|
|
||||||
locations = (
|
locations = (
|
||||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||||
@ -3147,10 +3242,10 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Rack.objects.bulk_create(racks)
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
Device(name=None, device_type=device_types[2], device_role=device_roles[2], site=sites[3]), # For cable connections
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
@ -3273,7 +3368,7 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||||
queryset = RearPort.objects.all()
|
queryset = RearPort.objects.all()
|
||||||
filterset = RearPortFilterSet
|
filterset = RearPortFilterSet
|
||||||
|
|
||||||
@ -3302,10 +3397,23 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||||
Site(name='Site X', slug='site-x'),
|
Site(name='Site X', slug='site-x'),
|
||||||
))
|
))
|
||||||
|
|
||||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
device_types = (
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 3', slug='device-type-3'),
|
||||||
|
)
|
||||||
|
DeviceType.objects.bulk_create(device_types)
|
||||||
|
|
||||||
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
|
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
|
||||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
|
||||||
|
device_roles = (
|
||||||
|
DeviceRole(name='Device Role 1', slug='device-role-1'),
|
||||||
|
DeviceRole(name='Device Role 2', slug='device-role-2'),
|
||||||
|
DeviceRole(name='Device Role 3', slug='device-role-3'),
|
||||||
|
)
|
||||||
|
DeviceRole.objects.bulk_create(device_roles)
|
||||||
|
|
||||||
locations = (
|
locations = (
|
||||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||||
@ -3323,10 +3431,10 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Rack.objects.bulk_create(racks)
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
Device(name=None, device_type=device_types[2], device_role=device_roles[2], site=sites[3]), # For cable connections
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
@ -3443,7 +3551,7 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||||
queryset = ModuleBay.objects.all()
|
queryset = ModuleBay.objects.all()
|
||||||
filterset = ModuleBayFilterSet
|
filterset = ModuleBayFilterSet
|
||||||
|
|
||||||
@ -3472,9 +3580,21 @@ class ModuleBayTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||||
Site(name='Site X', slug='site-x'),
|
Site(name='Site X', slug='site-x'),
|
||||||
))
|
))
|
||||||
|
|
||||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1')
|
device_types = (
|
||||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 3', slug='device-type-3'),
|
||||||
|
)
|
||||||
|
DeviceType.objects.bulk_create(device_types)
|
||||||
|
|
||||||
|
device_roles = (
|
||||||
|
DeviceRole(name='Device Role 1', slug='device-role-1'),
|
||||||
|
DeviceRole(name='Device Role 2', slug='device-role-2'),
|
||||||
|
DeviceRole(name='Device Role 3', slug='device-role-3'),
|
||||||
|
)
|
||||||
|
DeviceRole.objects.bulk_create(device_roles)
|
||||||
|
|
||||||
locations = (
|
locations = (
|
||||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||||
@ -3492,9 +3612,9 @@ class ModuleBayTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Rack.objects.bulk_create(racks)
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
@ -3560,7 +3680,7 @@ class ModuleBayTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||||
queryset = DeviceBay.objects.all()
|
queryset = DeviceBay.objects.all()
|
||||||
filterset = DeviceBayFilterSet
|
filterset = DeviceBayFilterSet
|
||||||
|
|
||||||
@ -3589,9 +3709,21 @@ class DeviceBayTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
|
||||||
Site(name='Site X', slug='site-x'),
|
Site(name='Site X', slug='site-x'),
|
||||||
))
|
))
|
||||||
|
|
||||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1')
|
device_types = (
|
||||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||||
|
DeviceType(manufacturer=manufacturer, model='Device Type 3', slug='device-type-3'),
|
||||||
|
)
|
||||||
|
DeviceType.objects.bulk_create(device_types)
|
||||||
|
|
||||||
|
device_roles = (
|
||||||
|
DeviceRole(name='Device Role 1', slug='device-role-1'),
|
||||||
|
DeviceRole(name='Device Role 2', slug='device-role-2'),
|
||||||
|
DeviceRole(name='Device Role 3', slug='device-role-3'),
|
||||||
|
)
|
||||||
|
DeviceRole.objects.bulk_create(device_roles)
|
||||||
|
|
||||||
locations = (
|
locations = (
|
||||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||||
@ -3609,9 +3741,9 @@ class DeviceBayTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Rack.objects.bulk_create(racks)
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
@ -3690,8 +3822,19 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
Manufacturer.objects.bulk_create(manufacturers)
|
Manufacturer.objects.bulk_create(manufacturers)
|
||||||
|
|
||||||
device_type = DeviceType.objects.create(manufacturer=manufacturers[0], model='Model 1', slug='model-1')
|
device_types = (
|
||||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
DeviceType(manufacturer=manufacturers[0], model='Device Type 1', slug='device-type-1'),
|
||||||
|
DeviceType(manufacturer=manufacturers[0], model='Device Type 2', slug='device-type-2'),
|
||||||
|
DeviceType(manufacturer=manufacturers[0], model='Device Type 3', slug='device-type-3'),
|
||||||
|
)
|
||||||
|
DeviceType.objects.bulk_create(device_types)
|
||||||
|
|
||||||
|
device_roles = (
|
||||||
|
DeviceRole(name='Device Role 1', slug='device-role-1'),
|
||||||
|
DeviceRole(name='Device Role 2', slug='device-role-2'),
|
||||||
|
DeviceRole(name='Device Role 3', slug='device-role-3'),
|
||||||
|
)
|
||||||
|
DeviceRole.objects.bulk_create(device_roles)
|
||||||
|
|
||||||
regions = (
|
regions = (
|
||||||
Region(name='Region 1', slug='region-1'),
|
Region(name='Region 1', slug='region-1'),
|
||||||
@ -3732,9 +3875,9 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Rack.objects.bulk_create(racks)
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
@ -3825,6 +3968,20 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'rack': [racks[0].name, racks[1].name]}
|
params = {'rack': [racks[0].name, racks[1].name]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_device_type(self):
|
||||||
|
device_types = DeviceType.objects.all()[:2]
|
||||||
|
params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
params = {'device_type': [device_types[0].model, device_types[1].model]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_device_role(self):
|
||||||
|
device_role = DeviceRole.objects.all()[:2]
|
||||||
|
params = {'device_role_id': [device_role[0].pk, device_role[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
params = {'device_role': [device_role[0].slug, device_role[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
def test_device(self):
|
def test_device(self):
|
||||||
devices = Device.objects.all()[:2]
|
devices = Device.objects.all()[:2]
|
||||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||||
|
@ -56,11 +56,13 @@ class CustomFieldVisibilityChoices(ChoiceSet):
|
|||||||
VISIBILITY_READ_WRITE = 'read-write'
|
VISIBILITY_READ_WRITE = 'read-write'
|
||||||
VISIBILITY_READ_ONLY = 'read-only'
|
VISIBILITY_READ_ONLY = 'read-only'
|
||||||
VISIBILITY_HIDDEN = 'hidden'
|
VISIBILITY_HIDDEN = 'hidden'
|
||||||
|
VISIBILITY_HIDDEN_IFUNSET = 'hidden-ifunset'
|
||||||
|
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
(VISIBILITY_READ_WRITE, 'Read/Write'),
|
(VISIBILITY_READ_WRITE, 'Read/Write'),
|
||||||
(VISIBILITY_READ_ONLY, 'Read-only'),
|
(VISIBILITY_READ_ONLY, 'Read-only'),
|
||||||
(VISIBILITY_HIDDEN, 'Hidden'),
|
(VISIBILITY_HIDDEN, 'Hidden'),
|
||||||
|
(VISIBILITY_HIDDEN_IFUNSET, 'Hidden (if unset)'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,8 +65,14 @@ class Condition:
|
|||||||
"""
|
"""
|
||||||
Evaluate the provided data to determine whether it matches the condition.
|
Evaluate the provided data to determine whether it matches the condition.
|
||||||
"""
|
"""
|
||||||
|
def _get(obj, key):
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return [dict.get(i, key) for i in obj]
|
||||||
|
|
||||||
|
return dict.get(obj, key)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
value = functools.reduce(dict.get, self.attr.split('.'), data)
|
value = functools.reduce(_get, self.attr.split('.'), data)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
# Invalid key path
|
# Invalid key path
|
||||||
value = None
|
value = None
|
||||||
|
@ -10,8 +10,9 @@ from django.conf import settings
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.http import QueryDict
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import NoReverseMatch, resolve, reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from extras.utils import FeatureQuery
|
from extras.utils import FeatureQuery
|
||||||
@ -35,7 +36,8 @@ def get_content_type_labels():
|
|||||||
return [
|
return [
|
||||||
(content_type_identifier(ct), content_type_name(ct))
|
(content_type_identifier(ct), content_type_name(ct))
|
||||||
for ct in ContentType.objects.filter(
|
for ct in ContentType.objects.filter(
|
||||||
FeatureQuery('export_templates').get_query() | Q(app_label='extras', model='objectchange')
|
FeatureQuery('export_templates').get_query() | Q(app_label='extras', model='objectchange') |
|
||||||
|
Q(app_label='extras', model='configcontext')
|
||||||
).order_by('app_label', 'model')
|
).order_by('app_label', 'model')
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -148,7 +150,7 @@ class ObjectCountsWidget(DashboardWidget):
|
|||||||
filters = forms.JSONField(
|
filters = forms.JSONField(
|
||||||
required=False,
|
required=False,
|
||||||
label='Object filters',
|
label='Object filters',
|
||||||
help_text=_("Only objects matching the specified filters will be counted")
|
help_text=_("Filters to apply when counting the number of objects")
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean_filters(self):
|
def clean_filters(self):
|
||||||
@ -157,13 +159,6 @@ class ObjectCountsWidget(DashboardWidget):
|
|||||||
dict(data)
|
dict(data)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
raise forms.ValidationError("Invalid format. Object filters must be passed as a dictionary.")
|
raise forms.ValidationError("Invalid format. Object filters must be passed as a dictionary.")
|
||||||
for model in get_models_from_content_types(self.cleaned_data.get('models')):
|
|
||||||
try:
|
|
||||||
# Validate the filters by creating a QuerySet
|
|
||||||
model.objects.filter(**data).none()
|
|
||||||
except Exception:
|
|
||||||
model_name = model._meta.verbose_name_plural
|
|
||||||
raise forms.ValidationError(f"Invalid filter specification for {model_name}.")
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def render(self, request):
|
def render(self, request):
|
||||||
@ -171,13 +166,19 @@ class ObjectCountsWidget(DashboardWidget):
|
|||||||
for model in get_models_from_content_types(self.config['models']):
|
for model in get_models_from_content_types(self.config['models']):
|
||||||
permission = get_permission_for_model(model, 'view')
|
permission = get_permission_for_model(model, 'view')
|
||||||
if request.user.has_perm(permission):
|
if request.user.has_perm(permission):
|
||||||
|
url = reverse(get_viewname(model, 'list'))
|
||||||
qs = model.objects.restrict(request.user, 'view')
|
qs = model.objects.restrict(request.user, 'view')
|
||||||
|
# Apply any specified filters
|
||||||
if filters := self.config.get('filters'):
|
if filters := self.config.get('filters'):
|
||||||
qs = qs.filter(**filters)
|
params = QueryDict(mutable=True)
|
||||||
|
params.update(filters)
|
||||||
|
filterset = getattr(resolve(url).func.view_class, 'filterset', None)
|
||||||
|
qs = filterset(params, qs).qs
|
||||||
|
url = f'{url}?{params.urlencode()}'
|
||||||
object_count = qs.count
|
object_count = qs.count
|
||||||
counts.append((model, object_count))
|
counts.append((model, object_count, url))
|
||||||
else:
|
else:
|
||||||
counts.append((model, None))
|
counts.append((model, None, None))
|
||||||
|
|
||||||
return render_to_string(self.template_name, {
|
return render_to_string(self.template_name, {
|
||||||
'counts': counts,
|
'counts': counts,
|
||||||
|
@ -7,12 +7,14 @@ class Empty(Lookup):
|
|||||||
Filter on whether a string is empty.
|
Filter on whether a string is empty.
|
||||||
"""
|
"""
|
||||||
lookup_name = 'empty'
|
lookup_name = 'empty'
|
||||||
|
prepare_rhs = False
|
||||||
|
|
||||||
def as_sql(self, qn, connection):
|
def as_sql(self, compiler, connection):
|
||||||
lhs, lhs_params = self.process_lhs(qn, connection)
|
sql, params = compiler.compile(self.lhs)
|
||||||
rhs, rhs_params = self.process_rhs(qn, connection)
|
if self.rhs:
|
||||||
params = lhs_params + rhs_params
|
return f"CAST(LENGTH({sql}) AS BOOLEAN) IS NOT TRUE", params
|
||||||
return 'CAST(LENGTH(%s) AS BOOLEAN) != %s' % (lhs, rhs), params
|
else:
|
||||||
|
return f"CAST(LENGTH({sql}) AS BOOLEAN) IS TRUE", params
|
||||||
|
|
||||||
|
|
||||||
class NetContainsOrEquals(Lookup):
|
class NetContainsOrEquals(Lookup):
|
||||||
|
@ -274,10 +274,10 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
|
|
||||||
:param context: The context passed to Jinja2
|
:param context: The context passed to Jinja2
|
||||||
"""
|
"""
|
||||||
text = render_jinja2(self.link_text, context)
|
text = render_jinja2(self.link_text, context).strip()
|
||||||
if not text:
|
if not text:
|
||||||
return {}
|
return {}
|
||||||
link = render_jinja2(self.link_url, context)
|
link = render_jinja2(self.link_url, context).strip()
|
||||||
link_target = ' target="_blank"' if self.new_window else ''
|
link_target = ' target="_blank"' if self.new_window else ''
|
||||||
|
|
||||||
# Sanitize link text
|
# Sanitize link text
|
||||||
|
@ -22,6 +22,14 @@ __all__ = (
|
|||||||
'WebhookTable',
|
'WebhookTable',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
IMAGEATTACHMENT_IMAGE = '''
|
||||||
|
{% if record.image %}
|
||||||
|
<a class="image-preview" href="{{ record.image.url }}" target="_blank">{{ record }}</a>
|
||||||
|
{% else %}
|
||||||
|
—
|
||||||
|
{% endif %}
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldTable(NetBoxTable):
|
class CustomFieldTable(NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
@ -96,6 +104,9 @@ class ImageAttachmentTable(NetBoxTable):
|
|||||||
parent = tables.Column(
|
parent = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
image = tables.TemplateColumn(
|
||||||
|
template_code=IMAGEATTACHMENT_IMAGE,
|
||||||
|
)
|
||||||
size = tables.Column(
|
size = tables.Column(
|
||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Size (bytes)'
|
verbose_name='Size (bytes)'
|
||||||
|
@ -9,6 +9,7 @@ from netbox.config import get_config
|
|||||||
from netbox.constants import RQ_QUEUE_DEFAULT
|
from netbox.constants import RQ_QUEUE_DEFAULT
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
|
from utilities.rqworker import get_rq_retry
|
||||||
from utilities.utils import serialize_object
|
from utilities.utils import serialize_object
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import Webhook
|
from .models import Webhook
|
||||||
@ -116,5 +117,6 @@ def flush_webhooks(queue):
|
|||||||
snapshots=data['snapshots'],
|
snapshots=data['snapshots'],
|
||||||
timestamp=str(timezone.now()),
|
timestamp=str(timezone.now()),
|
||||||
username=data['username'],
|
username=data['username'],
|
||||||
request_id=data['request_id']
|
request_id=data['request_id'],
|
||||||
|
retry=get_rq_retry()
|
||||||
)
|
)
|
||||||
|
@ -328,6 +328,12 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
|||||||
):
|
):
|
||||||
self.initial['primary_for_parent'] = True
|
self.initial['primary_for_parent'] = True
|
||||||
|
|
||||||
|
# Disable object assignment fields if the IP address is designated as primary
|
||||||
|
if self.initial.get('primary_for_parent'):
|
||||||
|
self.fields['interface'].disabled = True
|
||||||
|
self.fields['vminterface'].disabled = True
|
||||||
|
self.fields['fhrpgroup'].disabled = True
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
@ -340,7 +346,12 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
|||||||
selected_objects[1]: "An IP address can only be assigned to a single object."
|
selected_objects[1]: "An IP address can only be assigned to a single object."
|
||||||
})
|
})
|
||||||
elif selected_objects:
|
elif selected_objects:
|
||||||
self.instance.assigned_object = self.cleaned_data[selected_objects[0]]
|
assigned_object = self.cleaned_data[selected_objects[0]]
|
||||||
|
if self.cleaned_data['primary_for_parent'] and assigned_object != self.instance.assigned_object:
|
||||||
|
raise ValidationError(
|
||||||
|
"Cannot reassign IP address while it is designated as the primary IP for the parent object"
|
||||||
|
)
|
||||||
|
self.instance.assigned_object = assigned_object
|
||||||
else:
|
else:
|
||||||
self.instance.assigned_object = None
|
self.instance.assigned_object = None
|
||||||
|
|
||||||
@ -351,6 +362,18 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
|||||||
'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
|
'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Do not allow assigning a network ID or broadcast address to an interface.
|
||||||
|
if interface and (address := self.cleaned_data.get('address')):
|
||||||
|
if address.ip == address.network:
|
||||||
|
msg = f"{address} is a network ID, which may not be assigned to an interface."
|
||||||
|
if address.version == 4 and address.prefixlen not in (31, 32):
|
||||||
|
raise ValidationError(msg)
|
||||||
|
if address.version == 6 and address.prefixlen not in (127, 128):
|
||||||
|
raise ValidationError(msg)
|
||||||
|
if address.ip == address.broadcast:
|
||||||
|
msg = f"{address} is a broadcast address, which may not be assigned to an interface."
|
||||||
|
raise ValidationError(msg)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
ipaddress = super().save(*args, **kwargs)
|
ipaddress = super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -15,11 +15,12 @@ from utilities.api import get_serializer_for_model
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'BriefModeMixin',
|
'BriefModeMixin',
|
||||||
|
'BulkDestroyModelMixin',
|
||||||
'BulkUpdateModelMixin',
|
'BulkUpdateModelMixin',
|
||||||
'CustomFieldsMixin',
|
'CustomFieldsMixin',
|
||||||
'ExportTemplatesMixin',
|
'ExportTemplatesMixin',
|
||||||
'BulkDestroyModelMixin',
|
|
||||||
'ObjectValidationMixin',
|
'ObjectValidationMixin',
|
||||||
|
'SequentialBulkCreatesMixin',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -94,6 +95,30 @@ class ExportTemplatesMixin:
|
|||||||
return super().list(request, *args, **kwargs)
|
return super().list(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SequentialBulkCreatesMixin:
|
||||||
|
"""
|
||||||
|
Perform bulk creation of new objects sequentially, rather than all at once. This ensures that any validation
|
||||||
|
which depends on the evaluation of existing objects (such as checking for free space within a rack) functions
|
||||||
|
appropriately.
|
||||||
|
"""
|
||||||
|
@transaction.atomic
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
if not isinstance(request.data, list):
|
||||||
|
# Creating a single object
|
||||||
|
return super().create(request, *args, **kwargs)
|
||||||
|
|
||||||
|
return_data = []
|
||||||
|
for data in request.data:
|
||||||
|
serializer = self.get_serializer(data=data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
self.perform_create(serializer)
|
||||||
|
return_data.append(serializer.data)
|
||||||
|
|
||||||
|
headers = self.get_success_headers(serializer.data)
|
||||||
|
|
||||||
|
return Response(return_data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
class BulkUpdateModelMixin:
|
class BulkUpdateModelMixin:
|
||||||
"""
|
"""
|
||||||
Support bulk modification of objects using the list endpoint for a model. Accepts a PATCH action with a list of one
|
Support bulk modification of objects using the list endpoint for a model. Accepts a PATCH action with a list of one
|
||||||
|
@ -177,7 +177,8 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
# create the new filter with the same type because there is no guarantee the defined type
|
# create the new filter with the same type because there is no guarantee the defined type
|
||||||
# is the same as the default type for the field
|
# is the same as the default type for the field
|
||||||
resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid
|
resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid
|
||||||
new_filter = type(existing_filter)(
|
filter_cls = django_filters.BooleanFilter if lookup_expr == 'empty' else type(existing_filter)
|
||||||
|
new_filter = filter_cls(
|
||||||
field_name=field_name,
|
field_name=field_name,
|
||||||
lookup_expr=lookup_expr,
|
lookup_expr=lookup_expr,
|
||||||
label=existing_filter.label,
|
label=existing_filter.label,
|
||||||
@ -224,6 +225,14 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
|
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def filter_for_lookup(cls, field, lookup_type):
|
||||||
|
|
||||||
|
if lookup_type == 'empty':
|
||||||
|
return django_filters.BooleanFilter, {}
|
||||||
|
|
||||||
|
return super().filter_for_lookup(field, lookup_type)
|
||||||
|
|
||||||
|
|
||||||
class ChangeLoggedModelFilterSet(BaseFilterSet):
|
class ChangeLoggedModelFilterSet(BaseFilterSet):
|
||||||
"""
|
"""
|
||||||
|
@ -181,19 +181,23 @@ class MaintenanceModeMiddleware:
|
|||||||
|
|
||||||
def __call__(self, request):
|
def __call__(self, request):
|
||||||
if get_config().MAINTENANCE_MODE:
|
if get_config().MAINTENANCE_MODE:
|
||||||
self._prevent_db_write_operations()
|
self._set_session_type(
|
||||||
|
allow_write=request.path_info.startswith(settings.MAINTENANCE_EXEMPT_PATHS)
|
||||||
|
)
|
||||||
|
|
||||||
return self.get_response(request)
|
return self.get_response(request)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _prevent_db_write_operations():
|
def _set_session_type(allow_write):
|
||||||
"""
|
"""
|
||||||
Prevent any write-related database operations.
|
Prevent any write-related database operations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
allow_write (bool): If True, write operations will be permitted.
|
||||||
"""
|
"""
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute(
|
mode = 'READ WRITE' if allow_write else 'READ ONLY'
|
||||||
'SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;'
|
cursor.execute(f'SET SESSION CHARACTERISTICS AS TRANSACTION {mode};')
|
||||||
)
|
|
||||||
|
|
||||||
def process_exception(self, request, exception):
|
def process_exception(self, request, exception):
|
||||||
"""
|
"""
|
||||||
|
@ -197,11 +197,15 @@ class CustomFieldsMixin(models.Model):
|
|||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
for field in CustomField.objects.get_for_model(self):
|
for field in CustomField.objects.get_for_model(self):
|
||||||
# Skip fields that are hidden if 'omit_hidden' is set
|
|
||||||
if omit_hidden and field.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN:
|
|
||||||
continue
|
|
||||||
|
|
||||||
value = self.custom_field_data.get(field.name)
|
value = self.custom_field_data.get(field.name)
|
||||||
|
|
||||||
|
# Skip fields that are hidden if 'omit_hidden' is set
|
||||||
|
if omit_hidden:
|
||||||
|
if field.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN:
|
||||||
|
continue
|
||||||
|
if field.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN_IFUNSET and not value:
|
||||||
|
continue
|
||||||
|
|
||||||
data[field] = field.deserialize(value)
|
data[field] = field.deserialize(value)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@ -227,6 +231,8 @@ class CustomFieldsMixin(models.Model):
|
|||||||
|
|
||||||
for cf in visible_custom_fields:
|
for cf in visible_custom_fields:
|
||||||
value = self.custom_field_data.get(cf.name)
|
value = self.custom_field_data.get(cf.name)
|
||||||
|
if value in (None, []) and cf.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN_IFUNSET:
|
||||||
|
continue
|
||||||
value = cf.deserialize(value)
|
value = cf.deserialize(value)
|
||||||
groups[cf.group_name][cf] = value
|
groups[cf.group_name][cf] = value
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '3.5.2-dev'
|
VERSION = '3.5.4-dev'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
@ -140,6 +140,8 @@ REMOTE_AUTH_STAFF_USERS = getattr(configuration, 'REMOTE_AUTH_STAFF_USERS', [])
|
|||||||
REMOTE_AUTH_GROUP_SEPARATOR = getattr(configuration, 'REMOTE_AUTH_GROUP_SEPARATOR', '|')
|
REMOTE_AUTH_GROUP_SEPARATOR = getattr(configuration, 'REMOTE_AUTH_GROUP_SEPARATOR', '|')
|
||||||
REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/')
|
REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/')
|
||||||
RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
|
RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
|
||||||
|
RQ_RETRY_INTERVAL = getattr(configuration, 'RQ_RETRY_INTERVAL', 60)
|
||||||
|
RQ_RETRY_MAX = getattr(configuration, 'RQ_RETRY_MAX', 0)
|
||||||
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
|
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
|
||||||
SEARCH_BACKEND = getattr(configuration, 'SEARCH_BACKEND', 'netbox.search.backends.CachedValueSearchBackend')
|
SEARCH_BACKEND = getattr(configuration, 'SEARCH_BACKEND', 'netbox.search.backends.CachedValueSearchBackend')
|
||||||
SECURE_SSL_REDIRECT = getattr(configuration, 'SECURE_SSL_REDIRECT', False)
|
SECURE_SSL_REDIRECT = getattr(configuration, 'SECURE_SSL_REDIRECT', False)
|
||||||
@ -478,6 +480,11 @@ AUTH_EXEMPT_PATHS = (
|
|||||||
f'/{BASE_PATH}metrics',
|
f'/{BASE_PATH}metrics',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# All URLs starting with a string listed here are exempt from maintenance mode enforcement
|
||||||
|
MAINTENANCE_EXEMPT_PATHS = (
|
||||||
|
f'/{BASE_PATH}admin/',
|
||||||
|
)
|
||||||
|
|
||||||
SERIALIZATION_MODULES = {
|
SERIALIZATION_MODULES = {
|
||||||
'json': 'utilities.serializers.json',
|
'json': 'utilities.serializers.json',
|
||||||
}
|
}
|
||||||
|
@ -234,8 +234,12 @@ class ActionsColumn(tables.Column):
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
model = table.Meta.model
|
model = table.Meta.model
|
||||||
request = getattr(table, 'context', {}).get('request')
|
if request := getattr(table, 'context', {}).get('request'):
|
||||||
url_appendix = f'?return_url={quote(request.get_full_path())}' if request else ''
|
return_url = request.GET.get('return_url', request.get_full_path())
|
||||||
|
url_appendix = f'?return_url={quote(return_url)}'
|
||||||
|
else:
|
||||||
|
url_appendix = ''
|
||||||
|
|
||||||
html = ''
|
html = ''
|
||||||
|
|
||||||
# Compile actions menu
|
# Compile actions menu
|
||||||
|
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -30,6 +30,7 @@
|
|||||||
"dayjs": "^1.11.5",
|
"dayjs": "^1.11.5",
|
||||||
"flatpickr": "4.6.13",
|
"flatpickr": "4.6.13",
|
||||||
"gridstack": "^7.2.3",
|
"gridstack": "^7.2.3",
|
||||||
|
"html-entities": "^2.3.3",
|
||||||
"htmx.org": "^1.8.0",
|
"htmx.org": "^1.8.0",
|
||||||
"just-debounce-it": "^3.1.1",
|
"just-debounce-it": "^3.1.1",
|
||||||
"query-string": "^7.1.1",
|
"query-string": "^7.1.1",
|
||||||
|
@ -2,9 +2,10 @@ import { getElements, isTruthy } from './util';
|
|||||||
import { initButtons } from './buttons';
|
import { initButtons } from './buttons';
|
||||||
import { initSelect } from './select';
|
import { initSelect } from './select';
|
||||||
import { initObjectSelector } from './objectSelector';
|
import { initObjectSelector } from './objectSelector';
|
||||||
|
import { initBootstrap } from './bs';
|
||||||
|
|
||||||
function initDepedencies(): void {
|
function initDepedencies(): void {
|
||||||
for (const init of [initButtons, initSelect, initObjectSelector]) {
|
for (const init of [initButtons, initSelect, initObjectSelector, initBootstrap]) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -22,4 +23,8 @@ export function initHtmx(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const element of getElements('[hx-trigger=load]')) {
|
||||||
|
element.addEventListener('htmx:afterSettle', initDepedencies);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { readableColor } from 'color2k';
|
import { readableColor } from 'color2k';
|
||||||
import debounce from 'just-debounce-it';
|
import debounce from 'just-debounce-it';
|
||||||
|
import { encode } from 'html-entities';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
import SlimSelect from 'slim-select';
|
import SlimSelect from 'slim-select';
|
||||||
import { createToast } from '../../bs';
|
import { createToast } from '../../bs';
|
||||||
@ -446,7 +447,7 @@ export class APISelect {
|
|||||||
// Build SlimSelect options from all already-selected options.
|
// Build SlimSelect options from all already-selected options.
|
||||||
const preSelectedOptions = preSelected.map(option => ({
|
const preSelectedOptions = preSelected.map(option => ({
|
||||||
value: option.value,
|
value: option.value,
|
||||||
text: option.innerText,
|
text: encode(option.innerText),
|
||||||
selected: true,
|
selected: true,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
})) as Option[];
|
})) as Option[];
|
||||||
@ -454,7 +455,7 @@ export class APISelect {
|
|||||||
let options = [] as Option[];
|
let options = [] as Option[];
|
||||||
|
|
||||||
for (const result of data.results) {
|
for (const result of data.results) {
|
||||||
let text = result.display;
|
let text = encode(result.display);
|
||||||
|
|
||||||
if (typeof result._depth === 'number' && result._depth > 0) {
|
if (typeof result._depth === 'number' && result._depth > 0) {
|
||||||
// If the object has a `_depth` property, indent its display text.
|
// If the object has a `_depth` property, indent its display text.
|
||||||
|
@ -1818,6 +1818,11 @@ has@^1.0.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind "^1.1.1"
|
function-bind "^1.1.1"
|
||||||
|
|
||||||
|
html-entities@^2.3.3:
|
||||||
|
version "2.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46"
|
||||||
|
integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==
|
||||||
|
|
||||||
htmx.org@^1.8.0:
|
htmx.org@^1.8.0:
|
||||||
version "1.8.0"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/htmx.org/-/htmx.org-1.8.0.tgz#f3a2f681f3e2b6357b5a29bba24a2572a8e48fd3"
|
resolved "https://registry.yarnpkg.com/htmx.org/-/htmx.org-1.8.0.tgz#f3a2f681f3e2b6357b5a29bba24a2572a8e48fd3"
|
||||||
|
@ -28,8 +28,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-7">
|
<div class="col-7">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">Context Data</h5>
|
<div class="accordion accordion-flush" id="renderConfig">
|
||||||
<pre class="card-body">{{ context_data|pprint }}</pre>
|
<div class="card-body">
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="renderConfigHeading">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsedRenderConfig" aria-expanded="false" aria-controls="collapsedRenderConfig">
|
||||||
|
Context Data
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapsedRenderConfig" class="accordion-collapse collapse" aria-labelledby="renderConfigHeading" data-bs-parent="#renderConfig">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<pre class="card-body">{{ context_data|pprint }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -123,11 +123,11 @@
|
|||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">MAC Address</th>
|
<th scope="row">MAC Address</th>
|
||||||
<td><span class="text-monospace">{{ object.mac_address|placeholder }}</span></td>
|
<td><span class="font-monospace">{{ object.mac_address|placeholder }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">WWN</th>
|
<th scope="row">WWN</th>
|
||||||
<td><span class="text-monospace">{{ object.wwn|placeholder }}</span></td>
|
<td><span class="font-monospace">{{ object.wwn|placeholder }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">VRF</th>
|
<th scope="row">VRF</th>
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
{% load helpers %}
|
|
||||||
|
|
||||||
{% if counts %}
|
{% if counts %}
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
{% for model, count in counts %}
|
{% for model, count, url in counts %}
|
||||||
{% if count != None %}
|
{% if count != None %}
|
||||||
<a href="{% url model|viewname:"list" %}" class="list-group-item list-group-item-action">
|
<a href="{{ url }}" 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">
|
||||||
{{ model|meta:"verbose_name_plural"|bettertitle }}
|
{{ model|meta:"verbose_name_plural"|bettertitle }}
|
||||||
<h6 class="mb-1">{{ count }}</h6>
|
<h6 class="mb-1">{{ count }}</h6>
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">Images</h5>
|
||||||
Images
|
{% htmx_table 'extras:imageattachment_list' content_type_id=object|content_type_id object_id=object.pk %}
|
||||||
</h5>
|
|
||||||
<div class="card-body htmx-container table-responsive"
|
|
||||||
hx-get="{% url 'extras:imageattachment_list' %}?content_type_id={{ object|content_type_id }}&object_id={{ object.pk }}"
|
|
||||||
hx-trigger="load"></div>
|
|
||||||
{% if perms.extras.add_imageattachment %}
|
{% if perms.extras.add_imageattachment %}
|
||||||
<div class="card-footer text-end noprint">
|
<div class="card-footer text-end noprint">
|
||||||
<a href="{% url 'extras:imageattachment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}" class="btn btn-primary btn-sm">
|
<a href="{% url 'extras:imageattachment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}" class="btn btn-primary btn-sm">
|
||||||
|
@ -59,7 +59,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">MAC Address</th>
|
<th scope="row">MAC Address</th>
|
||||||
<td><span class="text-monospace">{{ object.mac_address|placeholder }}</span></td>
|
<td><span class="font-monospace">{{ object.mac_address|placeholder }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">802.1Q Mode</th>
|
<th scope="row">802.1Q Mode</th>
|
||||||
|
@ -15,21 +15,31 @@ from .models import *
|
|||||||
|
|
||||||
|
|
||||||
class ObjectContactsView(generic.ObjectChildrenView):
|
class ObjectContactsView(generic.ObjectChildrenView):
|
||||||
child_model = Contact
|
child_model = ContactAssignment
|
||||||
table = tables.ContactTable
|
table = tables.ContactAssignmentTable
|
||||||
filterset = filtersets.ContactFilterSet
|
filterset = filtersets.ContactAssignmentFilterSet
|
||||||
template_name = 'tenancy/object_contacts.html'
|
template_name = 'tenancy/object_contacts.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Contacts'),
|
label=_('Contacts'),
|
||||||
badge=lambda obj: obj.contacts.count(),
|
badge=lambda obj: obj.contacts.count(),
|
||||||
permission='tenancy.view_contact',
|
permission='tenancy.view_contactassignment',
|
||||||
weight=5000
|
weight=5000
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_children(self, request, parent):
|
def get_children(self, request, parent):
|
||||||
return Contact.objects.annotate(
|
return ContactAssignment.objects.restrict(request.user, 'view').filter(
|
||||||
assignment_count=count_related(ContactAssignment, 'contact')
|
content_type=ContentType.objects.get_for_model(parent),
|
||||||
).restrict(request.user, 'view').filter(assignments__object_id=parent.pk)
|
object_id=parent.pk
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_table(self, *args, **kwargs):
|
||||||
|
table = super().get_table(*args, **kwargs)
|
||||||
|
|
||||||
|
# Hide object columns
|
||||||
|
table.columns.hide('content_type')
|
||||||
|
table.columns.hide('object')
|
||||||
|
|
||||||
|
return table
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
return {
|
return {
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
import logging
|
import logging
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.contrib.auth.signals import user_login_failed
|
from django.contrib.auth.signals import user_login_failed
|
||||||
|
from utilities.request import get_client_ip
|
||||||
|
|
||||||
|
|
||||||
@receiver(user_login_failed)
|
@receiver(user_login_failed)
|
||||||
def log_user_login_failed(sender, credentials, request, **kwargs):
|
def log_user_login_failed(sender, credentials, request, **kwargs):
|
||||||
logger = logging.getLogger('netbox.auth.login')
|
logger = logging.getLogger('netbox.auth.login')
|
||||||
username = credentials.get("username")
|
username = credentials.get("username")
|
||||||
logger.info(f"Failed login attempt for username: {username}")
|
if client_ip := get_client_ip(request):
|
||||||
|
logger.info(f"Failed login attempt for username: {username} from {client_ip}")
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Client IP address could not be determined for validation. Check that the HTTP server is properly "
|
||||||
|
"configured to pass the required header(s)."
|
||||||
|
)
|
||||||
|
logger.info(f"Failed login attempt for username: {username}")
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
from django_rq.queues import get_connection
|
from django_rq.queues import get_connection
|
||||||
from rq import Worker
|
from rq import Retry, Worker
|
||||||
|
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
from netbox.constants import RQ_QUEUE_DEFAULT
|
from netbox.constants import RQ_QUEUE_DEFAULT
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'get_queue_for_model',
|
'get_queue_for_model',
|
||||||
|
'get_rq_retry',
|
||||||
'get_workers_for_queue',
|
'get_workers_for_queue',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -22,3 +23,14 @@ def get_workers_for_queue(queue_name):
|
|||||||
Returns True if a worker process is currently servicing the specified queue.
|
Returns True if a worker process is currently servicing the specified queue.
|
||||||
"""
|
"""
|
||||||
return Worker.count(get_connection(queue_name))
|
return Worker.count(get_connection(queue_name))
|
||||||
|
|
||||||
|
|
||||||
|
def get_rq_retry():
|
||||||
|
"""
|
||||||
|
If RQ_RETRY_MAX is defined and greater than zero, instantiate and return a Retry object to be
|
||||||
|
used when queuing a job. Otherwise, return None.
|
||||||
|
"""
|
||||||
|
retry_max = get_config().RQ_RETRY_MAX
|
||||||
|
retry_interval = get_config().RQ_RETRY_INTERVAL
|
||||||
|
if retry_max:
|
||||||
|
return Retry(max=retry_max, interval=retry_interval)
|
||||||
|
4
netbox/utilities/templates/builtins/htmx_table.html
Normal file
4
netbox/utilities/templates/builtins/htmx_table.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<div class="card-body htmx-container table-responsive"
|
||||||
|
hx-get="{% url viewname %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}"
|
||||||
|
hx-trigger="load"
|
||||||
|
></div>
|
@ -1,4 +1,5 @@
|
|||||||
from django import template
|
from django import template
|
||||||
|
from django.http import QueryDict
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'badge',
|
'badge',
|
||||||
@ -74,3 +75,22 @@ def checkmark(value, show_false=True, true='Yes', false='No'):
|
|||||||
'true_label': true,
|
'true_label': true,
|
||||||
'false_label': false,
|
'false_label': false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register.inclusion_tag('builtins/htmx_table.html', takes_context=True)
|
||||||
|
def htmx_table(context, viewname, return_url=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Embed an object list table retrieved using HTMX. Any extra keyword arguments are passed as URL query parameters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: The current request context
|
||||||
|
viewname: The name of the view to use for the HTMX request (e.g. `dcim:site_list`)
|
||||||
|
return_url: The URL to pass as the `return_url`. If not provided, the current request's path will be used.
|
||||||
|
"""
|
||||||
|
url_params = QueryDict(mutable=True)
|
||||||
|
url_params.update(kwargs)
|
||||||
|
url_params['return_url'] = return_url or context['request'].path
|
||||||
|
return {
|
||||||
|
'viewname': viewname,
|
||||||
|
'url_params': url_params,
|
||||||
|
}
|
||||||
|
@ -302,7 +302,7 @@ def to_meters(length, unit):
|
|||||||
if unit == CableLengthUnitChoices.UNIT_FOOT:
|
if unit == CableLengthUnitChoices.UNIT_FOOT:
|
||||||
return length * Decimal(0.3048)
|
return length * Decimal(0.3048)
|
||||||
if unit == CableLengthUnitChoices.UNIT_INCH:
|
if unit == CableLengthUnitChoices.UNIT_INCH:
|
||||||
return length * Decimal(0.3048) * 12
|
return length * Decimal(0.0254)
|
||||||
raise ValueError(f"Unknown unit {unit}. Must be 'km', 'm', 'cm', 'mi', 'ft', or 'in'.")
|
raise ValueError(f"Unknown unit {unit}. Must be 'km', 'm', 'cm', 'mi', 'ft', or 'in'.")
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ class ClusterImportForm(NetBoxModelImportForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Cluster
|
model = Cluster
|
||||||
fields = ('name', 'type', 'group', 'status', 'site', 'description', 'comments', 'tags')
|
fields = ('name', 'type', 'group', 'status', 'site', 'tenant', 'description', 'comments', 'tags')
|
||||||
|
|
||||||
|
|
||||||
class VirtualMachineImportForm(NetBoxModelImportForm):
|
class VirtualMachineImportForm(NetBoxModelImportForm):
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
bleach==6.0.0
|
bleach==6.0.0
|
||||||
boto3==1.26.127
|
boto3==1.26.145
|
||||||
Django==4.1.9
|
Django==4.1.9
|
||||||
django-cors-headers==3.14.0
|
django-cors-headers==4.0.0
|
||||||
django-debug-toolbar==4.0.0
|
django-debug-toolbar==4.1.0
|
||||||
django-filter==23.2
|
django-filter==23.2
|
||||||
django-graphiql-debug-toolbar==0.2.0
|
django-graphiql-debug-toolbar==0.2.0
|
||||||
django-mptt==0.14
|
django-mptt==0.14
|
||||||
@ -10,26 +10,26 @@ django-pglocks==1.0.4
|
|||||||
django-prometheus==2.3.1
|
django-prometheus==2.3.1
|
||||||
django-redis==5.2.0
|
django-redis==5.2.0
|
||||||
django-rich==1.5.0
|
django-rich==1.5.0
|
||||||
django-rq==2.8.0
|
django-rq==2.8.1
|
||||||
django-tables2==2.5.3
|
django-tables2==2.5.3
|
||||||
django-taggit==4.0.0
|
django-taggit==4.0.0
|
||||||
django-timezone-field==5.0
|
django-timezone-field==5.0
|
||||||
djangorestframework==3.14.0
|
djangorestframework==3.14.0
|
||||||
drf-spectacular==0.26.2
|
drf-spectacular==0.26.2
|
||||||
drf-spectacular-sidecar==2023.5.1
|
drf-spectacular-sidecar==2023.6.1
|
||||||
dulwich==0.21.5
|
dulwich==0.21.5
|
||||||
feedparser==6.0.10
|
feedparser==6.0.10
|
||||||
graphene-django==3.0.0
|
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==9.1.9
|
mkdocs-material==9.1.15
|
||||||
mkdocstrings[python-legacy]==0.21.2
|
mkdocstrings[python-legacy]==0.22.0
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==9.5.0
|
Pillow==9.5.0
|
||||||
psycopg2-binary==2.9.6
|
psycopg2-binary==2.9.6
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
sentry-sdk==1.22.1
|
sentry-sdk==1.25.0
|
||||||
social-auth-app-django==5.2.0
|
social-auth-app-django==5.2.0
|
||||||
social-auth-core[openidconnect]==4.4.2
|
social-auth-core[openidconnect]==4.4.2
|
||||||
svgwrite==1.4.3
|
svgwrite==1.4.3
|
||||||
|
Loading…
Reference in New Issue
Block a user