mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-05 16:39:32 +01:00
Compare commits
53 Commits
v3.6-beta2
...
v3.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfcfbe240d | ||
|
|
b040fdcf2c | ||
|
|
8525f994c0 | ||
|
|
eb9a804914 | ||
|
|
210d7bb573 | ||
|
|
dc85476b9e | ||
|
|
1854a6b76b | ||
|
|
aebf3288d1 | ||
|
|
065a40dfb3 | ||
|
|
83536fbb23 | ||
|
|
420090dc6c | ||
|
|
4ab0eb570c | ||
|
|
2a4e3dd09f | ||
|
|
0dbfbf6941 | ||
|
|
d515530277 | ||
|
|
4343e0566b | ||
|
|
8555269f7e | ||
|
|
f42a2ac10c | ||
|
|
4ea3a29c0e | ||
|
|
29877c9abe | ||
|
|
480f83c42d | ||
|
|
faf89350ac | ||
|
|
d9c3ce935f | ||
|
|
8d8f57e8b8 | ||
|
|
0a3be0b7ea | ||
|
|
00ebdfe0df | ||
|
|
d79fa131bb | ||
|
|
be2b24a155 | ||
|
|
03b341dbfd | ||
|
|
ca5e69897d | ||
|
|
3090dd4934 | ||
|
|
1f1d1ee502 | ||
|
|
1c2cf11f47 | ||
|
|
08961e751d | ||
|
|
88bf82be05 | ||
|
|
506884bc4d | ||
|
|
646fa341ab | ||
|
|
d73f7b1943 | ||
|
|
a75e8416a4 | ||
|
|
f743f2cfb8 | ||
|
|
7d7e8127f5 | ||
|
|
3c0a3ca703 | ||
|
|
45062697c5 | ||
|
|
66e4e31209 | ||
|
|
c86cfe3cbf | ||
|
|
28e112743f | ||
|
|
229007082b | ||
|
|
4004966b16 | ||
|
|
fe95cb434a | ||
|
|
16e2283d19 | ||
|
|
c46536f469 | ||
|
|
9450ce4c3a | ||
|
|
8f5005efd5 |
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.5.8
|
||||
placeholder: v3.6.0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.5.8
|
||||
placeholder: v3.6.0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
@@ -332,6 +332,7 @@
|
||||
"100gbase-x-cfp",
|
||||
"100gbase-x-cfp2",
|
||||
"200gbase-x-cfp2",
|
||||
"400gbase-x-cfp2",
|
||||
"100gbase-x-cfp4",
|
||||
"100gbase-x-cxp",
|
||||
"100gbase-x-cpak",
|
||||
|
||||
@@ -54,7 +54,7 @@ pg_dump --username netbox --password --host localhost -s netbox > netbox_schema.
|
||||
By default, NetBox stores uploaded files (such as image attachments) in its media directory. To fully replicate an instance of NetBox, you'll need to copy both the database and the media files.
|
||||
|
||||
!!! note
|
||||
These operations are not necessary if your installation is utilizing a [remote storage backend](../../configuration/optional-settings/#storage_backend).
|
||||
These operations are not necessary if your installation is utilizing a [remote storage backend](../configuration/system.md#storage_backend).
|
||||
|
||||
### Archive the Media Directory
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Default: Empty
|
||||
|
||||
A list of installed [NetBox plugins](../../plugins/) to enable. Plugins will not take effect unless they are listed here.
|
||||
A list of installed [NetBox plugins](../plugins/index.md) to enable. Plugins will not take effect unless they are listed here.
|
||||
|
||||
!!! warning
|
||||
Plugins extend NetBox by allowing external code to run with the same access and privileges as NetBox itself. Only install plugins from trusted sources. The NetBox maintainers make absolutely no guarantees about the integrity or security of your installation with plugins enabled.
|
||||
|
||||
@@ -111,7 +111,7 @@ The following methods are available to log results within a report:
|
||||
|
||||
The recording of one or more failure messages will automatically flag a report as failed. It is advised to log a success for each object that is evaluated so that the results will reflect how many objects are being reported on. (The inclusion of a log message is optional for successes.) Messages recorded with `log()` will appear in a report's results but are not associated with a particular object or status. Log messages also support using markdown syntax and will be rendered on the report result page.
|
||||
|
||||
To perform additional tasks, such as sending an email or calling a webhook, before or after a report is run, extend the `pre_run()` and/or `post_run()` methods, respectively. The status of a completed report is available as `self.failed` and the results object is `self.result`.
|
||||
To perform additional tasks, such as sending an email or calling a webhook, before or after a report is run, extend the `pre_run()` and/or `post_run()` methods, respectively.
|
||||
|
||||
By default, reports within a module are ordered alphabetically in the reports list page. To return reports in a specific order, you can define the `report_order` variable at the end of your module. The `report_order` variable is a tuple which contains each Report class in the desired order. Any reports that are omitted from this list will be listed last.
|
||||
|
||||
|
||||
@@ -37,6 +37,14 @@ Configuration templates are written in the [Jinja2 templating language](https://
|
||||
|
||||
When rendered for a specific NetBox device, the template's `device` variable will be populated with the device instance, and `ntp_servers` will be pulled from the device's available context data. The resulting output will be a valid configuration segment that can be applied directly to a compatible network device.
|
||||
|
||||
### Context Data
|
||||
|
||||
The objet for which the configuration is being rendered is made available as template context as `device` or `virtualmachine` for devices and virtual machines, respectively. Additionally, NetBox model classes can be accessed by the app or plugin in which they reside. For example:
|
||||
|
||||
```
|
||||
There are {{ dcim.Site.objects.count() }} sites.
|
||||
```
|
||||
|
||||
## Rendering Templates
|
||||
|
||||
### Device Configurations
|
||||
|
||||
@@ -59,7 +59,7 @@ Copy `local_requirements.txt`, `configuration.py`, and `ldap_config.py` (if pres
|
||||
|
||||
```no-highlight
|
||||
# Set $OLDVER to the NetBox version currently installed
|
||||
NEWVER=3.4.9
|
||||
OLDVER=3.4.9
|
||||
sudo cp /opt/netbox-$OLDVER/local_requirements.txt /opt/netbox/
|
||||
sudo cp /opt/netbox-$OLDVER/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/
|
||||
sudo cp /opt/netbox-$OLDVER/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 22 KiB |
@@ -61,13 +61,14 @@ These lookup expressions can be applied by adding a suffix to the desired field'
|
||||
|
||||
Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions:
|
||||
|
||||
| Filter | Description |
|
||||
|--------|-------------|
|
||||
| `n` | Not equal to |
|
||||
| `lt` | Less than |
|
||||
| `lte` | Less than or equal to |
|
||||
| `gt` | Greater than |
|
||||
| `gte` | Greater than or equal to |
|
||||
| Filter | Description |
|
||||
|---------|--------------------------|
|
||||
| `n` | Not equal to |
|
||||
| `lt` | Less than |
|
||||
| `lte` | Less than or equal to |
|
||||
| `gt` | Greater than |
|
||||
| `gte` | Greater than or equal to |
|
||||
| `empty` | Is empty/null (boolean) |
|
||||
|
||||
Here is an example of a numeric field lookup expression that will return all VLANs with a VLAN ID greater than 900:
|
||||
|
||||
@@ -79,18 +80,18 @@ GET /api/ipam/vlans/?vid__gt=900
|
||||
|
||||
String based (char) fields (Name, Address, etc) support these lookup expressions:
|
||||
|
||||
| Filter | Description |
|
||||
|--------|-------------|
|
||||
| `n` | Not equal to |
|
||||
| `ic` | Contains (case-insensitive) |
|
||||
| `nic` | Does not contain (case-insensitive) |
|
||||
| `isw` | Starts with (case-insensitive) |
|
||||
| `nisw` | Does not start with (case-insensitive) |
|
||||
| `iew` | Ends with (case-insensitive) |
|
||||
| `niew` | Does not end with (case-insensitive) |
|
||||
| `ie` | Exact match (case-insensitive) |
|
||||
| `nie` | Inverse exact match (case-insensitive) |
|
||||
| `empty` | Is empty (boolean) |
|
||||
| Filter | Description |
|
||||
|---------|----------------------------------------|
|
||||
| `n` | Not equal to |
|
||||
| `ic` | Contains (case-insensitive) |
|
||||
| `nic` | Does not contain (case-insensitive) |
|
||||
| `isw` | Starts with (case-insensitive) |
|
||||
| `nisw` | Does not start with (case-insensitive) |
|
||||
| `iew` | Ends with (case-insensitive) |
|
||||
| `niew` | Does not end with (case-insensitive) |
|
||||
| `ie` | Exact match (case-insensitive) |
|
||||
| `nie` | Inverse exact match (case-insensitive) |
|
||||
| `empty` | Is empty/null (boolean) |
|
||||
|
||||
Here is an example of a lookup expression on a string field that will return all devices with `switch` in the name:
|
||||
|
||||
|
||||
@@ -10,6 +10,15 @@ Minor releases are published in April, August, and December of each calendar yea
|
||||
|
||||
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
||||
|
||||
#### [Version 3.6](./version-3.6.md) (August 2023)
|
||||
|
||||
* Relocated Admin UI Views ([#12589](https://github.com/netbox-community/netbox/issues/12589), [#12590](https://github.com/netbox-community/netbox/issues/12590), [#12591](https://github.com/netbox-community/netbox/issues/12591), [#13044](https://github.com/netbox-community/netbox/issues/13044))
|
||||
* Configurable Default Permissions ([#13038](https://github.com/netbox-community/netbox/issues/13038))
|
||||
* User Bookmarks ([#8248](https://github.com/netbox-community/netbox/issues/8248))
|
||||
* Custom Field Choice Sets ([#12988](https://github.com/netbox-community/netbox/issues/12988))
|
||||
* Pre-Defined Location Choices for Custom Fields ([#12194](https://github.com/netbox-community/netbox/issues/12194))
|
||||
* Restrict Tag Usage by Object Type ([#11541](https://github.com/netbox-community/netbox/issues/11541))
|
||||
|
||||
#### [Version 3.5](./version-3.5.md) (April 2023)
|
||||
|
||||
* Customizable Dashboard ([#9416](https://github.com/netbox-community/netbox/issues/9416))
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
# NetBox v3.5
|
||||
|
||||
## v3.5.9 (FUTURE)
|
||||
## v3.5.9 (2023-08-28)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#12489](https://github.com/netbox-community/netbox/issues/12489) - Dynamically render location and device lists under site and location views
|
||||
* [#12825](https://github.com/netbox-community/netbox/issues/12825) - Display assigned values count per obejct type under custom field view
|
||||
* [#13313](https://github.com/netbox-community/netbox/issues/13313) - Enable filtering IP ranges by containing prefix
|
||||
* [#13415](https://github.com/netbox-community/netbox/issues/13415) - Include request object in custom link renderer on tables
|
||||
* [#13536](https://github.com/netbox-community/netbox/issues/13536) - Move child VLANs list to a separate tab under VLAN group view
|
||||
* [#13542](https://github.com/netbox-community/netbox/issues/13542) - Pass additional HTTP headers through to custom script context
|
||||
* [#13585](https://github.com/netbox-community/netbox/issues/13585) - Introduce `empty` lookup for numeric value filters
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#11272](https://github.com/netbox-community/netbox/issues/11272) - Fix localization support for device position field
|
||||
* [#13358](https://github.com/netbox-community/netbox/issues/13358) - Git backend should send HTTP auth headers only if credentials have been defined
|
||||
* [#13477](https://github.com/netbox-community/netbox/issues/13477) - Fix filtering of modified objects after bulk import/update
|
||||
* [#13478](https://github.com/netbox-community/netbox/issues/13478) - Fix filtering of export templates by content type under web UI
|
||||
* [#13500](https://github.com/netbox-community/netbox/issues/13500) - Fix form validation for bulk update of L2VPN terminations via bulk import form
|
||||
* [#13503](https://github.com/netbox-community/netbox/issues/13503) - Fix utilization graph proportions when localization is enabled
|
||||
* [#13507](https://github.com/netbox-community/netbox/issues/13507) - Avoid raising exception for invalid content type during global search
|
||||
* [#13516](https://github.com/netbox-community/netbox/issues/13516) - Plugin utility functions should be importable from `extras.plugins`
|
||||
* [#13530](https://github.com/netbox-community/netbox/issues/13530) - Ensure script log messages can be serialized as JSON data
|
||||
* [#13543](https://github.com/netbox-community/netbox/issues/13543) - Config context tab under device/VM view should not require `extras.view_configcontext` permission
|
||||
* [#13544](https://github.com/netbox-community/netbox/issues/13544) - Ensure `reindex` command clears all cached values when not in lazy mode
|
||||
* [#13556](https://github.com/netbox-community/netbox/issues/13556) - Correct REST API representation of VDC status choice
|
||||
* [#13569](https://github.com/netbox-community/netbox/issues/13569) - Fix selection widgets for related interfaces when bulk editing interfaces under device view
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
# NetBox v3.6
|
||||
|
||||
## v3.6-beta2 (2023-08-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#13351](https://github.com/netbox-community/netbox/issues/13351) - Fix missing text due to incorrectly applied translation tags
|
||||
* [#13361](https://github.com/netbox-community/netbox/issues/13361) - Extra choices field on custom field choice set form should not be required
|
||||
* [#13363](https://github.com/netbox-community/netbox/issues/13363) - Fix API endpoint for custom field choice selector in forms
|
||||
* [#13376](https://github.com/netbox-community/netbox/issues/13376) - Restrict add/remove tag fields by model on bulk edit forms
|
||||
* [#13410](https://github.com/netbox-community/netbox/issues/13410) - Fix rendering of custom choice fields with large number of choices
|
||||
* [#13433](https://github.com/netbox-community/netbox/issues/13433) - User field on API token form should be required
|
||||
* [#13434](https://github.com/netbox-community/netbox/issues/13434) - Randomly generate initial keys prior to the creation of new tokens
|
||||
* [#13437](https://github.com/netbox-community/netbox/issues/13437) - Display bookmark button only for relevant objects
|
||||
|
||||
---
|
||||
|
||||
## v3.6-beta1 (2023-08-02)
|
||||
## v3.6.0 (2023-08-30)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -23,12 +8,13 @@
|
||||
* The `device_role` field on the Device model has been renamed to `role`. The `device_role` field has been temporarily retained on the REST API serializer for devices for backward compatibility, but is read-only.
|
||||
* The `choices` array field has been removed from the CustomField model. Any defined choices are automatically migrated to CustomFieldChoiceSets, accessible via the new `choice_set` field on the CustomField model.
|
||||
* The `napalm_driver` and `napalm_args` fields (which were deprecated in v3.5) have been removed from the Platform model.
|
||||
* The `device` and `device_id` filter for interfaces will no longer include interfaces from virtual chassis peers. Two new filters, `virtual_chassis_member` and `virtual_chassis_member_id`, have been introduced to match all interfaces belonging to the specified device's virtual chassis (if any).
|
||||
* Reports and scripts are now returned within a `results` list when fetched via the REST API, consistent with other models.
|
||||
* Superusers can no longer retrieve API token keys via the web UI if [`ALLOW_TOKEN_RETRIEVAL`](https://docs.netbox.dev/en/stable/configuration/security/#allow_token_retrieval) is disabled. (The admin view has been removed per [#13044](https://github.com/netbox-community/netbox/issues/13044).)
|
||||
|
||||
### New Features
|
||||
|
||||
#### Relocated Admin Views ([#12589](https://github.com/netbox-community/netbox/issues/12589), [#12590](https://github.com/netbox-community/netbox/issues/12590), [#12591](https://github.com/netbox-community/netbox/issues/12591), [#13044](https://github.com/netbox-community/netbox/issues/13044))
|
||||
#### Relocated Admin UI Views ([#12589](https://github.com/netbox-community/netbox/issues/12589), [#12590](https://github.com/netbox-community/netbox/issues/12590), [#12591](https://github.com/netbox-community/netbox/issues/12591), [#13044](https://github.com/netbox-community/netbox/issues/13044))
|
||||
|
||||
Management views for the following object types, previously available only under the backend admin interface, have been relocated to the primary user interface:
|
||||
|
||||
@@ -72,6 +58,7 @@ Tags may now be restricted to use with designated object types. Tags that have n
|
||||
* [#8137](https://github.com/netbox-community/netbox/issues/8137) - Add a field for designating the out-of-band (OOB) IP address for devices
|
||||
* [#10197](https://github.com/netbox-community/netbox/issues/10197) - Cache the number of member devices on each virtual chassis
|
||||
* [#11305](https://github.com/netbox-community/netbox/issues/11305) - Add GPS coordinate fields to the device model
|
||||
* [#11478](https://github.com/netbox-community/netbox/issues/11478) - Introduce `virtual_chassis_member` filter for interfaces & restore default behavior for `device` filter
|
||||
* [#11519](https://github.com/netbox-community/netbox/issues/11519) - Add a SQL index for IP address host values to optimize queries
|
||||
* [#11732](https://github.com/netbox-community/netbox/issues/11732) - Prevent inadvertent overwriting of object attributes by competing users
|
||||
* [#11936](https://github.com/netbox-community/netbox/issues/11936) - Introduce support for tags and custom fields on webhooks
|
||||
@@ -84,6 +71,12 @@ Tags may now be restricted to use with designated object types. Tags that have n
|
||||
* [#13170](https://github.com/netbox-community/netbox/issues/13170) - Add `rf_role` to InterfaceTemplate
|
||||
* [#13269](https://github.com/netbox-community/netbox/issues/13269) - Cache the number of assigned component templates for device types
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#13513](https://github.com/netbox-community/netbox/issues/13513) - Prevent exception when rendering bookmarks widget for anonymous user
|
||||
* [#13599](https://github.com/netbox-community/netbox/issues/13599) - Fix errant counter increments when editing device/VM components
|
||||
* [#13605](https://github.com/netbox-community/netbox/issues/13605) - Optimize cached counter migrations to avoid excessive memory consumption
|
||||
|
||||
### Other Changes
|
||||
|
||||
* Work has begun on introducing translation and localization support in NetBox. This work is being performed in preparation for release 4.0.
|
||||
|
||||
@@ -125,12 +125,13 @@ class GitBackend(DataBackend):
|
||||
}
|
||||
|
||||
if self.url_scheme in ('http', 'https'):
|
||||
clone_args.update(
|
||||
{
|
||||
"username": self.params.get('username'),
|
||||
"password": self.params.get('password'),
|
||||
}
|
||||
)
|
||||
if self.params.get('username'):
|
||||
clone_args.update(
|
||||
{
|
||||
"username": self.params.get('username'),
|
||||
"password": self.params.get('password'),
|
||||
}
|
||||
)
|
||||
|
||||
logger.debug(f"Cloning git repo: {self.url}")
|
||||
try:
|
||||
|
||||
@@ -25,4 +25,7 @@ urlpatterns = (
|
||||
path('jobs/<int:pk>/', views.JobView.as_view(), name='job'),
|
||||
path('jobs/<int:pk>/delete/', views.JobDeleteView.as_view(), name='job_delete'),
|
||||
|
||||
# Configuration
|
||||
path('config/', views.ConfigView.as_view(), name='config'),
|
||||
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
|
||||
from extras.models import ConfigRevision
|
||||
from netbox.views import generic
|
||||
from netbox.views.generic.base import BaseObjectView
|
||||
from utilities.utils import count_related
|
||||
@@ -141,3 +142,14 @@ class JobBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Job.objects.all()
|
||||
filterset = filtersets.JobFilterSet
|
||||
table = tables.JobTable
|
||||
|
||||
|
||||
#
|
||||
# Config Revisions
|
||||
#
|
||||
|
||||
class ConfigView(generic.ObjectView):
|
||||
queryset = ConfigRevision.objects.all()
|
||||
|
||||
def get_object(self, **kwargs):
|
||||
return self.queryset.first()
|
||||
|
||||
@@ -738,12 +738,12 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
class Meta(DeviceSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial',
|
||||
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow',
|
||||
'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position',
|
||||
'vc_priority', 'description', 'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context',
|
||||
'config_template', 'created', 'last_updated', 'console_port_count', 'console_server_port_count',
|
||||
'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count',
|
||||
'device_bay_count', 'module_bay_count', 'inventory_item_count',
|
||||
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
|
||||
'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
|
||||
'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context',
|
||||
'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count',
|
||||
'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count',
|
||||
'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count',
|
||||
]
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
@@ -758,6 +758,7 @@ class VirtualDeviceContextSerializer(NetBoxModelSerializer):
|
||||
primary_ip = NestedIPAddressSerializer(read_only=True, allow_null=True)
|
||||
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=VirtualDeviceContextStatusChoices)
|
||||
|
||||
# Related object counts
|
||||
interface_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
@@ -1462,17 +1462,15 @@ class InterfaceFilterSet(
|
||||
PathEndpointFilterSet,
|
||||
CommonInterfaceFilterSet
|
||||
):
|
||||
# Override device and device_id filters from DeviceComponentFilterSet to match against any peer virtual chassis
|
||||
# members
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
virtual_chassis_member = MultiValueCharFilter(
|
||||
method='filter_virtual_chassis_member',
|
||||
field_name='name',
|
||||
label=_('Device'),
|
||||
label=_('Virtual Chassis Interfaces for Device')
|
||||
)
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_device_id',
|
||||
virtual_chassis_member_id = MultiValueNumberFilter(
|
||||
method='filter_virtual_chassis_member',
|
||||
field_name='pk',
|
||||
label=_('Device (ID)'),
|
||||
label=_('Virtual Chassis Interfaces for Device (ID)')
|
||||
)
|
||||
kind = django_filters.CharFilter(
|
||||
method='filter_kind',
|
||||
@@ -1540,23 +1538,11 @@ class InterfaceFilterSet(
|
||||
'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'cable_end',
|
||||
]
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
def filter_virtual_chassis_member(self, queryset, name, value):
|
||||
try:
|
||||
devices = Device.objects.filter(**{'{}__in'.format(name): value})
|
||||
vc_interface_ids = []
|
||||
for device in devices:
|
||||
vc_interface_ids.extend(device.vc_interfaces().values_list('id', flat=True))
|
||||
return queryset.filter(pk__in=vc_interface_ids)
|
||||
except Device.DoesNotExist:
|
||||
return queryset.none()
|
||||
|
||||
def filter_device_id(self, queryset, name, id_list):
|
||||
# Include interfaces belonging to peer virtual chassis members
|
||||
vc_interface_ids = []
|
||||
try:
|
||||
devices = Device.objects.filter(pk__in=id_list)
|
||||
for device in devices:
|
||||
vc_interface_ids += device.vc_interfaces(if_master=False).values_list('id', flat=True)
|
||||
for device in Device.objects.filter(**{f'{name}__in': value}):
|
||||
vc_interface_ids.extend(device.vc_interfaces(if_master=False).values_list('id', flat=True))
|
||||
return queryset.filter(pk__in=vc_interface_ids)
|
||||
except Device.DoesNotExist:
|
||||
return queryset.none()
|
||||
|
||||
@@ -421,12 +421,13 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
||||
label=_('Position'),
|
||||
required=False,
|
||||
help_text=_("The lowest-numbered unit occupied by the device"),
|
||||
localize=True,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/racks/{{rack}}/elevation/',
|
||||
attrs={
|
||||
'disabled-indicator': 'device',
|
||||
'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
device_type = DynamicModelChoiceField(
|
||||
@@ -1110,7 +1111,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
required=False,
|
||||
label=_('Parent interface'),
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
'virtual_chassis_member_id': '$device',
|
||||
}
|
||||
)
|
||||
bridge = DynamicModelChoiceField(
|
||||
@@ -1118,7 +1119,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
required=False,
|
||||
label=_('Bridged interface'),
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
'virtual_chassis_member_id': '$device',
|
||||
}
|
||||
)
|
||||
lag = DynamicModelChoiceField(
|
||||
@@ -1126,7 +1127,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
required=False,
|
||||
label=_('LAG interface'),
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
'virtual_chassis_member_id': '$device',
|
||||
'type': 'lag',
|
||||
}
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import utilities.fields
|
||||
|
||||
def recalculate_device_counts(apps, schema_editor):
|
||||
Device = apps.get_model("dcim", "Device")
|
||||
devices = list(Device.objects.all().annotate(
|
||||
devices = Device.objects.annotate(
|
||||
_console_port_count=Count('consoleports', distinct=True),
|
||||
_console_server_port_count=Count('consoleserverports', distinct=True),
|
||||
_power_port_count=Count('powerports', distinct=True),
|
||||
@@ -17,7 +17,7 @@ def recalculate_device_counts(apps, schema_editor):
|
||||
_device_bay_count=Count('devicebays', distinct=True),
|
||||
_module_bay_count=Count('modulebays', distinct=True),
|
||||
_inventory_item_count=Count('inventoryitems', distinct=True),
|
||||
))
|
||||
)
|
||||
|
||||
for device in devices:
|
||||
device.console_port_count = device._console_port_count
|
||||
@@ -42,7 +42,7 @@ def recalculate_device_counts(apps, schema_editor):
|
||||
'device_bay_count',
|
||||
'module_bay_count',
|
||||
'inventory_item_count',
|
||||
])
|
||||
], batch_size=100)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -7,12 +7,12 @@ import utilities.fields
|
||||
def populate_virtualchassis_members(apps, schema_editor):
|
||||
VirtualChassis = apps.get_model('dcim', 'VirtualChassis')
|
||||
|
||||
vcs = list(VirtualChassis.objects.annotate(_member_count=Count('members', distinct=True)))
|
||||
vcs = VirtualChassis.objects.annotate(_member_count=Count('members', distinct=True))
|
||||
|
||||
for vc in vcs:
|
||||
vc.member_count = vc._member_count
|
||||
|
||||
VirtualChassis.objects.bulk_update(vcs, ['member_count'])
|
||||
VirtualChassis.objects.bulk_update(vcs, ['member_count'], batch_size=100)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -2822,11 +2822,56 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
# VirtualChassis assignment for filtering
|
||||
virtual_chassis = VirtualChassis(name='Virtual Chassis')
|
||||
virtual_chassis.save()
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
|
||||
Device(
|
||||
name='Device 1A',
|
||||
device_type=device_types[0],
|
||||
role=roles[0],
|
||||
site=sites[0],
|
||||
location=locations[0],
|
||||
rack=racks[0],
|
||||
virtual_chassis=virtual_chassis,
|
||||
vc_position=1,
|
||||
vc_priority=1
|
||||
),
|
||||
Device(
|
||||
name='Device 1B',
|
||||
device_type=device_types[2],
|
||||
role=roles[2],
|
||||
site=sites[2],
|
||||
location=locations[2],
|
||||
rack=racks[2],
|
||||
virtual_chassis=virtual_chassis,
|
||||
vc_position=2,
|
||||
vc_priority=1
|
||||
),
|
||||
Device(
|
||||
name='Device 2',
|
||||
device_type=device_types[1],
|
||||
role=roles[1],
|
||||
site=sites[1],
|
||||
location=locations[1],
|
||||
rack=racks[1]
|
||||
),
|
||||
Device(
|
||||
name='Device 3',
|
||||
device_type=device_types[2],
|
||||
role=roles[2],
|
||||
site=sites[2],
|
||||
location=locations[2],
|
||||
rack=racks[2]
|
||||
),
|
||||
# For cable connections
|
||||
Device(
|
||||
name=None,
|
||||
device_type=device_types[2],
|
||||
role=roles[2],
|
||||
site=sites[3]
|
||||
),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -2834,6 +2879,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
ModuleBay(device=devices[0], name='Module Bay 1'),
|
||||
ModuleBay(device=devices[1], name='Module Bay 2'),
|
||||
ModuleBay(device=devices[2], name='Module Bay 3'),
|
||||
ModuleBay(device=devices[3], name='Module Bay 4'),
|
||||
)
|
||||
ModuleBay.objects.bulk_create(module_bays)
|
||||
|
||||
@@ -2841,6 +2887,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
|
||||
Module(device=devices[1], module_bay=module_bays[1], module_type=module_type),
|
||||
Module(device=devices[2], module_bay=module_bays[2], module_type=module_type),
|
||||
Module(device=devices[3], module_bay=module_bays[3], module_type=module_type),
|
||||
)
|
||||
Module.objects.bulk_create(modules)
|
||||
|
||||
@@ -2853,16 +2900,11 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
|
||||
# Virtual Device Context Creation
|
||||
vdcs = (
|
||||
VirtualDeviceContext(device=devices[3], name='VDC 1', identifier=1, status=VirtualDeviceContextStatusChoices.STATUS_ACTIVE),
|
||||
VirtualDeviceContext(device=devices[3], name='VDC 2', identifier=2, status=VirtualDeviceContextStatusChoices.STATUS_PLANNED),
|
||||
VirtualDeviceContext(device=devices[4], name='VDC 1', identifier=1, status=VirtualDeviceContextStatusChoices.STATUS_ACTIVE),
|
||||
VirtualDeviceContext(device=devices[4], name='VDC 2', identifier=2, status=VirtualDeviceContextStatusChoices.STATUS_PLANNED),
|
||||
)
|
||||
VirtualDeviceContext.objects.bulk_create(vdcs)
|
||||
|
||||
# VirtualChassis assignment for filtering
|
||||
virtual_chassis = VirtualChassis.objects.create(master=devices[0])
|
||||
Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=virtual_chassis, vc_position=1, vc_priority=1)
|
||||
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2)
|
||||
|
||||
interfaces = (
|
||||
Interface(
|
||||
device=devices[0],
|
||||
@@ -2885,6 +2927,13 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
Interface(
|
||||
device=devices[1],
|
||||
module=modules[1],
|
||||
name='VC Chassis Interface',
|
||||
type=InterfaceTypeChoices.TYPE_1GE_SFP,
|
||||
enabled=True
|
||||
),
|
||||
Interface(
|
||||
device=devices[2],
|
||||
module=modules[2],
|
||||
name='Interface 2',
|
||||
label='B',
|
||||
type=InterfaceTypeChoices.TYPE_1GE_GBIC,
|
||||
@@ -2901,8 +2950,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
poe_type=InterfacePoETypeChoices.TYPE_1_8023AF
|
||||
),
|
||||
Interface(
|
||||
device=devices[2],
|
||||
module=modules[2],
|
||||
device=devices[3],
|
||||
module=modules[3],
|
||||
name='Interface 3',
|
||||
label='C',
|
||||
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
||||
@@ -2919,7 +2968,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
poe_type=InterfacePoETypeChoices.TYPE_2_8023AT
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
device=devices[4],
|
||||
name='Interface 4',
|
||||
label='D',
|
||||
type=InterfaceTypeChoices.TYPE_OTHER,
|
||||
@@ -2932,7 +2981,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
poe_type=InterfacePoETypeChoices.TYPE_2_8023AT
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
device=devices[4],
|
||||
name='Interface 5',
|
||||
label='E',
|
||||
type=InterfaceTypeChoices.TYPE_OTHER,
|
||||
@@ -2941,7 +2990,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
tx_power=40
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
device=devices[4],
|
||||
name='Interface 6',
|
||||
label='F',
|
||||
type=InterfaceTypeChoices.TYPE_OTHER,
|
||||
@@ -2950,7 +2999,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
tx_power=40
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
device=devices[4],
|
||||
name='Interface 7',
|
||||
type=InterfaceTypeChoices.TYPE_80211AC,
|
||||
rf_role=WirelessRoleChoices.ROLE_AP,
|
||||
@@ -2959,7 +3008,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
rf_channel_width=22
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
device=devices[4],
|
||||
name='Interface 8',
|
||||
type=InterfaceTypeChoices.TYPE_80211AC,
|
||||
rf_role=WirelessRoleChoices.ROLE_STATION,
|
||||
@@ -2977,8 +3026,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
interfaces[7].vdcs.set([vdcs[1]])
|
||||
|
||||
# Cables
|
||||
Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[3]]).save()
|
||||
Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[4]]).save()
|
||||
Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[5]]).save()
|
||||
Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[6]]).save()
|
||||
# Third pair is not connected
|
||||
|
||||
def test_name(self):
|
||||
@@ -2991,7 +3040,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
|
||||
def test_enabled(self):
|
||||
params = {'enabled': 'true'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
|
||||
params = {'enabled': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
@@ -3011,7 +3060,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
params = {'mgmt_only': 'true'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'mgmt_only': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
|
||||
def test_poe_mode(self):
|
||||
params = {'poe_mode': [InterfacePoEModeChoices.MODE_PD, InterfacePoEModeChoices.MODE_PSE]}
|
||||
@@ -3116,6 +3165,14 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
params = {'device': [devices[0].name, devices[1].name]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_virtual_chassis_member(self):
|
||||
# Device 1A & 3 have 1 management interface, Device 1B has 1 interfaces
|
||||
devices = Device.objects.filter(name__in=['Device 1A', 'Device 3'])
|
||||
params = {'virtual_chassis_member_id': [devices[0].pk, devices[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'virtual_chassis_member': [devices[0].name, devices[1].name]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_module(self):
|
||||
modules = Module.objects.all()[:2]
|
||||
params = {'module_id': [modules[0].pk, modules[1].pk]}
|
||||
@@ -3125,23 +3182,23 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
params = {'cabled': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'cabled': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
|
||||
def test_occupied(self):
|
||||
params = {'occupied': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'occupied': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
|
||||
def test_connected(self):
|
||||
params = {'connected': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'connected': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
|
||||
def test_kind(self):
|
||||
params = {'kind': 'physical'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
|
||||
params = {'kind': 'virtual'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
|
||||
|
||||
|
||||
@@ -398,32 +398,8 @@ class SiteView(generic.ObjectView):
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), 'site_id'),
|
||||
)
|
||||
|
||||
locations = Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Rack,
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
)
|
||||
locations = Location.objects.add_related_count(
|
||||
locations,
|
||||
Device,
|
||||
'location',
|
||||
'device_count',
|
||||
cumulative=True
|
||||
).restrict(request.user, 'view').filter(site=instance)
|
||||
|
||||
nonracked_devices = Device.objects.filter(
|
||||
site=instance,
|
||||
rack__isnull=True,
|
||||
parent_bay__isnull=True
|
||||
).prefetch_related('device_type__manufacturer', 'parent_bay', 'role')
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'locations': locations,
|
||||
'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
|
||||
'total_nonracked_devices_count': nonracked_devices.count(),
|
||||
}
|
||||
|
||||
|
||||
@@ -495,16 +471,8 @@ class LocationView(generic.ObjectView):
|
||||
(Device.objects.restrict(request.user, 'view').filter(location__in=locations), 'location_id'),
|
||||
)
|
||||
|
||||
nonracked_devices = Device.objects.filter(
|
||||
location=instance,
|
||||
rack__isnull=True,
|
||||
parent_bay__isnull=True
|
||||
).prefetch_related('device_type__manufacturer', 'parent_bay', 'role')
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
|
||||
'total_nonracked_devices_count': nonracked_devices.count(),
|
||||
}
|
||||
|
||||
|
||||
@@ -2055,7 +2023,6 @@ class DeviceConfigContextView(ObjectConfigContextView):
|
||||
base_template = 'dcim/device/base.html'
|
||||
tab = ViewTab(
|
||||
label=_('Config Context'),
|
||||
permission='extras.view_configcontext',
|
||||
weight=2000
|
||||
)
|
||||
|
||||
|
||||
@@ -346,13 +346,16 @@ class BookmarksWidget(DashboardWidget):
|
||||
def render(self, request):
|
||||
from extras.models import Bookmark
|
||||
|
||||
bookmarks = Bookmark.objects.filter(user=request.user).order_by(self.config['order_by'])
|
||||
if object_types := self.config.get('object_types'):
|
||||
models = get_models_from_content_types(object_types)
|
||||
conent_types = ContentType.objects.get_for_models(*models).values()
|
||||
bookmarks = bookmarks.filter(object_type__in=conent_types)
|
||||
if max_items := self.config.get('max_items'):
|
||||
bookmarks = bookmarks[:max_items]
|
||||
if request.user.is_anonymous:
|
||||
bookmarks = list()
|
||||
else:
|
||||
bookmarks = Bookmark.objects.filter(user=request.user).order_by(self.config['order_by'])
|
||||
if object_types := self.config.get('object_types'):
|
||||
models = get_models_from_content_types(object_types)
|
||||
conent_types = ContentType.objects.get_for_models(*models).values()
|
||||
bookmarks = bookmarks.filter(object_type__in=conent_types)
|
||||
if max_items := self.config.get('max_items'):
|
||||
bookmarks = bookmarks[:max_items]
|
||||
|
||||
return render_to_string(self.template_name, {
|
||||
'bookmarks': bookmarks,
|
||||
|
||||
@@ -136,7 +136,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id')),
|
||||
(_('Data'), ('data_source_id', 'data_file_id')),
|
||||
(_('Attributes'), ('content_types', 'mime_type', 'file_extension', 'as_attachment')),
|
||||
(_('Attributes'), ('content_type_id', 'mime_type', 'file_extension', 'as_attachment')),
|
||||
)
|
||||
data_source_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DataSource.objects.all(),
|
||||
@@ -151,10 +151,10 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
||||
'source_id': '$data_source_id'
|
||||
}
|
||||
)
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Content types'),
|
||||
content_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
|
||||
required=False
|
||||
required=False,
|
||||
label=_('Content types')
|
||||
)
|
||||
mime_type = forms.CharField(
|
||||
required=False,
|
||||
|
||||
@@ -490,7 +490,9 @@ class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMe
|
||||
(_('Pagination'), ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')),
|
||||
(_('Validation'), ('CUSTOM_VALIDATORS',)),
|
||||
(_('User Preferences'), ('DEFAULT_USER_PREFERENCES',)),
|
||||
(_('Miscellaneous'), ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL')),
|
||||
(_('Miscellaneous'), (
|
||||
'MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL',
|
||||
)),
|
||||
(_('Config Revision'), ('comment',))
|
||||
)
|
||||
|
||||
@@ -524,6 +526,8 @@ class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMe
|
||||
elif value == param.default:
|
||||
help_text += _(' (default)')
|
||||
self.fields[param.name].help_text = help_text
|
||||
if type(value) in (tuple, list):
|
||||
value = ', '.join(value)
|
||||
self.fields[param.name].initial = value
|
||||
if is_static:
|
||||
self.fields[param.name].disabled = True
|
||||
|
||||
@@ -69,10 +69,7 @@ class Command(BaseCommand):
|
||||
if not kwargs['lazy']:
|
||||
self.stdout.write('Clearing cached values... ', ending='')
|
||||
self.stdout.flush()
|
||||
content_types = [
|
||||
ContentType.objects.get_for_model(model) for model in indexers.keys()
|
||||
]
|
||||
deleted_count = search_backend.clear(content_types)
|
||||
deleted_count = search_backend.clear()
|
||||
self.stdout.write(f'{deleted_count} entries deleted.')
|
||||
|
||||
# Index models
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import json
|
||||
import urllib.parse
|
||||
|
||||
from django.contrib import admin
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -12,7 +11,7 @@ from django.http import HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
|
||||
from extras.choices import *
|
||||
@@ -724,7 +723,9 @@ class ConfigRevision(models.Model):
|
||||
verbose_name_plural = _('config revisions')
|
||||
|
||||
def __str__(self):
|
||||
return f'Config revision #{self.pk} ({self.created})'
|
||||
if self.is_active:
|
||||
return gettext('Current configuration')
|
||||
return gettext('Config revision #{id}').format(id=self.pk)
|
||||
|
||||
def __getattr__(self, item):
|
||||
if item in self.data:
|
||||
@@ -742,6 +743,6 @@ class ConfigRevision(models.Model):
|
||||
cache.set('config_version', self.pk, None)
|
||||
activate.alters_data = True
|
||||
|
||||
@admin.display(boolean=True)
|
||||
@property
|
||||
def is_active(self):
|
||||
return cache.get('config_version') == self.pk
|
||||
|
||||
@@ -11,6 +11,7 @@ from netbox.search import register_search
|
||||
from .navigation import *
|
||||
from .registration import *
|
||||
from .templates import *
|
||||
from .utils import *
|
||||
|
||||
# Initialize plugin registry
|
||||
registry['plugins'].update({
|
||||
|
||||
@@ -401,23 +401,23 @@ class BaseScript:
|
||||
|
||||
def log_debug(self, message):
|
||||
self.logger.log(logging.DEBUG, message)
|
||||
self.log.append((LogLevelChoices.LOG_DEFAULT, message))
|
||||
self.log.append((LogLevelChoices.LOG_DEFAULT, str(message)))
|
||||
|
||||
def log_success(self, message):
|
||||
self.logger.log(logging.INFO, message) # No syslog equivalent for SUCCESS
|
||||
self.log.append((LogLevelChoices.LOG_SUCCESS, message))
|
||||
self.log.append((LogLevelChoices.LOG_SUCCESS, str(message)))
|
||||
|
||||
def log_info(self, message):
|
||||
self.logger.log(logging.INFO, message)
|
||||
self.log.append((LogLevelChoices.LOG_INFO, message))
|
||||
self.log.append((LogLevelChoices.LOG_INFO, str(message)))
|
||||
|
||||
def log_warning(self, message):
|
||||
self.logger.log(logging.WARNING, message)
|
||||
self.log.append((LogLevelChoices.LOG_WARNING, message))
|
||||
self.log.append((LogLevelChoices.LOG_WARNING, str(message)))
|
||||
|
||||
def log_failure(self, message):
|
||||
self.logger.log(logging.ERROR, message)
|
||||
self.log.append((LogLevelChoices.LOG_FAILURE, message))
|
||||
self.log.append((LogLevelChoices.LOG_FAILURE, str(message)))
|
||||
|
||||
# Convenience functions
|
||||
|
||||
|
||||
@@ -1109,11 +1109,13 @@ class ChangeLoggedFilterSetTestCase(TestCase):
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2'),
|
||||
Site(name='Site 3', slug='site-3'),
|
||||
Site(name='Site 4', slug='site-4'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
# Simulate *creation* changelog records for two of the sites
|
||||
request_id = uuid.uuid4()
|
||||
cls.create_request_id = request_id
|
||||
objectchanges = (
|
||||
ObjectChange(
|
||||
changed_object_type=content_type,
|
||||
@@ -1132,6 +1134,7 @@ class ChangeLoggedFilterSetTestCase(TestCase):
|
||||
|
||||
# Simulate *update* changelog records for two of the sites
|
||||
request_id = uuid.uuid4()
|
||||
cls.update_request_id = request_id
|
||||
objectchanges = (
|
||||
ObjectChange(
|
||||
changed_object_type=content_type,
|
||||
@@ -1148,14 +1151,36 @@ class ChangeLoggedFilterSetTestCase(TestCase):
|
||||
)
|
||||
ObjectChange.objects.bulk_create(objectchanges)
|
||||
|
||||
# Simulate *create* and *update* changelog records for two of the sites
|
||||
request_id = uuid.uuid4()
|
||||
cls.create_update_request_id = request_id
|
||||
objectchanges = (
|
||||
ObjectChange(
|
||||
changed_object_type=content_type,
|
||||
changed_object_id=sites[2].pk,
|
||||
action=ObjectChangeActionChoices.ACTION_CREATE,
|
||||
request_id=request_id
|
||||
),
|
||||
ObjectChange(
|
||||
changed_object_type=content_type,
|
||||
changed_object_id=sites[3].pk,
|
||||
action=ObjectChangeActionChoices.ACTION_UPDATE,
|
||||
request_id=request_id
|
||||
),
|
||||
)
|
||||
ObjectChange.objects.bulk_create(objectchanges)
|
||||
|
||||
def test_created_by_request(self):
|
||||
request_id = ObjectChange.objects.filter(action=ObjectChangeActionChoices.ACTION_CREATE).first().request_id
|
||||
params = {'created_by_request': request_id}
|
||||
params = {'created_by_request': self.create_request_id}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.queryset.count(), 3)
|
||||
self.assertEqual(self.queryset.count(), 4)
|
||||
|
||||
def test_updated_by_request(self):
|
||||
request_id = ObjectChange.objects.filter(action=ObjectChangeActionChoices.ACTION_UPDATE).first().request_id
|
||||
params = {'updated_by_request': request_id}
|
||||
params = {'updated_by_request': self.update_request_id}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.queryset.count(), 3)
|
||||
self.assertEqual(self.queryset.count(), 4)
|
||||
|
||||
def test_modified_by_request(self):
|
||||
params = {'modified_by_request': self.create_update_request_id}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.queryset.count(), 4)
|
||||
|
||||
@@ -46,6 +46,21 @@ class CustomFieldListView(generic.ObjectListView):
|
||||
class CustomFieldView(generic.ObjectView):
|
||||
queryset = CustomField.objects.select_related('choice_set')
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = ()
|
||||
|
||||
for content_type in instance.content_types.all():
|
||||
related_models += (
|
||||
content_type.model_class().objects.restrict(request.user, 'view').exclude(
|
||||
Q(**{f'custom_field_data__{instance.name}': ''}) |
|
||||
Q(**{f'custom_field_data__{instance.name}': None})
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(CustomField, 'edit')
|
||||
class CustomFieldEditView(generic.ObjectEditView):
|
||||
|
||||
@@ -467,6 +467,10 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
choices=IPRangeStatusChoices,
|
||||
null_value=None
|
||||
)
|
||||
parent = MultiValueCharFilter(
|
||||
method='search_by_parent',
|
||||
label=_('Parent prefix'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IPRange
|
||||
@@ -501,6 +505,18 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
except ValidationError:
|
||||
return queryset.none()
|
||||
|
||||
def search_by_parent(self, queryset, name, value):
|
||||
if not value:
|
||||
return queryset
|
||||
q = Q()
|
||||
for prefix in value:
|
||||
try:
|
||||
query = str(netaddr.IPNetwork(prefix.strip()).cidr)
|
||||
q |= Q(start_address__net_host_contained=query, end_address__net_host_contained=query)
|
||||
except (AddrFormatError, ValueError):
|
||||
return queryset.none()
|
||||
return queryset.filter(q)
|
||||
|
||||
|
||||
class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
family = django_filters.NumberFilter(
|
||||
|
||||
@@ -592,9 +592,11 @@ class L2VPNTerminationImportForm(NetBoxModelImportForm):
|
||||
|
||||
if self.cleaned_data.get('device') and self.cleaned_data.get('virtual_machine'):
|
||||
raise ValidationError(_('Cannot import device and VM interface terminations simultaneously.'))
|
||||
if not (self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')):
|
||||
if not self.instance and not (self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')):
|
||||
raise ValidationError(_('Each termination must specify either an interface or a VLAN.'))
|
||||
if self.cleaned_data.get('interface') and self.cleaned_data.get('vlan'):
|
||||
raise ValidationError(_('Cannot assign both an interface and a VLAN.'))
|
||||
|
||||
self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')
|
||||
# if this is an update we might not have interface or vlan in the form data
|
||||
if self.cleaned_data.get('interface') or self.cleaned_data.get('vlan'):
|
||||
self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')
|
||||
|
||||
@@ -118,6 +118,12 @@ class VLANGroup(OrganizationalModel):
|
||||
return available_vids[0]
|
||||
return None
|
||||
|
||||
def get_child_vlans(self):
|
||||
"""
|
||||
Return all VLANs within this group.
|
||||
"""
|
||||
return VLAN.objects.filter(group=self).order_by('vid')
|
||||
|
||||
|
||||
class VLAN(PrimaryModel):
|
||||
"""
|
||||
|
||||
@@ -10,7 +10,6 @@ from ipam.models import *
|
||||
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
@@ -807,6 +806,12 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_parent(self):
|
||||
params = {'parent': ['10.0.1.0/24', '10.0.2.0/24']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'parent': ['10.0.1.0/25']} # Range 10.0.1.100-199 is not fully contained by 10.0.1.0/25
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
|
||||
|
||||
|
||||
class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = IPAddress.objects.all()
|
||||
|
||||
@@ -897,21 +897,8 @@ class VLANGroupView(generic.ObjectView):
|
||||
(VLAN.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'),
|
||||
)
|
||||
|
||||
# TODO: Replace with embedded table
|
||||
vlans = VLAN.objects.restrict(request.user, 'view').filter(group=instance).prefetch_related(
|
||||
Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user)),
|
||||
'tenant', 'site', 'role',
|
||||
).order_by('vid')
|
||||
vlans = add_available_vlans(vlans, vlan_group=instance)
|
||||
|
||||
vlans_table = tables.VLANTable(vlans, user=request.user, exclude=('group',))
|
||||
if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'):
|
||||
vlans_table.columns.show('pk')
|
||||
vlans_table.configure(request)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'vlans_table': vlans_table,
|
||||
}
|
||||
|
||||
|
||||
@@ -944,6 +931,30 @@ class VLANGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
table = tables.VLANGroupTable
|
||||
|
||||
|
||||
@register_model_view(VLANGroup, 'vlans')
|
||||
class VLANGroupVLANsView(generic.ObjectChildrenView):
|
||||
queryset = VLANGroup.objects.all()
|
||||
child_model = VLAN
|
||||
table = tables.VLANTable
|
||||
filterset = filtersets.VLANFilterSet
|
||||
template_name = 'generic/object_children.html'
|
||||
tab = ViewTab(
|
||||
label=_('VLANs'),
|
||||
badge=lambda x: x.get_child_vlans().count(),
|
||||
permission='ipam.view_vlan',
|
||||
weight=500
|
||||
)
|
||||
|
||||
def get_children(self, request, parent):
|
||||
return parent.get_child_vlans().restrict(request.user, 'view').prefetch_related(
|
||||
Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user)),
|
||||
'tenant', 'site', 'role',
|
||||
)
|
||||
|
||||
def prep_table_data(self, request, queryset, parent):
|
||||
return add_available_vlans(parent.get_child_vlans(), parent)
|
||||
|
||||
|
||||
#
|
||||
# FHRP groups
|
||||
#
|
||||
|
||||
@@ -102,7 +102,6 @@ PARAMS = (
|
||||
description=_("Default voltage for powerfeeds"),
|
||||
field=forms.IntegerField
|
||||
),
|
||||
|
||||
ConfigParam(
|
||||
name='POWERFEED_DEFAULT_AMPERAGE',
|
||||
label=_('Powerfeed amperage'),
|
||||
@@ -110,7 +109,6 @@ PARAMS = (
|
||||
description=_("Default amperage for powerfeeds"),
|
||||
field=forms.IntegerField
|
||||
),
|
||||
|
||||
ConfigParam(
|
||||
name='POWERFEED_DEFAULT_MAX_UTILIZATION',
|
||||
label=_('Powerfeed max utilization'),
|
||||
|
||||
@@ -246,18 +246,22 @@ class ChangeLoggedModelFilterSet(BaseFilterSet):
|
||||
updated_by_request = django_filters.UUIDFilter(
|
||||
method='filter_by_request'
|
||||
)
|
||||
modified_by_request = django_filters.UUIDFilter(
|
||||
method='filter_by_request'
|
||||
)
|
||||
|
||||
def filter_by_request(self, queryset, name, value):
|
||||
content_type = ContentType.objects.get_for_model(self.Meta.model)
|
||||
action = {
|
||||
'created_by_request': ObjectChangeActionChoices.ACTION_CREATE,
|
||||
'updated_by_request': ObjectChangeActionChoices.ACTION_UPDATE,
|
||||
'created_by_request': Q(action=ObjectChangeActionChoices.ACTION_CREATE),
|
||||
'updated_by_request': Q(action=ObjectChangeActionChoices.ACTION_UPDATE),
|
||||
'modified_by_request': Q(action__in=[ObjectChangeActionChoices.ACTION_CREATE, ObjectChangeActionChoices.ACTION_UPDATE]),
|
||||
}.get(name)
|
||||
request_id = value
|
||||
pks = ObjectChange.objects.filter(
|
||||
action,
|
||||
changed_object_type=content_type,
|
||||
action=action,
|
||||
request_id=request_id
|
||||
request_id=request_id,
|
||||
).values_list('changed_object_id', flat=True)
|
||||
return queryset.filter(pk__in=pks)
|
||||
|
||||
|
||||
@@ -406,6 +406,11 @@ ADMIN_MENU = Menu(
|
||||
MenuGroup(
|
||||
label=_('Configuration'),
|
||||
items=(
|
||||
MenuItem(
|
||||
link='core:config',
|
||||
link_text=_('Current Config'),
|
||||
permissions=['extras.view_configrevision']
|
||||
),
|
||||
MenuItem(
|
||||
link='extras:configrevision_list',
|
||||
link_text=_('Config Revisions'),
|
||||
|
||||
@@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '3.6-beta2'
|
||||
VERSION = '3.6.0'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
|
||||
@@ -4,6 +4,7 @@ from urllib.parse import quote
|
||||
|
||||
import django_tables2 as tables
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.context_processors import auth
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.db.models import DateField, DateTimeField
|
||||
from django.template import Context, Template
|
||||
@@ -517,24 +518,32 @@ class CustomLinkColumn(tables.Column):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def render(self, record):
|
||||
try:
|
||||
rendered = self.customlink.render({
|
||||
'object': record,
|
||||
def _render_customlink(self, record, table):
|
||||
context = {
|
||||
'object': record,
|
||||
'debug': settings.DEBUG,
|
||||
}
|
||||
if request := getattr(table, 'context', {}).get('request'):
|
||||
# If the request is available, include it as context
|
||||
context.update({
|
||||
'request': request,
|
||||
**auth(request),
|
||||
})
|
||||
if rendered:
|
||||
|
||||
return self.customlink.render(context)
|
||||
|
||||
def render(self, record, table, **kwargs):
|
||||
try:
|
||||
if rendered := self._render_customlink(record, table):
|
||||
return mark_safe(f'<a href="{rendered["link"]}"{rendered["link_target"]}>{rendered["text"]}</a>')
|
||||
except Exception as e:
|
||||
error_text = _('Error')
|
||||
return mark_safe(f'<span class="text-danger" title="{e}"><i class="mdi mdi-alert"></i> {error_text}</span>')
|
||||
return ''
|
||||
|
||||
def value(self, record):
|
||||
def value(self, record, table, **kwargs):
|
||||
try:
|
||||
rendered = self.customlink.render({
|
||||
'object': record,
|
||||
})
|
||||
if rendered:
|
||||
if rendered := self._render_customlink(record, table):
|
||||
return rendered['link']
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -465,7 +465,7 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
messages.success(request, msg)
|
||||
|
||||
view_name = get_viewname(model, action='list')
|
||||
results_url = f"{reverse(view_name)}?created_by_request={request.id}"
|
||||
results_url = f"{reverse(view_name)}?modified_by_request={request.id}"
|
||||
return redirect(results_url)
|
||||
|
||||
except (AbortTransaction, ValidationError):
|
||||
|
||||
2
netbox/project-static/dist/graphiql.css
vendored
2
netbox/project-static/dist/graphiql.css
vendored
File diff suppressed because one or more lines are too long
271
netbox/project-static/dist/graphiql.js
vendored
271
netbox/project-static/dist/graphiql.js
vendored
File diff suppressed because one or more lines are too long
4
netbox/project-static/dist/graphiql.js.map
vendored
4
netbox/project-static/dist/graphiql.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"graphiql": "1.4.1",
|
||||
"graphiql": "1.8.9",
|
||||
"graphql": ">= v14.5.0 <= 15.5.0",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,15 @@
|
||||
{% load helpers %}
|
||||
|
||||
{% block bulk_edit_controls %}
|
||||
{{ block.super }}
|
||||
{% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
|
||||
{% if 'bulk_edit' in actions and bulk_edit_view %}
|
||||
<button type="submit" name="_edit"
|
||||
formaction="{% url bulk_edit_view %}?device={{ object.pk }}&return_url={{ return_url }}"
|
||||
class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
|
||||
{% if 'bulk_rename' in actions and bulk_rename_view %}
|
||||
<button type="submit" name="_rename"
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
{% trans "Non-Racked Devices" %}
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
{% if nonracked_devices %}
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Role" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th colspan="2">{% trans "Parent Device" %}</th>
|
||||
</tr>
|
||||
{% for device in nonracked_devices %}
|
||||
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
|
||||
</td>
|
||||
<td>{{ device.role }}</td>
|
||||
<td>{{ device.device_type }}</td>
|
||||
{% if device.parent_bay %}
|
||||
<td>{{ device.parent_bay.device|linkify }}</td>
|
||||
<td>{{ device.parent_bay }}</td>
|
||||
{% else %}
|
||||
<td colspan="2" class="text-muted">—</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% if total_nonracked_devices_count > nonracked_devices.count %}
|
||||
{% if object|meta:'verbose_name' == 'site' %}
|
||||
<div class="text-muted">
|
||||
{% blocktrans with count=nonracked_devices.count total=total_nonracked_devices_count %}
|
||||
Displaying {{ count }} of {{ total }} devices
|
||||
{% endblocktrans %}
|
||||
(<a href="{% url 'dcim:device_list' %}?site_id={{ object.pk }}&rack_id=null">{% trans "View full list" %}</a>)
|
||||
</div>
|
||||
{% elif object|meta:'verbose_name' == 'location' %}
|
||||
<div class="text-muted">
|
||||
{% blocktrans with count=nonracked_devices.count total=total_nonracked_devices_count %}
|
||||
Displaying {{ count }} of {{ total }} devices
|
||||
{% endblocktrans %}
|
||||
(<a href="{% url 'dcim:device_list' %}?location_id={{ object.pk }}&rack_id=null">{% trans "View full list" %}</a>)
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="text-muted">
|
||||
{% trans "None" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if perms.dcim.add_device %}
|
||||
{% if object|meta:'verbose_name' == 'rack' %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<a href="{% url 'dcim:device_add' %}?site={{ object.site.pk }}&rack={{ object.pk }}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
|
||||
{% trans "Add a Non-Racked Device" %}
|
||||
</a>
|
||||
</div>
|
||||
{% elif object|meta:'verbose_name' == 'site' %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<a href="{% url 'dcim:device_add' %}?site={{ object.pk }}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
|
||||
{% trans "Add a Non-Racked Device" %}
|
||||
</a>
|
||||
</div>
|
||||
{% elif object|meta:'verbose_name' == 'location' %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<a href="{% url 'dcim:device_add' %}?site={{ object.site.pk }}&location={{ object.pk }}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
|
||||
{% trans "Add a Non-Racked Device" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -66,7 +66,6 @@
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'dcim/inc/nonracked_devices.html' %}
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
@@ -79,6 +78,27 @@
|
||||
hx-get="{% url 'dcim:location_list' %}?parent_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
{% if perms.dcim.add_location %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<a href="{% url 'dcim:location_add' %}?site={{ object.site.pk }}&parent={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Location" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">Non-Racked Devices</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'dcim:device_list' %}?location_id={{ object.pk }}&rack_id=null&parent_bay_id=null"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
{% if perms.dcim.add_device %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<a href="{% url 'dcim:device_add' %}?site={{ object.site.pk }}&location={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Device" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
|
||||
@@ -132,56 +132,40 @@
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' with filter_name='site_id' %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Locations" %}</h5>
|
||||
<div class='card-body'>
|
||||
{% if locations %}
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>{% trans "Location" %}</th>
|
||||
<th>{% trans "Racks" %}</th>
|
||||
<th>{% trans "Devices" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for location in locations %}
|
||||
<tr>
|
||||
<td>
|
||||
{% for i in location.level|as_range %}<i class="mdi mdi-circle-small"></i>{% endfor %}
|
||||
{{ location|linkify }}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'dcim:rack_list' %}?location_id={{ location.pk }}">{{ location.rack_count }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'dcim:device_list' %}?location_id={{ location.pk }}">{{ location.device_count }}</a>
|
||||
</td>
|
||||
<td class="text-end noprint">
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}?location_id={{ location.pk }}" class="btn btn-sm btn-primary" title="{% trans "View Elevations" %}">
|
||||
<i class="mdi mdi-server"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<span class="text-muted">{% trans "None" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if perms.dcim.add_location %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<a href="{% url 'dcim:location_add' %}?site={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a location" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% include 'dcim/inc/nonracked_devices.html' %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">Locations</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'dcim:location_list' %}?site_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
{% if perms.dcim.add_location %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<a href="{% url 'dcim:location_add' %}?site={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Location" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">Non-Racked Devices</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'dcim:device_list' %}?site_id={{ object.pk }}&rack_id=null&parent_bay_id=null"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
{% if perms.dcim.add_device %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<a href="{% url 'dcim:device_add' %}?site={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Device" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
{% plugin_buttons object %}
|
||||
{% if object.is_active and perms.extras.add_configrevision %}
|
||||
{% url 'extras:configrevision_add' as edit_url %}
|
||||
{% include "buttons/edit.html" with url=edit_url %}
|
||||
{% endif %}
|
||||
{% if not object.is_active and perms.extras.delete_configrevision %}
|
||||
{% delete_button object %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="control-group">
|
||||
{% custom_links object %}
|
||||
@@ -23,17 +30,17 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Rack Elevation" %}</h5>
|
||||
<h5 class="card-header">{% trans "Rack Elevations" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rack elevation default unit height" %}:</th>
|
||||
<th scope="row">{% trans "Default unit height" %}</th>
|
||||
<td>{{ object.data.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rack elevation default unit width" %}:</th>
|
||||
<th scope="row">{% trans "Default unit width" %}</th>
|
||||
<td>{{ object.data.RACK_ELEVATION_DEFAULT_UNIT_WIDTH }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -41,19 +48,19 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Power" %}</h5>
|
||||
<h5 class="card-header">{% trans "Power Feeds" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Powerfeed default voltage" %}:</th>
|
||||
<th scope="row">{% trans "Default voltage" %}</th>
|
||||
<td>{{ object.data.POWERFEED_DEFAULT_VOLTAGE }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Powerfeed default amperage" %}:</th>
|
||||
<th scope="row">{% trans "Default amperage" %}</th>
|
||||
<td>{{ object.data.POWERFEED_DEFAULT_AMPERAGE }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Powerfeed default max utilization" %}:</th>
|
||||
<th scope="row">{% trans "Default max utilization" %}</th>
|
||||
<td>{{ object.data.POWERFEED_DEFAULT_MAX_UTILIZATION }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -65,11 +72,11 @@
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Enforce global unique" %}:</th>
|
||||
<th scope="row">{% trans "Enforce global unique" %}</th>
|
||||
<td>{{ object.data.ENFORCE_GLOBAL_UNIQUE }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Prefer IPv4" %}:</th>
|
||||
<th scope="row">{% trans "Prefer IPv4" %}</th>
|
||||
<td>{{ object.data.PREFER_IPV4 }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -81,8 +88,8 @@
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Allowed URL schemes" %}:</th>
|
||||
<td>{{ object.data.ALLOWED_URL_SCHEMES }}</td>
|
||||
<th scope="row">{% trans "Allowed URL schemes" %}</th>
|
||||
<td>{{ object.data.ALLOWED_URL_SCHEMES|join:", "|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -93,39 +100,35 @@
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Login banner" %}:</th>
|
||||
<th scope="row">{% trans "Login banner" %}</th>
|
||||
<td>{{ object.data.BANNER_LOGIN }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Maintenance banner" %}:</th>
|
||||
<th scope="row">{% trans "Maintenance banner" %}</th>
|
||||
<td>{{ object.data.BANNER_MAINTENANCE }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Top banner" %}:</th>
|
||||
<th scope="row">{% trans "Top banner" %}</th>
|
||||
<td>{{ object.data.BANNER_TOP }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Bottom banner" %}:</th>
|
||||
<th scope="row">{% trans "Bottom banner" %}</th>
|
||||
<td>{{ object.data.BANNER_BOTTOM }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Pagination" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Paginate count" %}:</th>
|
||||
<th scope="row">{% trans "Paginate count" %}</th>
|
||||
<td>{{ object.data.PAGINATE_COUNT }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Max page size" %}:</th>
|
||||
<th scope="row">{% trans "Max page size" %}</th>
|
||||
<td>{{ object.data.MAX_PAGE_SIZE }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -137,8 +140,8 @@
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Custom validators" %}:</th>
|
||||
<td>{{ object.data.CUSTOM_VALIDATORS }}</td>
|
||||
<th scope="row">{% trans "Custom validators" %}</th>
|
||||
<td>{{ object.data.CUSTOM_VALIDATORS|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -149,8 +152,8 @@
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Default user preferences" %}:</th>
|
||||
<td>{{ object.data.DEFAULT_USER_PREFERENCES }}</td>
|
||||
<th scope="row">{% trans "Default user preferences" %}</th>
|
||||
<td>{{ object.data.DEFAULT_USER_PREFERENCES|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -161,23 +164,23 @@
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Maintenance mode" %}:</th>
|
||||
<th scope="row">{% trans "Maintenance mode" %}</th>
|
||||
<td>{{ object.data.MAINTENANCE_MODE }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "GraphQL enabled" %}:</th>
|
||||
<th scope="row">{% trans "GraphQL enabled" %}</th>
|
||||
<td>{{ object.data.GRAPHQL_ENABLED }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Changelog retention" %}:</th>
|
||||
<th scope="row">{% trans "Changelog retention" %}</th>
|
||||
<td>{{ object.data.CHANGELOG_RETENTION }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Job retention" %}:</th>
|
||||
<th scope="row">{% trans "Job retention" %}</th>
|
||||
<td>{{ object.data.JOB_RETENTION }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Maps URL" %}:</th>
|
||||
<th scope="row">{% trans "Maps URL" %}</th>
|
||||
<td>{{ object.data.MAPS_URL }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -185,14 +188,9 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Config Revision" %}</h5>
|
||||
<h5 class="card-header">{% trans "Comment" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Comment" %}:</th>
|
||||
<td>{{ object.comment }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{ object.comment|placeholder }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -125,6 +125,24 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">Related Objects</h5>
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for qs in related_models %}
|
||||
<a class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||
{{ qs.model|meta:"verbose_name_plural"|bettertitle }}
|
||||
{% with count=qs.count %}
|
||||
{% if count %}
|
||||
<span class="badge bg-primary rounded-pill">{{ count }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-light rounded-pill">—</span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,8 +46,10 @@ Context:
|
||||
{% block subtitle %}
|
||||
<div class="object-subtitle">
|
||||
<span>{% trans "Created" %} {{ object.created|annotated_date }}</span>
|
||||
<span class="separator">·</span>
|
||||
<span>{% trans "Updated" %} <span title="{{ object.last_updated }}">{{ object.last_updated|timesince }}</span> {% trans "ago" %}</span>
|
||||
{% if object.last_updated %}
|
||||
<span class="separator">·</span>
|
||||
<span>{% trans "Updated" %} <span title="{{ object.last_updated }}">{{ object.last_updated|timesince }}</span> {% trans "ago" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock subtitle %}
|
||||
|
||||
|
||||
@@ -59,15 +59,4 @@
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "VLANs" %}</h5>
|
||||
<div class="card-body table-responsive">
|
||||
{% render_table vlans_table 'inc/table.html' %}
|
||||
{% include 'inc/paginator.html' with paginator=vlans_table.paginator page=vlans_table.page %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -60,7 +60,13 @@ class TokenProvisionView(APIView):
|
||||
"""
|
||||
permission_classes = []
|
||||
|
||||
# @extend_schema(methods=["post"], responses={201: serializers.TokenSerializer})
|
||||
@extend_schema(
|
||||
request=serializers.TokenProvisionSerializer,
|
||||
responses={
|
||||
201: serializers.TokenSerializer,
|
||||
401: OpenApiTypes.OBJECT,
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
serializer = serializers.TokenProvisionSerializer(data=request.data)
|
||||
serializer.is_valid()
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.db.models import Q
|
||||
|
||||
|
||||
OBJECTPERMISSION_OBJECT_TYPES = Q(
|
||||
~Q(app_label__in=['admin', 'auth', 'contenttypes', 'sessions', 'taggit', 'users']) |
|
||||
~Q(app_label__in=['account', 'admin', 'auth', 'contenttypes', 'sessions', 'taggit', 'users']) |
|
||||
Q(app_label='auth', model__in=['group', 'user']) |
|
||||
Q(app_label='users', model__in=['objectpermission', 'token'])
|
||||
)
|
||||
|
||||
@@ -66,7 +66,7 @@ class Migration(migrations.Migration):
|
||||
('actions', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=30), size=None)),
|
||||
('constraints', models.JSONField(blank=True, null=True)),
|
||||
('groups', models.ManyToManyField(blank=True, related_name='object_permissions', to='auth.Group')),
|
||||
('object_types', models.ManyToManyField(limit_choices_to=models.Q(models.Q(models.Q(('app_label__in', ['admin', 'auth', 'contenttypes', 'sessions', 'taggit', 'users']), _negated=True), models.Q(('app_label', 'auth'), ('model__in', ['group', 'user'])), models.Q(('app_label', 'users'), ('model__in', ['objectpermission', 'token'])), _connector='OR')), related_name='object_permissions', to='contenttypes.ContentType')),
|
||||
('object_types', models.ManyToManyField(limit_choices_to=models.Q(models.Q(models.Q(('app_label__in', ['account', 'admin', 'auth', 'contenttypes', 'sessions', 'taggit', 'users']), _negated=True), models.Q(('app_label', 'auth'), ('model__in', ['group', 'user'])), models.Q(('app_label', 'users'), ('model__in', ['objectpermission', 'token'])), _connector='OR')), related_name='object_permissions', to='contenttypes.ContentType')),
|
||||
('users', models.ManyToManyField(blank=True, related_name='object_permissions', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
|
||||
@@ -20,7 +20,8 @@ FILTER_NUMERIC_BASED_LOOKUP_MAP = dict(
|
||||
lte='lte',
|
||||
lt='lt',
|
||||
gte='gte',
|
||||
gt='gt'
|
||||
gt='gt',
|
||||
empty='isnull',
|
||||
)
|
||||
|
||||
FILTER_NEGATION_LOOKUP_MAP = dict(
|
||||
@@ -45,6 +46,10 @@ HTTP_REQUEST_META_SAFE_COPY = [
|
||||
'HTTP_REFERER',
|
||||
'HTTP_USER_AGENT',
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
'HTTP_X_FORWARDED_HOST',
|
||||
'HTTP_X_FORWARDED_PORT',
|
||||
'HTTP_X_FORWARDED_PROTO',
|
||||
'HTTP_X_REAL_IP',
|
||||
'QUERY_STRING',
|
||||
'REMOTE_ADDR',
|
||||
'REMOTE_HOST',
|
||||
|
||||
@@ -27,7 +27,7 @@ def update_counter(model, pk, counter_name, value):
|
||||
# Signal handlers
|
||||
#
|
||||
|
||||
def post_save_receiver(sender, instance, **kwargs):
|
||||
def post_save_receiver(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Update counter fields on related objects when a TrackingModelMixin subclass is created or modified.
|
||||
"""
|
||||
@@ -39,7 +39,7 @@ def post_save_receiver(sender, instance, **kwargs):
|
||||
# Update the counters on the old and/or new parents as needed
|
||||
if old_pk is not None:
|
||||
update_counter(parent_model, old_pk, counter_name, -1)
|
||||
if new_pk is not None:
|
||||
if new_pk is not None and (old_pk or created):
|
||||
update_counter(parent_model, new_pk, counter_name, 1)
|
||||
|
||||
|
||||
|
||||
@@ -105,6 +105,10 @@ class RestrictedGenericForeignKey(GenericForeignKey):
|
||||
# We avoid looking for values if either ct_id or fkey value is None
|
||||
ct_id = getattr(instance, ct_attname)
|
||||
if ct_id is not None:
|
||||
# Check if the content type actually exists
|
||||
if not self.get_content_type(id=ct_id, using=instance._state.db).model_class():
|
||||
continue
|
||||
|
||||
fk_val = getattr(instance, self.fk_field)
|
||||
if fk_val is not None:
|
||||
fk_dict[ct_id].add(fk_val)
|
||||
@@ -129,13 +133,14 @@ class RestrictedGenericForeignKey(GenericForeignKey):
|
||||
if ct_id is None:
|
||||
return None
|
||||
else:
|
||||
model = self.get_content_type(
|
||||
if model := self.get_content_type(
|
||||
id=ct_id, using=obj._state.db
|
||||
).model_class()
|
||||
return (
|
||||
model._meta.pk.get_prep_value(getattr(obj, self.fk_field)),
|
||||
model,
|
||||
)
|
||||
).model_class():
|
||||
return (
|
||||
model._meta.pk.get_prep_value(getattr(obj, self.fk_field)),
|
||||
model,
|
||||
)
|
||||
return None
|
||||
|
||||
return (
|
||||
ret_val,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% load l10n %}
|
||||
<div class="progress">
|
||||
<div
|
||||
role="progressbar"
|
||||
@@ -5,7 +6,7 @@
|
||||
aria-valuemax="100"
|
||||
aria-valuenow="{{ utilization }}"
|
||||
class="progress-bar {{ bar_class }}"
|
||||
style="width: {{ utilization }}%;"
|
||||
style="width: {{ utilization|unlocalize }}%;"
|
||||
>
|
||||
{% if utilization >= 35 %}{{ utilization|floatformat:1 }}%{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -29,13 +29,17 @@ class CountersTest(TestCase):
|
||||
self.assertEqual(device1.interface_count, 2)
|
||||
self.assertEqual(device2.interface_count, 2)
|
||||
|
||||
Interface.objects.create(device=device1, name='Interface 5')
|
||||
interface1 = Interface.objects.create(device=device1, name='Interface 5')
|
||||
Interface.objects.create(device=device2, name='Interface 6')
|
||||
device1.refresh_from_db()
|
||||
device2.refresh_from_db()
|
||||
self.assertEqual(device1.interface_count, 3)
|
||||
self.assertEqual(device2.interface_count, 3)
|
||||
|
||||
interface1.save()
|
||||
device1.refresh_from_db()
|
||||
self.assertEqual(device1.interface_count, 3)
|
||||
|
||||
def test_interface_count_deletion(self):
|
||||
"""
|
||||
When a tracked object (Interface) is deleted the tracking counter should be updated.
|
||||
|
||||
@@ -86,6 +86,10 @@ class DummyModel(models.Model):
|
||||
charfield = models.CharField(
|
||||
max_length=10
|
||||
)
|
||||
numberfield = models.IntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
choicefield = models.IntegerField(
|
||||
choices=(('A', 1), ('B', 2), ('C', 3))
|
||||
)
|
||||
@@ -108,6 +112,7 @@ class BaseFilterSetTest(TestCase):
|
||||
"""
|
||||
class DummyFilterSet(BaseFilterSet):
|
||||
charfield = django_filters.CharFilter()
|
||||
numberfield = django_filters.NumberFilter()
|
||||
macaddressfield = MACAddressFilter()
|
||||
modelchoicefield = django_filters.ModelChoiceFilter(
|
||||
field_name='integerfield', # We're pretending this is a ForeignKey field
|
||||
@@ -132,6 +137,7 @@ class BaseFilterSetTest(TestCase):
|
||||
model = DummyModel
|
||||
fields = (
|
||||
'charfield',
|
||||
'numberfield',
|
||||
'choicefield',
|
||||
'datefield',
|
||||
'datetimefield',
|
||||
@@ -171,6 +177,25 @@ class BaseFilterSetTest(TestCase):
|
||||
self.assertEqual(self.filters['charfield__iew'].exclude, False)
|
||||
self.assertEqual(self.filters['charfield__niew'].lookup_expr, 'iendswith')
|
||||
self.assertEqual(self.filters['charfield__niew'].exclude, True)
|
||||
self.assertEqual(self.filters['charfield__empty'].lookup_expr, 'empty')
|
||||
self.assertEqual(self.filters['charfield__empty'].exclude, False)
|
||||
|
||||
def test_number_filter(self):
|
||||
self.assertIsInstance(self.filters['numberfield'], django_filters.NumberFilter)
|
||||
self.assertEqual(self.filters['numberfield'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['numberfield'].exclude, False)
|
||||
self.assertEqual(self.filters['numberfield__n'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['numberfield__n'].exclude, True)
|
||||
self.assertEqual(self.filters['numberfield__lt'].lookup_expr, 'lt')
|
||||
self.assertEqual(self.filters['numberfield__lt'].exclude, False)
|
||||
self.assertEqual(self.filters['numberfield__lte'].lookup_expr, 'lte')
|
||||
self.assertEqual(self.filters['numberfield__lte'].exclude, False)
|
||||
self.assertEqual(self.filters['numberfield__gt'].lookup_expr, 'gt')
|
||||
self.assertEqual(self.filters['numberfield__gt'].exclude, False)
|
||||
self.assertEqual(self.filters['numberfield__gte'].lookup_expr, 'gte')
|
||||
self.assertEqual(self.filters['numberfield__gte'].exclude, False)
|
||||
self.assertEqual(self.filters['numberfield__empty'].lookup_expr, 'isnull')
|
||||
self.assertEqual(self.filters['numberfield__empty'].exclude, False)
|
||||
|
||||
def test_mac_address_filter(self):
|
||||
self.assertIsInstance(self.filters['macaddressfield'], MACAddressFilter)
|
||||
|
||||
@@ -7,12 +7,12 @@ import utilities.fields
|
||||
def populate_virtualmachine_counts(apps, schema_editor):
|
||||
VirtualMachine = apps.get_model('virtualization', 'VirtualMachine')
|
||||
|
||||
vms = list(VirtualMachine.objects.annotate(_interface_count=Count('interfaces', distinct=True)))
|
||||
vms = VirtualMachine.objects.annotate(_interface_count=Count('interfaces', distinct=True))
|
||||
|
||||
for vm in vms:
|
||||
vm.interface_count = vm._interface_count
|
||||
|
||||
VirtualMachine.objects.bulk_update(vms, ['interface_count'])
|
||||
VirtualMachine.objects.bulk_update(vms, ['interface_count'], batch_size=100)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -387,7 +387,6 @@ class VirtualMachineConfigContextView(ObjectConfigContextView):
|
||||
base_template = 'virtualization/virtualmachine.html'
|
||||
tab = ViewTab(
|
||||
label=_('Config Context'),
|
||||
permission='extras.view_configcontext',
|
||||
weight=2000
|
||||
)
|
||||
|
||||
|
||||
@@ -21,13 +21,13 @@ graphene-django==3.0.0
|
||||
gunicorn==21.2.0
|
||||
Jinja2==3.1.2
|
||||
Markdown==3.3.7
|
||||
mkdocs-material==9.1.21
|
||||
mkdocs-material==9.2.5
|
||||
mkdocstrings[python-legacy]==0.22.0
|
||||
netaddr==0.8.0
|
||||
Pillow==10.0.0
|
||||
psycopg[binary,pool]==3.1.10
|
||||
PyYAML==6.0.1
|
||||
sentry-sdk==1.29.2
|
||||
sentry-sdk==1.30.0
|
||||
social-auth-app-django==5.2.0
|
||||
social-auth-core[openidconnect]==4.4.2
|
||||
svgwrite==1.4.3
|
||||
|
||||
Reference in New Issue
Block a user