mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-19 01:58:43 -06:00
Compare commits
14 Commits
4e0e4598b0
...
fix-19669-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef7880a013 | ||
|
|
8fd8493d11 | ||
|
|
db805053d9 | ||
|
|
cf4db67e0b | ||
|
|
f48e1cb534 | ||
|
|
ffa9a52667 | ||
|
|
47320f9958 | ||
|
|
d08a1bd07d | ||
|
|
14c4aeca54 | ||
|
|
26bec1275f | ||
|
|
fa2d7f6516 | ||
|
|
d571cb4867 | ||
|
|
2129355c30 | ||
|
|
c40bfb1445 |
17
contrib/netbox-housekeeping.service
Normal file
17
contrib/netbox-housekeeping.service
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=NetBox Housekeeping Service
|
||||||
|
Documentation=https://docs.netbox.dev/
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
|
||||||
|
User=netbox
|
||||||
|
Group=netbox
|
||||||
|
WorkingDirectory=/opt/netbox
|
||||||
|
|
||||||
|
ExecStart=/opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py housekeeping
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
9
contrib/netbox-housekeeping.sh
Executable file
9
contrib/netbox-housekeeping.sh
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# This shell script invokes NetBox's housekeeping management command, which
|
||||||
|
# intended to be run nightly. This script can be copied into your system's
|
||||||
|
# daily cron directory (e.g. /etc/cron.daily), or referenced directly from
|
||||||
|
# within the cron configuration file.
|
||||||
|
#
|
||||||
|
# If NetBox has been installed into a nonstandard location, update the paths
|
||||||
|
# below.
|
||||||
|
/opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py housekeeping
|
||||||
13
contrib/netbox-housekeeping.timer
Normal file
13
contrib/netbox-housekeeping.timer
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=NetBox Housekeeping Timer
|
||||||
|
Documentation=https://docs.netbox.dev/
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=daily
|
||||||
|
AccuracySec=1h
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
49
docs/administration/housekeeping.md
Normal file
49
docs/administration/housekeeping.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Housekeeping
|
||||||
|
|
||||||
|
NetBox includes a `housekeeping` management command that should be run nightly. This command handles:
|
||||||
|
|
||||||
|
* Clearing expired authentication sessions from the database
|
||||||
|
* Deleting changelog records older than the configured [retention time](../configuration/miscellaneous.md#changelog_retention)
|
||||||
|
* Deleting job result records older than the configured [retention time](../configuration/miscellaneous.md#job_retention)
|
||||||
|
* Check for new NetBox releases (if [`RELEASE_CHECK_URL`](../configuration/miscellaneous.md#release_check_url) is set)
|
||||||
|
|
||||||
|
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`.
|
||||||
|
|
||||||
|
## Scheduling
|
||||||
|
|
||||||
|
### Using Cron
|
||||||
|
|
||||||
|
This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
On Debian-based systems, be sure to omit the `.sh` file extension when linking to the script from within a cron directory. Otherwise, the task may not run.
|
||||||
|
|
||||||
|
### Using Systemd
|
||||||
|
|
||||||
|
First, create symbolic links for the systemd service and timer files. Link the existing service and timer files from the `/opt/netbox/contrib/` directory to the `/etc/systemd/system/` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.service /etc/systemd/system/netbox-housekeeping.service
|
||||||
|
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.timer /etc/systemd/system/netbox-housekeeping.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, reload the systemd configuration and enable the timer to start automatically at boot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now netbox-housekeeping.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
Check the status of your timer by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl list-timers --all
|
||||||
|
```
|
||||||
|
|
||||||
|
This command will show a list of all timers, including your `netbox-housekeeping.timer`. Make sure the timer is active and properly scheduled.
|
||||||
|
|
||||||
|
That's it! Your NetBox housekeeping service is now configured to run daily using systemd.
|
||||||
@@ -72,16 +72,6 @@ Email is sent from NetBox only for critical events or if configured for [logging
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## HOSTNAME
|
|
||||||
|
|
||||||
!!! info "This parameter was introduced in NetBox v4.4."
|
|
||||||
|
|
||||||
Default: System hostname
|
|
||||||
|
|
||||||
The hostname displayed in the user interface identifying the system on which NetBox is running. If not defined, this defaults to the system hostname as reported by Python's `platform.node()`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## HTTP_PROXIES
|
## HTTP_PROXIES
|
||||||
|
|
||||||
Default: `None`
|
Default: `None`
|
||||||
@@ -169,7 +159,6 @@ LOGGING = {
|
|||||||
* `netbox.auth.*` - Authentication events
|
* `netbox.auth.*` - Authentication events
|
||||||
* `netbox.api.views.*` - Views which handle business logic for the REST API
|
* `netbox.api.views.*` - Views which handle business logic for the REST API
|
||||||
* `netbox.event_rules` - Event rules
|
* `netbox.event_rules` - Event rules
|
||||||
* `netbox.jobs.*` - Background jobs
|
|
||||||
* `netbox.reports.*` - Report execution (`module.name`)
|
* `netbox.reports.*` - Report execution (`module.name`)
|
||||||
* `netbox.scripts.*` - Custom script execution (`module.name`)
|
* `netbox.scripts.*` - Custom script execution (`module.name`)
|
||||||
* `netbox.views.*` - Views which handle business logic for the web UI
|
* `netbox.views.*` - Views which handle business logic for the web UI
|
||||||
|
|||||||
@@ -264,6 +264,18 @@ cd /opt/netbox/netbox
|
|||||||
python3 manage.py createsuperuser
|
python3 manage.py createsuperuser
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Schedule the Housekeeping Task
|
||||||
|
|
||||||
|
NetBox includes a `housekeeping` management command that handles some recurring cleanup tasks, such as clearing out old sessions and expired change records. Although this command may be run manually, it is recommended to configure a scheduled job using the system's `cron` daemon or a similar utility.
|
||||||
|
|
||||||
|
A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be copied to or linked from your system's daily cron task directory, or included within the crontab directly. (If installing NetBox into a nonstandard path, be sure to update the system paths within this script first.)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [housekeeping documentation](../administration/housekeeping.md) for further details.
|
||||||
|
|
||||||
## Test the Application
|
## Test the Application
|
||||||
|
|
||||||
At this point, we should be able to run NetBox's development server for testing. We can check by starting a development instance locally.
|
At this point, we should be able to run NetBox's development server for testing. We can check by starting a development instance locally.
|
||||||
@@ -290,13 +302,6 @@ Quit the server with CONTROL-C.
|
|||||||
|
|
||||||
Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on port 8000; for example, <http://127.0.0.1:8000/>. You should be greeted with the NetBox home page. Try logging in using the username and password specified when creating a superuser.
|
Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on port 8000; for example, <http://127.0.0.1:8000/>. You should be greeted with the NetBox home page. Try logging in using the username and password specified when creating a superuser.
|
||||||
|
|
||||||
!!! note
|
|
||||||
By default RHEL based distros will likely block your testing attempts with firewalld. The development server port can be opened with `firewall-cmd` (add `--permanent` if you want the rule to survive server restarts):
|
|
||||||
|
|
||||||
```no-highlight
|
|
||||||
firewall-cmd --zone=public --add-port=8000/tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! danger "Not for production use"
|
!!! danger "Not for production use"
|
||||||
The development server is for development and testing purposes only. It is neither performant nor secure enough for production use. **Do not use it in production.**
|
The development server is for development and testing purposes only. It is neither performant nor secure enough for production use. **Do not use it in production.**
|
||||||
|
|
||||||
|
|||||||
@@ -183,3 +183,13 @@ Finally, restart the gunicorn and RQ services:
|
|||||||
```no-highlight
|
```no-highlight
|
||||||
sudo systemctl restart netbox netbox-rq
|
sudo systemctl restart netbox netbox-rq
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 6. Verify Housekeeping Scheduling
|
||||||
|
|
||||||
|
If upgrading from a release prior to NetBox v3.0, check that a cron task (or similar scheduled process) has been configured to run NetBox's nightly housekeeping command. A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be linked from your system's daily cron task directory, or included within the crontab directly. (If NetBox has been installed in a nonstandard path, be sure to update the system paths within this script first.)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [housekeeping documentation](../administration/housekeeping.md) for further details.
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ The assignment of platforms to devices is an optional feature, and may be disreg
|
|||||||
|
|
||||||
### Name
|
### Name
|
||||||
|
|
||||||
A human-friendly name for the platform. Must be unique per manufacturer.
|
A unique human-friendly name.
|
||||||
|
|
||||||
### Slug
|
### Slug
|
||||||
|
|
||||||
A URL-friendly identifier; must be unique per manufacturer. (This value can be used for filtering.)
|
A unique URL-friendly identifier. (This value can be used for filtering.)
|
||||||
|
|
||||||
### Manufacturer
|
### Manufacturer
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,6 @@ The user-defined functional [role](./role.md) assigned to the VLAN.
|
|||||||
|
|
||||||
### VLAN Group or Site
|
### VLAN Group or Site
|
||||||
|
|
||||||
!!! warning "Site assignment is deprecated"
|
|
||||||
The assignment of individual VLANs directly to a site has been deprecated. This ability will be removed in a future NetBox release. Users are strongly encouraged to utilize VLAN groups, which have the added benefit of supporting the assignment of a VLAN to multiple sites.
|
|
||||||
|
|
||||||
The [VLAN group](./vlangroup.md) or [site](../dcim/site.md) to which the VLAN is assigned.
|
The [VLAN group](./vlangroup.md) or [site](../dcim/site.md) to which the VLAN is assigned.
|
||||||
|
|
||||||
### Q-in-Q Role
|
### Q-in-Q Role
|
||||||
|
|||||||
@@ -39,27 +39,6 @@ You can schedule the background job from within your code (e.g. from a model's `
|
|||||||
|
|
||||||
This is the human-friendly names of your background job. If omitted, the class name will be used.
|
This is the human-friendly names of your background job. If omitted, the class name will be used.
|
||||||
|
|
||||||
### Logging
|
|
||||||
|
|
||||||
!!! info "This feature was introduced in NetBox v4.4."
|
|
||||||
|
|
||||||
A Python logger is instantiated by the runner for each job. It can be utilized within a job's `run()` method as needed:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def run(self, *args, **kwargs):
|
|
||||||
obj = MyModel.objects.get(pk=kwargs.get('pk'))
|
|
||||||
self.logger.info("Retrieved object {obj}")
|
|
||||||
```
|
|
||||||
|
|
||||||
Four of the standard Python logging levels are supported:
|
|
||||||
|
|
||||||
* `debug()`
|
|
||||||
* `info()`
|
|
||||||
* `warning()`
|
|
||||||
* `error()`
|
|
||||||
|
|
||||||
Log entries recorded using the runner's logger will be saved in the job's log in the database in addition to being processed by other [system logging handlers](../../configuration/system.md#logging).
|
|
||||||
|
|
||||||
### Scheduled Jobs
|
### Scheduled Jobs
|
||||||
|
|
||||||
As described above, jobs can be scheduled for immediate execution or at any later time using the `enqueue()` method. However, for management purposes, the `enqueue_once()` method allows a job to be scheduled exactly once avoiding duplicates. If a job is already scheduled for a particular instance, a second one won't be scheduled, respecting thread safety. An example use case would be to schedule a periodic task that is bound to an instance in general, but not to any event of that instance (such as updates). The parameters of the `enqueue_once()` method are identical to those of `enqueue()`.
|
As described above, jobs can be scheduled for immediate execution or at any later time using the `enqueue()` method. However, for management purposes, the `enqueue_once()` method allows a job to be scheduled exactly once avoiding duplicates. If a job is already scheduled for a particular instance, a second one won't be scheduled, respecting thread safety. An example use case would be to schedule a periodic task that is bound to an instance in general, but not to any event of that instance (such as updates). The parameters of the `enqueue_once()` method are identical to those of `enqueue()`.
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ Generic view classes (documented below) facilitate common operations, such as cr
|
|||||||
| `ObjectListView` | View a list of objects |
|
| `ObjectListView` | View a list of objects |
|
||||||
| `BulkImportView` | Import a set of new objects |
|
| `BulkImportView` | Import a set of new objects |
|
||||||
| `BulkEditView` | Edit multiple objects |
|
| `BulkEditView` | Edit multiple objects |
|
||||||
| `BulkRenameView` | Rename multiple objects |
|
|
||||||
| `BulkDeleteView` | Delete multiple objects |
|
| `BulkDeleteView` | Delete multiple objects |
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
@@ -172,10 +171,6 @@ Below are the class definitions for NetBox's multi-object views. These views han
|
|||||||
options:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
::: netbox.views.generic.BulkRenameView
|
|
||||||
options:
|
|
||||||
members: false
|
|
||||||
|
|
||||||
::: netbox.views.generic.BulkDeleteView
|
::: netbox.views.generic.BulkDeleteView
|
||||||
options:
|
options:
|
||||||
members:
|
members:
|
||||||
|
|||||||
@@ -80,18 +80,20 @@ GET /api/ipam/vlans/?vid__gt=900
|
|||||||
|
|
||||||
String based (char) fields (Name, Address, etc) support these lookup expressions:
|
String based (char) fields (Name, Address, etc) support these lookup expressions:
|
||||||
|
|
||||||
| Filter | Description |
|
| Filter | Description |
|
||||||
|---------|----------------------------------------|
|
|----------|----------------------------------------|
|
||||||
| `n` | Not equal to |
|
| `n` | Not equal to |
|
||||||
| `ic` | Contains (case-insensitive) |
|
| `ic` | Contains (case-insensitive) |
|
||||||
| `nic` | Does not contain (case-insensitive) |
|
| `nic` | Does not contain (case-insensitive) |
|
||||||
| `isw` | Starts with (case-insensitive) |
|
| `isw` | Starts with (case-insensitive) |
|
||||||
| `nisw` | Does not start with (case-insensitive) |
|
| `nisw` | Does not start with (case-insensitive) |
|
||||||
| `iew` | Ends with (case-insensitive) |
|
| `iew` | Ends with (case-insensitive) |
|
||||||
| `niew` | Does not end with (case-insensitive) |
|
| `niew` | Does not end with (case-insensitive) |
|
||||||
| `ie` | Exact match (case-insensitive) |
|
| `ie` | Exact match (case-insensitive) |
|
||||||
| `nie` | Inverse exact match (case-insensitive) |
|
| `nie` | Inverse exact match (case-insensitive) |
|
||||||
| `empty` | Is empty/null (boolean) |
|
| `empty` | Is empty/null (boolean) |
|
||||||
|
| `regex` | Regexp matching |
|
||||||
|
| `iregex` | Regexp matching (case-insensitive) |
|
||||||
|
|
||||||
Here is an example of a lookup expression on a string field that will return all devices with `switch` in the name:
|
Here is an example of a lookup expression on a string field that will return all devices with `switch` in the name:
|
||||||
|
|
||||||
|
|||||||
@@ -434,7 +434,7 @@ A new management command has been added: `manage.py housekeeping`. This command
|
|||||||
* Delete change log records which have surpassed the configured retention period (if configured)
|
* Delete change log records which have surpassed the configured retention period (if configured)
|
||||||
* Check for new NetBox releases (if enabled)
|
* Check for new NetBox releases (if enabled)
|
||||||
|
|
||||||
A convenience script for calling this command via an automated scheduler has been included at `/contrib/netbox-housekeeping.sh`. Please see the housekeeping documentation for further details.
|
A convenience script for calling this command via an automated scheduler has been included at `/contrib/netbox-housekeeping.sh`. Please see the [housekeeping documentation](../administration/housekeeping.md) for further details.
|
||||||
|
|
||||||
#### Custom Queue Support for Plugins ([#6651](https://github.com/netbox-community/netbox/issues/6651))
|
#### Custom Queue Support for Plugins ([#6651](https://github.com/netbox-community/netbox/issues/6651))
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ nav:
|
|||||||
- Okta: 'administration/authentication/okta.md'
|
- Okta: 'administration/authentication/okta.md'
|
||||||
- Permissions: 'administration/permissions.md'
|
- Permissions: 'administration/permissions.md'
|
||||||
- Error Reporting: 'administration/error-reporting.md'
|
- Error Reporting: 'administration/error-reporting.md'
|
||||||
|
- Housekeeping: 'administration/housekeeping.md'
|
||||||
- Replicating NetBox: 'administration/replicating-netbox.md'
|
- Replicating NetBox: 'administration/replicating-netbox.md'
|
||||||
- NetBox Shell: 'administration/netbox-shell.md'
|
- NetBox Shell: 'administration/netbox-shell.md'
|
||||||
- Data Model:
|
- Data Model:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from dcim.views import PathTraceView
|
from dcim.views import PathTraceView
|
||||||
from ipam.models import ASN
|
from ipam.models import ASN
|
||||||
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
|
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.query import count_related
|
from utilities.query import count_related
|
||||||
@@ -80,11 +79,6 @@ class ProviderBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.ProviderBulkEditForm
|
form = forms.ProviderBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Provider, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class ProviderBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = Provider.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Provider, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(Provider, 'bulk_delete', path='delete', detail=False)
|
||||||
class ProviderBulkDeleteView(generic.BulkDeleteView):
|
class ProviderBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Provider.objects.annotate(
|
queryset = Provider.objects.annotate(
|
||||||
@@ -147,11 +141,6 @@ class ProviderAccountBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.ProviderAccountBulkEditForm
|
form = forms.ProviderAccountBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ProviderAccount, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class ProviderAccountBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = ProviderAccount.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ProviderAccount, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(ProviderAccount, 'bulk_delete', path='delete', detail=False)
|
||||||
class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
|
class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = ProviderAccount.objects.annotate(
|
queryset = ProviderAccount.objects.annotate(
|
||||||
@@ -223,11 +212,6 @@ class ProviderNetworkBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.ProviderNetworkBulkEditForm
|
form = forms.ProviderNetworkBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ProviderNetwork, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class ProviderNetworkBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = ProviderNetwork.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ProviderNetwork, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(ProviderNetwork, 'bulk_delete', path='delete', detail=False)
|
||||||
class ProviderNetworkBulkDeleteView(generic.BulkDeleteView):
|
class ProviderNetworkBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = ProviderNetwork.objects.all()
|
queryset = ProviderNetwork.objects.all()
|
||||||
@@ -287,11 +271,6 @@ class CircuitTypeBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.CircuitTypeBulkEditForm
|
form = forms.CircuitTypeBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(CircuitType, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class CircuitTypeBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = CircuitType.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(CircuitType, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(CircuitType, 'bulk_delete', path='delete', detail=False)
|
||||||
class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = CircuitType.objects.annotate(
|
queryset = CircuitType.objects.annotate(
|
||||||
@@ -358,12 +337,6 @@ class CircuitBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.CircuitBulkEditForm
|
form = forms.CircuitBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Circuit, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class CircuitBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = Circuit.objects.all()
|
|
||||||
field_name = 'cid'
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Circuit, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(Circuit, 'bulk_delete', path='delete', detail=False)
|
||||||
class CircuitBulkDeleteView(generic.BulkDeleteView):
|
class CircuitBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Circuit.objects.prefetch_related(
|
queryset = Circuit.objects.prefetch_related(
|
||||||
@@ -459,7 +432,6 @@ class CircuitTerminationListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.CircuitTerminationFilterSet
|
filterset = filtersets.CircuitTerminationFilterSet
|
||||||
filterset_form = forms.CircuitTerminationFilterForm
|
filterset_form = forms.CircuitTerminationFilterForm
|
||||||
table = tables.CircuitTerminationTable
|
table = tables.CircuitTerminationTable
|
||||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(CircuitTermination)
|
@register_model_view(CircuitTermination)
|
||||||
@@ -554,11 +526,6 @@ class CircuitGroupBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.CircuitGroupBulkEditForm
|
form = forms.CircuitGroupBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(CircuitGroup, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class CircuitGroupBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = CircuitGroup.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(CircuitGroup, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(CircuitGroup, 'bulk_delete', path='delete', detail=False)
|
||||||
class CircuitGroupBulkDeleteView(generic.BulkDeleteView):
|
class CircuitGroupBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = CircuitGroup.objects.all()
|
queryset = CircuitGroup.objects.all()
|
||||||
@@ -576,7 +543,6 @@ class CircuitGroupAssignmentListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.CircuitGroupAssignmentFilterSet
|
filterset = filtersets.CircuitGroupAssignmentFilterSet
|
||||||
filterset_form = forms.CircuitGroupAssignmentFilterForm
|
filterset_form = forms.CircuitGroupAssignmentFilterForm
|
||||||
table = tables.CircuitGroupAssignmentTable
|
table = tables.CircuitGroupAssignmentTable
|
||||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(CircuitGroupAssignment)
|
@register_model_view(CircuitGroupAssignment)
|
||||||
@@ -669,11 +635,6 @@ class VirtualCircuitTypeBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.VirtualCircuitTypeBulkEditForm
|
form = forms.VirtualCircuitTypeBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VirtualCircuitType, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class VirtualCircuitTypeBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = VirtualCircuitType.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VirtualCircuitType, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(VirtualCircuitType, 'bulk_delete', path='delete', detail=False)
|
||||||
class VirtualCircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
class VirtualCircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = VirtualCircuitType.objects.annotate(
|
queryset = VirtualCircuitType.objects.annotate(
|
||||||
@@ -736,12 +697,6 @@ class VirtualCircuitBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.VirtualCircuitBulkEditForm
|
form = forms.VirtualCircuitBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VirtualCircuit, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class VirtualCircuitulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = VirtualCircuit.objects.all()
|
|
||||||
field_name = 'cid'
|
|
||||||
|
|
||||||
|
|
||||||
class VirtualCircuitBulkDeleteView(generic.BulkDeleteView):
|
class VirtualCircuitBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = VirtualCircuit.objects.annotate(
|
queryset = VirtualCircuit.objects.annotate(
|
||||||
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
|
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
|
||||||
@@ -759,7 +714,6 @@ class VirtualCircuitTerminationListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.VirtualCircuitTerminationFilterSet
|
filterset = filtersets.VirtualCircuitTerminationFilterSet
|
||||||
filterset_form = forms.VirtualCircuitTerminationFilterForm
|
filterset_form = forms.VirtualCircuitTerminationFilterForm
|
||||||
table = tables.VirtualCircuitTerminationTable
|
table = tables.VirtualCircuitTerminationTable
|
||||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VirtualCircuitTermination)
|
@register_model_view(VirtualCircuitTermination)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from .serializers_.change_logging import *
|
from .serializers_.change_logging import *
|
||||||
from .serializers_.data import *
|
from .serializers_.data import *
|
||||||
from .serializers_.jobs import *
|
from .serializers_.jobs import *
|
||||||
from .serializers_.object_types import *
|
|
||||||
from .serializers_.tasks import *
|
from .serializers_.tasks import *
|
||||||
|
|||||||
@@ -23,6 +23,6 @@ class JobSerializer(BaseModelSerializer):
|
|||||||
model = Job
|
model = Job
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled',
|
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled',
|
||||||
'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'log_entries',
|
'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id',
|
||||||
]
|
]
|
||||||
brief_fields = ('url', 'created', 'completed', 'user', 'status')
|
brief_fields = ('url', 'created', 'completed', 'user', 'status')
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import inspect
|
|
||||||
|
|
||||||
from django.urls import NoReverseMatch, reverse
|
|
||||||
from drf_spectacular.types import OpenApiTypes
|
|
||||||
from drf_spectacular.utils import extend_schema_field
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from core.models import ObjectType
|
|
||||||
from netbox.api.serializers import BaseModelSerializer
|
|
||||||
from utilities.views import get_viewname
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
'ObjectTypeSerializer',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ObjectTypeSerializer(BaseModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:objecttype-detail')
|
|
||||||
app_name = serializers.CharField(source='app_verbose_name', read_only=True)
|
|
||||||
model_name = serializers.CharField(source='model_verbose_name', read_only=True)
|
|
||||||
model_name_plural = serializers.CharField(source='model_verbose_name_plural', read_only=True)
|
|
||||||
is_plugin_model = serializers.BooleanField(read_only=True)
|
|
||||||
rest_api_endpoint = serializers.SerializerMethodField()
|
|
||||||
description = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ObjectType
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'display', 'app_label', 'app_name', 'model', 'model_name', 'model_name_plural',
|
|
||||||
'is_plugin_model', 'rest_api_endpoint', 'description',
|
|
||||||
]
|
|
||||||
|
|
||||||
@extend_schema_field(OpenApiTypes.STR)
|
|
||||||
def get_rest_api_endpoint(self, obj):
|
|
||||||
if not (model := obj.model_class()):
|
|
||||||
return
|
|
||||||
if viewname := get_viewname(model, action='list', rest_api=True):
|
|
||||||
try:
|
|
||||||
return reverse(viewname)
|
|
||||||
except NoReverseMatch:
|
|
||||||
return
|
|
||||||
|
|
||||||
@extend_schema_field(OpenApiTypes.STR)
|
|
||||||
def get_description(self, obj):
|
|
||||||
if not (model := obj.model_class()):
|
|
||||||
return
|
|
||||||
return inspect.getdoc(model)
|
|
||||||
@@ -10,7 +10,6 @@ router.register('data-sources', views.DataSourceViewSet)
|
|||||||
router.register('data-files', views.DataFileViewSet)
|
router.register('data-files', views.DataFileViewSet)
|
||||||
router.register('jobs', views.JobViewSet)
|
router.register('jobs', views.JobViewSet)
|
||||||
router.register('object-changes', views.ObjectChangeViewSet)
|
router.register('object-changes', views.ObjectChangeViewSet)
|
||||||
router.register('object-types', views.ObjectTypeViewSet)
|
|
||||||
router.register('background-queues', views.BackgroundQueueViewSet, basename='rqqueue')
|
router.register('background-queues', views.BackgroundQueueViewSet, basename='rqqueue')
|
||||||
router.register('background-workers', views.BackgroundWorkerViewSet, basename='rqworker')
|
router.register('background-workers', views.BackgroundWorkerViewSet, basename='rqworker')
|
||||||
router.register('background-tasks', views.BackgroundTaskViewSet, basename='rqtask')
|
router.register('background-tasks', views.BackgroundTaskViewSet, basename='rqtask')
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs, requeue_rq_jo
|
|||||||
from django_rq.queues import get_redis_connection
|
from django_rq.queues import get_redis_connection
|
||||||
from django_rq.utils import get_statistics
|
from django_rq.utils import get_statistics
|
||||||
from django_rq.settings import QUEUES_LIST
|
from django_rq.settings import QUEUES_LIST
|
||||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
|
||||||
from netbox.api.metadata import ContentTypeMetadata
|
from netbox.api.metadata import ContentTypeMetadata
|
||||||
from netbox.api.pagination import LimitOffsetListPagination
|
from netbox.api.pagination import LimitOffsetListPagination
|
||||||
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
|
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
|
||||||
@@ -86,16 +85,6 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet):
|
|||||||
filterset_class = filtersets.ObjectChangeFilterSet
|
filterset_class = filtersets.ObjectChangeFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ObjectTypeViewSet(ReadOnlyModelViewSet):
|
|
||||||
"""
|
|
||||||
Read-only list of ObjectTypes.
|
|
||||||
"""
|
|
||||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
|
||||||
queryset = ObjectType.objects.order_by('app_label', 'model')
|
|
||||||
serializer_class = serializers.ObjectTypeSerializer
|
|
||||||
filterset_class = filtersets.ObjectTypeFilterSet
|
|
||||||
|
|
||||||
|
|
||||||
class BaseRQViewSet(viewsets.ViewSet):
|
class BaseRQViewSet(viewsets.ViewSet):
|
||||||
"""
|
"""
|
||||||
Base class for RQ view sets. Provides a list() method. Subclasses must implement get_data().
|
Base class for RQ view sets. Provides a list() method. Subclasses must implement get_data().
|
||||||
|
|||||||
@@ -4,31 +4,23 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from rq.job import JobStatus
|
from rq.job import JobStatus
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'JOB_LOG_ENTRY_LEVELS',
|
|
||||||
'RQ_TASK_STATUSES',
|
'RQ_TASK_STATUSES',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Badge:
|
class Status:
|
||||||
label: str
|
label: str
|
||||||
color: str
|
color: str
|
||||||
|
|
||||||
|
|
||||||
RQ_TASK_STATUSES = {
|
RQ_TASK_STATUSES = {
|
||||||
JobStatus.QUEUED: Badge(_('Queued'), 'cyan'),
|
JobStatus.QUEUED: Status(_('Queued'), 'cyan'),
|
||||||
JobStatus.FINISHED: Badge(_('Finished'), 'green'),
|
JobStatus.FINISHED: Status(_('Finished'), 'green'),
|
||||||
JobStatus.FAILED: Badge(_('Failed'), 'red'),
|
JobStatus.FAILED: Status(_('Failed'), 'red'),
|
||||||
JobStatus.STARTED: Badge(_('Started'), 'blue'),
|
JobStatus.STARTED: Status(_('Started'), 'blue'),
|
||||||
JobStatus.DEFERRED: Badge(_('Deferred'), 'gray'),
|
JobStatus.DEFERRED: Status(_('Deferred'), 'gray'),
|
||||||
JobStatus.SCHEDULED: Badge(_('Scheduled'), 'purple'),
|
JobStatus.SCHEDULED: Status(_('Scheduled'), 'purple'),
|
||||||
JobStatus.STOPPED: Badge(_('Stopped'), 'orange'),
|
JobStatus.STOPPED: Status(_('Stopped'), 'orange'),
|
||||||
JobStatus.CANCELED: Badge(_('Cancelled'), 'yellow'),
|
JobStatus.CANCELED: Status(_('Cancelled'), 'yellow'),
|
||||||
}
|
|
||||||
|
|
||||||
JOB_LOG_ENTRY_LEVELS = {
|
|
||||||
'debug': Badge(_('Debug'), 'gray'),
|
|
||||||
'info': Badge(_('Info'), 'blue'),
|
|
||||||
'warning': Badge(_('Warning'), 'orange'),
|
|
||||||
'error': Badge(_('Error'), 'red'),
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
'JobLogEntry',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class JobLogEntry:
|
|
||||||
level: str
|
|
||||||
message: str
|
|
||||||
timestamp: datetime = field(default_factory=timezone.now)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_logrecord(cls, record: logging.LogRecord):
|
|
||||||
return cls(record.levelname.lower(), record.msg)
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import django_filters
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
import django_filters
|
||||||
|
|
||||||
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
|
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
|
||||||
from netbox.utils import get_data_backend_choices
|
from netbox.utils import get_data_backend_choices
|
||||||
from users.models import User
|
from users.models import User
|
||||||
@@ -16,7 +17,6 @@ __all__ = (
|
|||||||
'DataSourceFilterSet',
|
'DataSourceFilterSet',
|
||||||
'JobFilterSet',
|
'JobFilterSet',
|
||||||
'ObjectChangeFilterSet',
|
'ObjectChangeFilterSet',
|
||||||
'ObjectTypeFilterSet',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -134,25 +134,6 @@ class JobFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ObjectTypeFilterSet(django_filters.FilterSet):
|
|
||||||
q = django_filters.CharFilter(
|
|
||||||
method='search',
|
|
||||||
label=_('Search'),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ObjectType
|
|
||||||
fields = ('id', 'app_label', 'model')
|
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
|
||||||
if not value.strip():
|
|
||||||
return queryset
|
|
||||||
return queryset.filter(
|
|
||||||
Q(app_label__icontains=value) |
|
|
||||||
Q(model__icontains=value)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ObjectChangeFilterSet(BaseFilterSet):
|
class ObjectChangeFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
|
||||||
from datetime import timedelta
|
|
||||||
from importlib import import_module
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from django.conf import settings
|
import sys
|
||||||
from django.core.cache import cache
|
|
||||||
from django.utils import timezone
|
|
||||||
from packaging import version
|
|
||||||
|
|
||||||
from core.models import Job, ObjectChange
|
from django.conf import settings
|
||||||
from netbox.config import Config
|
|
||||||
from netbox.jobs import JobRunner, system_job
|
from netbox.jobs import JobRunner, system_job
|
||||||
from netbox.search.backends import search_backend
|
from netbox.search.backends import search_backend
|
||||||
from utilities.proxy import resolve_proxies
|
from utilities.proxy import resolve_proxies
|
||||||
from .choices import DataSourceStatusChoices, JobIntervalChoices
|
from .choices import DataSourceStatusChoices, JobIntervalChoices
|
||||||
|
from .exceptions import SyncError
|
||||||
from .models import DataSource
|
from .models import DataSource
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -30,23 +23,19 @@ class SyncDataSourceJob(JobRunner):
|
|||||||
|
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
datasource = DataSource.objects.get(pk=self.job.object_id)
|
datasource = DataSource.objects.get(pk=self.job.object_id)
|
||||||
self.logger.debug(f"Found DataSource ID {datasource.pk}")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.logger.info(f"Syncing data source {datasource}")
|
|
||||||
datasource.sync()
|
datasource.sync()
|
||||||
|
|
||||||
# Update the search cache for DataFiles belonging to this source
|
# Update the search cache for DataFiles belonging to this source
|
||||||
self.logger.debug("Updating search cache for data files")
|
|
||||||
search_backend.cache(datasource.datafiles.iterator())
|
search_backend.cache(datasource.datafiles.iterator())
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error syncing data source: {e}")
|
|
||||||
DataSource.objects.filter(pk=datasource.pk).update(status=DataSourceStatusChoices.FAILED)
|
DataSource.objects.filter(pk=datasource.pk).update(status=DataSourceStatusChoices.FAILED)
|
||||||
|
if type(e) is SyncError:
|
||||||
|
logging.error(e)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
self.logger.info("Syncing completed successfully")
|
|
||||||
|
|
||||||
|
|
||||||
@system_job(interval=JobIntervalChoices.INTERVAL_DAILY)
|
@system_job(interval=JobIntervalChoices.INTERVAL_DAILY)
|
||||||
class SystemHousekeepingJob(JobRunner):
|
class SystemHousekeepingJob(JobRunner):
|
||||||
@@ -61,23 +50,16 @@ class SystemHousekeepingJob(JobRunner):
|
|||||||
if settings.DEBUG or 'test' in sys.argv:
|
if settings.DEBUG or 'test' in sys.argv:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# TODO: Migrate other housekeeping functions from the `housekeeping` management command.
|
||||||
self.send_census_report()
|
self.send_census_report()
|
||||||
self.clear_expired_sessions()
|
|
||||||
self.prune_changelog()
|
|
||||||
self.delete_expired_jobs()
|
|
||||||
self.check_for_new_releases()
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def send_census_report():
|
def send_census_report():
|
||||||
"""
|
"""
|
||||||
Send a census report (if enabled).
|
Send a census report (if enabled).
|
||||||
"""
|
"""
|
||||||
logging.info("Reporting census data...")
|
# Skip if census reporting is disabled
|
||||||
if settings.ISOLATED_DEPLOYMENT:
|
if settings.ISOLATED_DEPLOYMENT or not settings.CENSUS_REPORTING_ENABLED:
|
||||||
logging.info("ISOLATED_DEPLOYMENT is enabled; skipping")
|
|
||||||
return
|
|
||||||
if not settings.CENSUS_REPORTING_ENABLED:
|
|
||||||
logging.info("CENSUS_REPORTING_ENABLED is disabled; skipping")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
census_data = {
|
census_data = {
|
||||||
@@ -94,94 +76,3 @@ class SystemHousekeepingJob(JobRunner):
|
|||||||
)
|
)
|
||||||
except requests.exceptions.RequestException:
|
except requests.exceptions.RequestException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def clear_expired_sessions():
|
|
||||||
"""
|
|
||||||
Clear any expired sessions from the database.
|
|
||||||
"""
|
|
||||||
logging.info("Clearing expired sessions...")
|
|
||||||
engine = import_module(settings.SESSION_ENGINE)
|
|
||||||
try:
|
|
||||||
engine.SessionStore.clear_expired()
|
|
||||||
logging.info("Sessions cleared.")
|
|
||||||
except NotImplementedError:
|
|
||||||
logging.warning(
|
|
||||||
f"The configured session engine ({settings.SESSION_ENGINE}) does not support "
|
|
||||||
f"clearing sessions; skipping."
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def prune_changelog():
|
|
||||||
"""
|
|
||||||
Delete any ObjectChange records older than the configured changelog retention time (if any).
|
|
||||||
"""
|
|
||||||
logging.info("Pruning old changelog entries...")
|
|
||||||
config = Config()
|
|
||||||
if not config.CHANGELOG_RETENTION:
|
|
||||||
logging.info("No retention period specified; skipping.")
|
|
||||||
return
|
|
||||||
|
|
||||||
cutoff = timezone.now() - timedelta(days=config.CHANGELOG_RETENTION)
|
|
||||||
logging.debug(f"Retention period: {config.CHANGELOG_RETENTION} days")
|
|
||||||
logging.debug(f"Cut-off time: {cutoff}")
|
|
||||||
|
|
||||||
count = ObjectChange.objects.filter(time__lt=cutoff).delete()[0]
|
|
||||||
logging.info(f"Deleted {count} expired records")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def delete_expired_jobs():
|
|
||||||
"""
|
|
||||||
Delete any jobs older than the configured retention period (if any).
|
|
||||||
"""
|
|
||||||
logging.info("Deleting expired jobs...")
|
|
||||||
config = Config()
|
|
||||||
if not config.JOB_RETENTION:
|
|
||||||
logging.info("No retention period specified; skipping.")
|
|
||||||
return
|
|
||||||
|
|
||||||
cutoff = timezone.now() - timedelta(days=config.JOB_RETENTION)
|
|
||||||
logging.debug(f"Retention period: {config.CHANGELOG_RETENTION} days")
|
|
||||||
logging.debug(f"Cut-off time: {cutoff}")
|
|
||||||
|
|
||||||
count = Job.objects.filter(created__lt=cutoff).delete()[0]
|
|
||||||
logging.info(f"Deleted {count} expired records")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_for_new_releases():
|
|
||||||
"""
|
|
||||||
Check for new releases and cache the latest release.
|
|
||||||
"""
|
|
||||||
logging.info("Checking for new releases...")
|
|
||||||
if settings.ISOLATED_DEPLOYMENT:
|
|
||||||
logging.info("ISOLATED_DEPLOYMENT is enabled; skipping")
|
|
||||||
return
|
|
||||||
if not settings.RELEASE_CHECK_URL:
|
|
||||||
logging.info("RELEASE_CHECK_URL is not set; skipping")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Fetch the latest releases
|
|
||||||
logging.debug(f"Release check URL: {settings.RELEASE_CHECK_URL}")
|
|
||||||
try:
|
|
||||||
response = requests.get(
|
|
||||||
url=settings.RELEASE_CHECK_URL,
|
|
||||||
headers={'Accept': 'application/vnd.github.v3+json'},
|
|
||||||
proxies=resolve_proxies(url=settings.RELEASE_CHECK_URL)
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
except requests.exceptions.RequestException as exc:
|
|
||||||
logging.error(f"Error fetching release: {exc}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Determine the most recent stable release
|
|
||||||
releases = []
|
|
||||||
for release in response.json():
|
|
||||||
if 'tag_name' not in release or release.get('devrelease') or release.get('prerelease'):
|
|
||||||
continue
|
|
||||||
releases.append((version.parse(release['tag_name']), release.get('html_url')))
|
|
||||||
latest_release = max(releases)
|
|
||||||
logging.debug(f"Found {len(response.json())} releases; {len(releases)} usable")
|
|
||||||
logging.info(f"Latest release: {latest_release[0]}")
|
|
||||||
|
|
||||||
# Cache the most recent release
|
|
||||||
cache.set('latest_release', latest_release, None)
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import django.contrib.postgres.fields
|
|
||||||
import django.core.serializers.json
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import utilities.json
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('core', '0015_remove_redundant_indexes'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='job',
|
|
||||||
name='log_entries',
|
|
||||||
field=django.contrib.postgres.fields.ArrayField(
|
|
||||||
base_field=models.JSONField(
|
|
||||||
decoder=utilities.json.JobLogDecoder,
|
|
||||||
encoder=django.core.serializers.json.DjangoJSONEncoder
|
|
||||||
),
|
|
||||||
blank=True,
|
|
||||||
default=list,
|
|
||||||
size=None
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType, ContentTypeManager
|
from django.contrib.contenttypes.models import ContentType, ContentTypeManager
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from netbox.plugins import PluginConfig
|
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from utilities.string import title
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ObjectType',
|
'ObjectType',
|
||||||
@@ -50,29 +48,3 @@ class ObjectType(ContentType):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
proxy = True
|
proxy = True
|
||||||
|
|
||||||
@property
|
|
||||||
def app_labeled_name(self):
|
|
||||||
# Override ContentType's "app | model" representation style.
|
|
||||||
return f"{self.app_verbose_name} > {title(self.model_verbose_name)}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def app_verbose_name(self):
|
|
||||||
if model := self.model_class():
|
|
||||||
return model._meta.app_config.verbose_name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def model_verbose_name(self):
|
|
||||||
if model := self.model_class():
|
|
||||||
return model._meta.verbose_name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def model_verbose_name_plural(self):
|
|
||||||
if model := self.model_class():
|
|
||||||
return model._meta.verbose_name_plural
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_plugin_model(self):
|
|
||||||
if not (model := self.model_class()):
|
|
||||||
return # Return null if model class is invalid
|
|
||||||
return isinstance(model._meta.app_config, PluginConfig)
|
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import logging
|
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import asdict
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
import django_rq
|
import django_rq
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.postgres.fields import ArrayField
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
@@ -17,10 +14,8 @@ from django.utils.translation import gettext as _
|
|||||||
from rq.exceptions import InvalidJobOperation
|
from rq.exceptions import InvalidJobOperation
|
||||||
|
|
||||||
from core.choices import JobStatusChoices
|
from core.choices import JobStatusChoices
|
||||||
from core.dataclasses import JobLogEntry
|
|
||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
from core.signals import job_end, job_start
|
from core.signals import job_end, job_start
|
||||||
from utilities.json import JobLogDecoder
|
|
||||||
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
|
||||||
|
|
||||||
@@ -109,15 +104,6 @@ class Job(models.Model):
|
|||||||
verbose_name=_('job ID'),
|
verbose_name=_('job ID'),
|
||||||
unique=True
|
unique=True
|
||||||
)
|
)
|
||||||
log_entries = ArrayField(
|
|
||||||
verbose_name=_('log entries'),
|
|
||||||
base_field=models.JSONField(
|
|
||||||
encoder=DjangoJSONEncoder,
|
|
||||||
decoder=JobLogDecoder,
|
|
||||||
),
|
|
||||||
blank=True,
|
|
||||||
default=list,
|
|
||||||
)
|
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
@@ -130,7 +116,7 @@ class Job(models.Model):
|
|||||||
verbose_name_plural = _('jobs')
|
verbose_name_plural = _('jobs')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return str(self.job_id)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
# TODO: Employ dynamic registration
|
# TODO: Employ dynamic registration
|
||||||
@@ -218,13 +204,6 @@ class Job(models.Model):
|
|||||||
# Send signal
|
# Send signal
|
||||||
job_end.send(self)
|
job_end.send(self)
|
||||||
|
|
||||||
def log(self, record: logging.LogRecord):
|
|
||||||
"""
|
|
||||||
Record a LogRecord from Python's native logging in the job's log.
|
|
||||||
"""
|
|
||||||
entry = JobLogEntry.from_logrecord(record)
|
|
||||||
self.log_entries.append(asdict(entry))
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def enqueue(
|
def enqueue(
|
||||||
cls,
|
cls,
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
from django.utils.translation import gettext as _
|
|
||||||
|
|
||||||
from netbox.object_actions import ObjectAction
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
'BulkSync',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BulkSync(ObjectAction):
|
|
||||||
"""
|
|
||||||
Synchronize multiple objects at once.
|
|
||||||
"""
|
|
||||||
name = 'bulk_sync'
|
|
||||||
label = _('Sync Data')
|
|
||||||
multi = True
|
|
||||||
permissions_required = {'sync'}
|
|
||||||
template_name = 'core/buttons/bulk_sync.html'
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
from core.constants import RQ_TASK_STATUSES
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'BackendTypeColumn',
|
'BackendTypeColumn',
|
||||||
'BadgeColumn',
|
'RQJobStatusColumn',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -22,21 +23,14 @@ class BackendTypeColumn(tables.Column):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class BadgeColumn(tables.Column):
|
class RQJobStatusColumn(tables.Column):
|
||||||
"""
|
"""
|
||||||
Render a colored badge for a value.
|
Render a colored label for the status of an RQ job.
|
||||||
|
|
||||||
Args:
|
|
||||||
badges: A dictionary mapping of values to core.constants.Badge instances.
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, badges, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.badges = badges
|
|
||||||
|
|
||||||
def render(self, value):
|
def render(self, value):
|
||||||
badge = self.badges.get(value)
|
status = RQ_TASK_STATUSES.get(value)
|
||||||
return mark_safe(f'<span class="badge text-bg-{badge.color}">{badge.label}</span>')
|
return mark_safe(f'<span class="badge text-bg-{status.color}">{status.label}</span>')
|
||||||
|
|
||||||
def value(self, value):
|
def value(self, value):
|
||||||
badge = self.badges.get(value)
|
status = RQ_TASK_STATUSES.get(value)
|
||||||
return badge.label
|
return status.label
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from netbox.tables import BaseTable, NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from core.constants import JOB_LOG_ENTRY_LEVELS
|
from ..models import Job
|
||||||
from core.models import Job
|
|
||||||
from core.tables.columns import BadgeColumn
|
|
||||||
|
|
||||||
|
|
||||||
class JobTable(NetBoxTable):
|
class JobTable(NetBoxTable):
|
||||||
@@ -42,9 +40,6 @@ class JobTable(NetBoxTable):
|
|||||||
completed = columns.DateTimeColumn(
|
completed = columns.DateTimeColumn(
|
||||||
verbose_name=_('Completed'),
|
verbose_name=_('Completed'),
|
||||||
)
|
)
|
||||||
log_entries = tables.Column(
|
|
||||||
verbose_name=_('Log Entries'),
|
|
||||||
)
|
|
||||||
actions = columns.ActionsColumn(
|
actions = columns.ActionsColumn(
|
||||||
actions=('delete',)
|
actions=('delete',)
|
||||||
)
|
)
|
||||||
@@ -58,24 +53,3 @@ class JobTable(NetBoxTable):
|
|||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user',
|
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user',
|
||||||
)
|
)
|
||||||
|
|
||||||
def render_log_entries(self, value):
|
|
||||||
return len(value)
|
|
||||||
|
|
||||||
|
|
||||||
class JobLogEntryTable(BaseTable):
|
|
||||||
timestamp = columns.DateTimeColumn(
|
|
||||||
timespec='milliseconds',
|
|
||||||
verbose_name=_('Time'),
|
|
||||||
)
|
|
||||||
level = BadgeColumn(
|
|
||||||
badges=JOB_LOG_ENTRY_LEVELS,
|
|
||||||
verbose_name=_('Level'),
|
|
||||||
)
|
|
||||||
message = tables.Column(
|
|
||||||
verbose_name=_('Message'),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
|
||||||
empty_text = _('No log entries')
|
|
||||||
fields = ('timestamp', 'level', 'message')
|
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import django_tables2 as tables
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_tables2.utils import A
|
from django_tables2.utils import A
|
||||||
|
|
||||||
from core.constants import RQ_TASK_STATUSES
|
from core.tables.columns import RQJobStatusColumn
|
||||||
from core.tables.columns import BadgeColumn
|
|
||||||
from netbox.tables import BaseTable, columns
|
from netbox.tables import BaseTable, columns
|
||||||
|
|
||||||
|
|
||||||
@@ -85,8 +84,7 @@ class BackgroundTaskTable(BaseTable):
|
|||||||
ended_at = columns.DateTimeColumn(
|
ended_at = columns.DateTimeColumn(
|
||||||
verbose_name=_("Ended")
|
verbose_name=_("Ended")
|
||||||
)
|
)
|
||||||
status = BadgeColumn(
|
status = RQJobStatusColumn(
|
||||||
badges=RQ_TASK_STATUSES,
|
|
||||||
verbose_name=_("Status"),
|
verbose_name=_("Status"),
|
||||||
accessor='get_status'
|
accessor='get_status'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from django.utils import timezone
|
|||||||
from rq.job import Job as RQ_Job, JobStatus
|
from rq.job import Job as RQ_Job, JobStatus
|
||||||
from rq.registry import FailedJobRegistry, StartedJobRegistry
|
from rq.registry import FailedJobRegistry, StartedJobRegistry
|
||||||
|
|
||||||
from rest_framework import status
|
|
||||||
from users.models import Token, User
|
from users.models import Token, User
|
||||||
from utilities.testing import APITestCase, APIViewTestCases, TestCase
|
from utilities.testing import APITestCase, APIViewTestCases, TestCase
|
||||||
from utilities.testing.utils import disable_logging
|
from utilities.testing.utils import disable_logging
|
||||||
@@ -102,22 +101,6 @@ class DataFileTest(
|
|||||||
DataFile.objects.bulk_create(data_files)
|
DataFile.objects.bulk_create(data_files)
|
||||||
|
|
||||||
|
|
||||||
class ObjectTypeTest(APITestCase):
|
|
||||||
|
|
||||||
def test_list_objects(self):
|
|
||||||
object_type_count = ObjectType.objects.count()
|
|
||||||
|
|
||||||
response = self.client.get(reverse('extras-api:objecttype-list'), **self.header)
|
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
||||||
self.assertEqual(response.data['count'], object_type_count)
|
|
||||||
|
|
||||||
def test_get_object(self):
|
|
||||||
object_type = ObjectType.objects.first()
|
|
||||||
|
|
||||||
url = reverse('extras-api:objecttype-detail', kwargs={'pk': object_type.pk})
|
|
||||||
self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
|
||||||
class BackgroundTaskTestCase(TestCase):
|
class BackgroundTaskTestCase(TestCase):
|
||||||
user_permissions = ()
|
user_permissions = ()
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ from rq.worker_registration import clean_worker_registry
|
|||||||
|
|
||||||
from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs_from_status, requeue_rq_job, stop_rq_job
|
from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs_from_status, requeue_rq_job, stop_rq_job
|
||||||
from netbox.config import get_config, PARAMS
|
from netbox.config import get_config, PARAMS
|
||||||
from netbox.object_actions import AddObject, BulkDelete, BulkExport, DeleteObject
|
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from netbox.views.generic.base import BaseObjectView
|
from netbox.views.generic.base import BaseObjectView
|
||||||
@@ -32,13 +31,13 @@ from utilities.forms import ConfirmationForm
|
|||||||
from utilities.htmx import htmx_partial
|
from utilities.htmx import htmx_partial
|
||||||
from utilities.json import ConfigJSONEncoder
|
from utilities.json import ConfigJSONEncoder
|
||||||
from utilities.query import count_related
|
from utilities.query import count_related
|
||||||
from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, ViewTab, register_model_view
|
from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, register_model_view
|
||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
from .choices import DataSourceStatusChoices
|
from .choices import DataSourceStatusChoices
|
||||||
from .jobs import SyncDataSourceJob
|
from .jobs import SyncDataSourceJob
|
||||||
from .models import *
|
from .models import *
|
||||||
from .plugins import get_catalog_plugins, get_local_plugins
|
from .plugins import get_catalog_plugins, get_local_plugins
|
||||||
from .tables import CatalogPluginTable, JobLogEntryTable, PluginVersionTable
|
from .tables import CatalogPluginTable, PluginVersionTable
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -120,11 +119,6 @@ class DataSourceBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.DataSourceBulkEditForm
|
form = forms.DataSourceBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(DataSource, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class DataSourceBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = DataSource.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(DataSource, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(DataSource, 'bulk_delete', path='delete', detail=False)
|
||||||
class DataSourceBulkDeleteView(generic.BulkDeleteView):
|
class DataSourceBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = DataSource.objects.annotate(
|
queryset = DataSource.objects.annotate(
|
||||||
@@ -144,13 +138,14 @@ class DataFileListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.DataFileFilterSet
|
filterset = filtersets.DataFileFilterSet
|
||||||
filterset_form = forms.DataFileFilterForm
|
filterset_form = forms.DataFileFilterForm
|
||||||
table = tables.DataFileTable
|
table = tables.DataFileTable
|
||||||
actions = (BulkDelete,)
|
actions = {
|
||||||
|
'bulk_delete': {'delete'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(DataFile)
|
@register_model_view(DataFile)
|
||||||
class DataFileView(generic.ObjectView):
|
class DataFileView(generic.ObjectView):
|
||||||
queryset = DataFile.objects.all()
|
queryset = DataFile.objects.all()
|
||||||
actions = (DeleteObject,)
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(DataFile, 'delete')
|
@register_model_view(DataFile, 'delete')
|
||||||
@@ -175,32 +170,15 @@ class JobListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.JobFilterSet
|
filterset = filtersets.JobFilterSet
|
||||||
filterset_form = forms.JobFilterForm
|
filterset_form = forms.JobFilterForm
|
||||||
table = tables.JobTable
|
table = tables.JobTable
|
||||||
actions = (BulkExport, BulkDelete)
|
actions = {
|
||||||
|
'export': {'view'},
|
||||||
|
'bulk_delete': {'delete'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Job)
|
@register_model_view(Job)
|
||||||
class JobView(generic.ObjectView):
|
class JobView(generic.ObjectView):
|
||||||
queryset = Job.objects.all()
|
queryset = Job.objects.all()
|
||||||
actions = (DeleteObject,)
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Job, 'log')
|
|
||||||
class JobLogView(generic.ObjectView):
|
|
||||||
queryset = Job.objects.all()
|
|
||||||
actions = (DeleteObject,)
|
|
||||||
template_name = 'core/job/log.html'
|
|
||||||
tab = ViewTab(
|
|
||||||
label=_('Log'),
|
|
||||||
badge=lambda obj: len(obj.log_entries),
|
|
||||||
weight=500,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
|
||||||
table = JobLogEntryTable(instance.log_entries)
|
|
||||||
table.configure(request)
|
|
||||||
return {
|
|
||||||
'table': table,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Job, 'delete')
|
@register_model_view(Job, 'delete')
|
||||||
@@ -226,7 +204,9 @@ class ObjectChangeListView(generic.ObjectListView):
|
|||||||
filterset_form = forms.ObjectChangeFilterForm
|
filterset_form = forms.ObjectChangeFilterForm
|
||||||
table = tables.ObjectChangeTable
|
table = tables.ObjectChangeTable
|
||||||
template_name = 'core/objectchange_list.html'
|
template_name = 'core/objectchange_list.html'
|
||||||
actions = (BulkExport,)
|
actions = {
|
||||||
|
'export': {'view'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ObjectChange)
|
@register_model_view(ObjectChange)
|
||||||
@@ -294,7 +274,6 @@ class ConfigRevisionListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.ConfigRevisionFilterSet
|
filterset = filtersets.ConfigRevisionFilterSet
|
||||||
filterset_form = forms.ConfigRevisionFilterForm
|
filterset_form = forms.ConfigRevisionFilterForm
|
||||||
table = tables.ConfigRevisionTable
|
table = tables.ConfigRevisionTable
|
||||||
actions = (AddObject, BulkExport)
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ConfigRevision)
|
@register_model_view(ConfigRevision)
|
||||||
|
|||||||
@@ -1335,6 +1335,13 @@ class MACAddressImportForm(NetBoxModelImportForm):
|
|||||||
|
|
||||||
class CableImportForm(NetBoxModelImportForm):
|
class CableImportForm(NetBoxModelImportForm):
|
||||||
# Termination A
|
# Termination A
|
||||||
|
side_a_site = CSVModelChoiceField(
|
||||||
|
label=_('Side A site'),
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text=_('Site of parent device A (if any)'),
|
||||||
|
)
|
||||||
side_a_device = CSVModelChoiceField(
|
side_a_device = CSVModelChoiceField(
|
||||||
label=_('Side A device'),
|
label=_('Side A device'),
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
@@ -1353,6 +1360,13 @@ class CableImportForm(NetBoxModelImportForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Termination B
|
# Termination B
|
||||||
|
side_b_site = CSVModelChoiceField(
|
||||||
|
label=_('Side B site'),
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text=_('Site of parent device B (if any)'),
|
||||||
|
)
|
||||||
side_b_device = CSVModelChoiceField(
|
side_b_device = CSVModelChoiceField(
|
||||||
label=_('Side B device'),
|
label=_('Side B device'),
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
@@ -1396,14 +1410,39 @@ class CableImportForm(NetBoxModelImportForm):
|
|||||||
required=False,
|
required=False,
|
||||||
help_text=_('Length unit')
|
help_text=_('Length unit')
|
||||||
)
|
)
|
||||||
|
color = forms.CharField(
|
||||||
|
label=_('Color'),
|
||||||
|
required=False,
|
||||||
|
max_length=16,
|
||||||
|
help_text=_('Color name (e.g. "Red") or hex code (e.g. "f44336")')
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Cable
|
model = Cable
|
||||||
fields = [
|
fields = [
|
||||||
'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
|
'side_a_site', 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_site', 'side_b_device', 'side_b_type',
|
||||||
'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags',
|
'side_b_name', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description',
|
||||||
|
'comments', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
|
super().__init__(data, *args, **kwargs)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
# Limit choices for side_a_device to the assigned side_a_site
|
||||||
|
if side_a_site := data.get('side_a_site'):
|
||||||
|
side_a_device_params = {f'site__{self.fields["side_a_site"].to_field_name}': side_a_site}
|
||||||
|
self.fields['side_a_device'].queryset = self.fields['side_a_device'].queryset.filter(
|
||||||
|
**side_a_device_params
|
||||||
|
)
|
||||||
|
|
||||||
|
# Limit choices for side_b_device to the assigned side_b_site
|
||||||
|
if side_b_site := data.get('side_b_site'):
|
||||||
|
side_b_device_params = {f'site__{self.fields["side_b_site"].to_field_name}': side_b_site}
|
||||||
|
self.fields['side_b_device'].queryset = self.fields['side_b_device'].queryset.filter(
|
||||||
|
**side_b_device_params
|
||||||
|
)
|
||||||
|
|
||||||
def _clean_side(self, side):
|
def _clean_side(self, side):
|
||||||
"""
|
"""
|
||||||
Derive a Cable's A/B termination objects.
|
Derive a Cable's A/B termination objects.
|
||||||
@@ -1440,6 +1479,24 @@ class CableImportForm(NetBoxModelImportForm):
|
|||||||
setattr(self.instance, f'{side}_terminations', [termination_object])
|
setattr(self.instance, f'{side}_terminations', [termination_object])
|
||||||
return termination_object
|
return termination_object
|
||||||
|
|
||||||
|
def _clean_color(self, color):
|
||||||
|
"""
|
||||||
|
Derive a colors hex code
|
||||||
|
|
||||||
|
:param color: color as hex or color name
|
||||||
|
"""
|
||||||
|
color_parsed = color.strip().lower()
|
||||||
|
|
||||||
|
for hex_code, label in ColorChoices.CHOICES:
|
||||||
|
if color.lower() == label.lower():
|
||||||
|
color_parsed = hex_code
|
||||||
|
|
||||||
|
if len(color_parsed) > 6:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
_(f"{color} did not match any used color name and was longer than six characters: invalid hex.")
|
||||||
|
)
|
||||||
|
return color_parsed
|
||||||
|
|
||||||
def clean_side_a_name(self):
|
def clean_side_a_name(self):
|
||||||
return self._clean_side('a')
|
return self._clean_side('a')
|
||||||
|
|
||||||
@@ -1451,11 +1508,14 @@ class CableImportForm(NetBoxModelImportForm):
|
|||||||
length_unit = self.cleaned_data.get('length_unit', None)
|
length_unit = self.cleaned_data.get('length_unit', None)
|
||||||
return length_unit if length_unit is not None else ''
|
return length_unit if length_unit is not None else ''
|
||||||
|
|
||||||
|
def clean_color(self):
|
||||||
|
color = self.cleaned_data.get('color', None)
|
||||||
|
return self._clean_color(color) if color is not None else ''
|
||||||
#
|
#
|
||||||
# Virtual chassis
|
# Virtual chassis
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class VirtualChassisImportForm(NetBoxModelImportForm):
|
class VirtualChassisImportForm(NetBoxModelImportForm):
|
||||||
master = CSVModelChoiceField(
|
master = CSVModelChoiceField(
|
||||||
label=_('Master'),
|
label=_('Master'),
|
||||||
|
|||||||
@@ -1507,7 +1507,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
tx_power = forms.IntegerField(
|
tx_power = forms.IntegerField(
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Transmit power (dBm)'),
|
label=_('Transmit power (dBm)'),
|
||||||
min_value=-40,
|
min_value=0,
|
||||||
max_value=127
|
max_value=127
|
||||||
)
|
)
|
||||||
vrf_id = DynamicModelMultipleChoiceField(
|
vrf_id = DynamicModelMultipleChoiceField(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('dcim', '0209_interface_tx_power_negative'),
|
('dcim', '0207_remove_redundant_indexes'),
|
||||||
('extras', '0129_fix_script_paths'),
|
('extras', '0129_fix_script_paths'),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('dcim', '0207_remove_redundant_indexes'),
|
|
||||||
('extras', '0129_fix_script_paths'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='platform',
|
|
||||||
name='name',
|
|
||||||
field=models.CharField(max_length=100),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='platform',
|
|
||||||
name='slug',
|
|
||||||
field=models.SlugField(max_length=100),
|
|
||||||
),
|
|
||||||
migrations.AddConstraint(
|
|
||||||
model_name='platform',
|
|
||||||
constraint=models.UniqueConstraint(
|
|
||||||
fields=('manufacturer', 'name'),
|
|
||||||
name='dcim_platform_manufacturer_name'
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddConstraint(
|
|
||||||
model_name='platform',
|
|
||||||
constraint=models.UniqueConstraint(
|
|
||||||
condition=models.Q(('manufacturer__isnull', True)),
|
|
||||||
fields=('name',),
|
|
||||||
name='dcim_platform_name',
|
|
||||||
violation_error_message='Platform name must be unique.'
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddConstraint(
|
|
||||||
model_name='platform',
|
|
||||||
constraint=models.UniqueConstraint(
|
|
||||||
fields=('manufacturer', 'slug'),
|
|
||||||
name='dcim_platform_manufacturer_slug'
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddConstraint(
|
|
||||||
model_name='platform',
|
|
||||||
constraint=models.UniqueConstraint(
|
|
||||||
condition=models.Q(('manufacturer__isnull', True)),
|
|
||||||
fields=('slug',),
|
|
||||||
name='dcim_platform_slug',
|
|
||||||
violation_error_message='Platform slug must be unique.'
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import django.core.validators
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('dcim', '0208_platform_manufacturer_uniqueness'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='interface',
|
|
||||||
name='tx_power',
|
|
||||||
field=models.SmallIntegerField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(-40),
|
|
||||||
django.core.validators.MaxValueValidator(127)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -719,13 +719,10 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
|||||||
verbose_name=('channel width (MHz)'),
|
verbose_name=('channel width (MHz)'),
|
||||||
help_text=_("Populated by selected channel (if set)")
|
help_text=_("Populated by selected channel (if set)")
|
||||||
)
|
)
|
||||||
tx_power = models.SmallIntegerField(
|
tx_power = models.PositiveSmallIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=(
|
validators=(MaxValueValidator(127),),
|
||||||
MinValueValidator(-40),
|
|
||||||
MaxValueValidator(127),
|
|
||||||
),
|
|
||||||
verbose_name=_('transmit power (dBm)')
|
verbose_name=_('transmit power (dBm)')
|
||||||
)
|
)
|
||||||
poe_mode = models.CharField(
|
poe_mode = models.CharField(
|
||||||
|
|||||||
@@ -437,15 +437,6 @@ class Platform(OrganizationalModel):
|
|||||||
null=True,
|
null=True,
|
||||||
help_text=_('Optionally limit this platform to devices of a certain manufacturer')
|
help_text=_('Optionally limit this platform to devices of a certain manufacturer')
|
||||||
)
|
)
|
||||||
# Override name & slug from OrganizationalModel to not enforce uniqueness
|
|
||||||
name = models.CharField(
|
|
||||||
verbose_name=_('name'),
|
|
||||||
max_length=100
|
|
||||||
)
|
|
||||||
slug = models.SlugField(
|
|
||||||
verbose_name=_('slug'),
|
|
||||||
max_length=100
|
|
||||||
)
|
|
||||||
config_template = models.ForeignKey(
|
config_template = models.ForeignKey(
|
||||||
to='extras.ConfigTemplate',
|
to='extras.ConfigTemplate',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
@@ -458,28 +449,6 @@ class Platform(OrganizationalModel):
|
|||||||
ordering = ('name',)
|
ordering = ('name',)
|
||||||
verbose_name = _('platform')
|
verbose_name = _('platform')
|
||||||
verbose_name_plural = _('platforms')
|
verbose_name_plural = _('platforms')
|
||||||
constraints = (
|
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=('manufacturer', 'name'),
|
|
||||||
name='%(app_label)s_%(class)s_manufacturer_name',
|
|
||||||
),
|
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=('name',),
|
|
||||||
name='%(app_label)s_%(class)s_name',
|
|
||||||
condition=Q(manufacturer__isnull=True),
|
|
||||||
violation_error_message=_("Platform name must be unique.")
|
|
||||||
),
|
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=('manufacturer', 'slug'),
|
|
||||||
name='%(app_label)s_%(class)s_manufacturer_slug',
|
|
||||||
),
|
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=('slug',),
|
|
||||||
name='%(app_label)s_%(class)s_slug',
|
|
||||||
condition=Q(manufacturer__isnull=True),
|
|
||||||
violation_error_message=_("Platform slug must be unique.")
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Device(
|
class Device(
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
from django.utils.translation import gettext as _
|
|
||||||
|
|
||||||
from netbox.object_actions import ObjectAction
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
'BulkAddComponents',
|
|
||||||
'BulkDisconnect',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BulkAddComponents(ObjectAction):
|
|
||||||
"""
|
|
||||||
Add components to the selected devices.
|
|
||||||
"""
|
|
||||||
label = _('Add Components')
|
|
||||||
multi = True
|
|
||||||
permissions_required = {'change'}
|
|
||||||
template_name = 'dcim/buttons/bulk_add_components.html'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_context(cls, context, obj):
|
|
||||||
return {
|
|
||||||
'perms': context.get('perms'),
|
|
||||||
'request': context.get('request'),
|
|
||||||
'formaction': context.get('formaction'),
|
|
||||||
'label': cls.label,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class BulkDisconnect(ObjectAction):
|
|
||||||
"""
|
|
||||||
Disconnect each of a set of objects to which a cable is connected.
|
|
||||||
"""
|
|
||||||
name = 'bulk_disconnect'
|
|
||||||
label = _('Disconnect Selected')
|
|
||||||
multi = True
|
|
||||||
permissions_required = {'change'}
|
|
||||||
template_name = 'dcim/buttons/bulk_disconnect.html'
|
|
||||||
@@ -3,6 +3,7 @@ import svgwrite
|
|||||||
from svgwrite.container import Hyperlink
|
from svgwrite.container import Hyperlink
|
||||||
from svgwrite.image import Image
|
from svgwrite.image import Image
|
||||||
from svgwrite.gradients import LinearGradient
|
from svgwrite.gradients import LinearGradient
|
||||||
|
from svgwrite.masking import ClipPath
|
||||||
from svgwrite.shapes import Rect
|
from svgwrite.shapes import Rect
|
||||||
from svgwrite.text import Text
|
from svgwrite.text import Text
|
||||||
|
|
||||||
@@ -67,6 +68,20 @@ def get_device_description(device):
|
|||||||
return description
|
return description
|
||||||
|
|
||||||
|
|
||||||
|
def truncate_text(text, width, font_size=15):
|
||||||
|
"""
|
||||||
|
Truncate text to fit within the width of a rectangle.
|
||||||
|
|
||||||
|
:param text: The text to truncate
|
||||||
|
:param width: Width of rectangle
|
||||||
|
:param font_size: Font size (default is 15, ~0.875rem)
|
||||||
|
"""
|
||||||
|
char_width = font_size * 0.6 # 0.6 is an approximation of the average character width in pixels
|
||||||
|
max_char = int(width / char_width)
|
||||||
|
|
||||||
|
return text if len(text) <= max_char else text[:max_char] + '...'
|
||||||
|
|
||||||
|
|
||||||
class RackElevationSVG:
|
class RackElevationSVG:
|
||||||
"""
|
"""
|
||||||
Use this class to render a rack elevation as an SVG image.
|
Use this class to render a rack elevation as an SVG image.
|
||||||
@@ -177,12 +192,26 @@ class RackElevationSVG:
|
|||||||
link = Hyperlink(href=f'{self.base_url}{device.get_absolute_url()}', target="_parent")
|
link = Hyperlink(href=f'{self.base_url}{device.get_absolute_url()}', target="_parent")
|
||||||
link.set_desc(description)
|
link.set_desc(description)
|
||||||
|
|
||||||
|
# Create clipPath element
|
||||||
|
# This is necessary as fallback because the truncate_text method is an approximation
|
||||||
|
clip_id = f"clip-{device.id}"
|
||||||
|
clip_path = ClipPath(id=clip_id)
|
||||||
|
clip_path.add(Rect(coords, size))
|
||||||
|
|
||||||
|
self.drawing.defs.add(clip_path)
|
||||||
|
|
||||||
|
# Name to display
|
||||||
|
display_name = truncate_text(name, size[0])
|
||||||
|
|
||||||
# Add rect element to hyperlink
|
# Add rect element to hyperlink
|
||||||
if color:
|
if color:
|
||||||
link.add(Rect(coords, size, style=f'fill: #{color}', class_=f'slot{css_extra}'))
|
link.add(Rect(coords, size, style=f'fill: #{color}', class_=f'slot{css_extra}'))
|
||||||
else:
|
else:
|
||||||
link.add(Rect(coords, size, class_=f'slot blocked{css_extra}'))
|
link.add(Rect(coords, size, class_=f'slot blocked{css_extra}'))
|
||||||
link.add(Text(name, insert=text_coords, fill=text_color, class_=f'label{css_extra}'))
|
link.add(
|
||||||
|
Text(display_name, insert=text_coords, fill=text_color, clip_path=f"url(#{clip_id})",
|
||||||
|
class_=f'label{css_extra}')
|
||||||
|
)
|
||||||
|
|
||||||
# Embed device type image if provided
|
# Embed device type image if provided
|
||||||
if self.include_images and image:
|
if self.include_images and image:
|
||||||
|
|||||||
@@ -3266,17 +3266,27 @@ class CableTestCase(
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
sites = (
|
||||||
|
Site(name='Site 1', slug='site-1'),
|
||||||
|
Site(name='Site 2', slug='site-2'),
|
||||||
|
)
|
||||||
|
Site.objects.bulk_create(sites)
|
||||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
devicetype = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
|
devicetype = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
|
||||||
role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||||
vc = VirtualChassis.objects.create(name='Virtual Chassis')
|
vc = VirtualChassis.objects.create(name='Virtual Chassis')
|
||||||
|
|
||||||
|
# NOTE: By design, NetBox now allows for the creation of devices with the same name if they belong to
|
||||||
|
# different sites.
|
||||||
|
# The CSV test below demonstrates that devices with identical names on different sites can be created
|
||||||
|
# and referenced successfully.
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', site=site, device_type=devicetype, role=role),
|
# Create 'Device 1' assigned to 'Site 1'
|
||||||
Device(name='Device 2', site=site, device_type=devicetype, role=role),
|
Device(name='Device 1', site=sites[0], device_type=devicetype, role=role),
|
||||||
Device(name='Device 3', site=site, device_type=devicetype, role=role),
|
Device(name='Device 2', site=sites[0], device_type=devicetype, role=role),
|
||||||
Device(name='Device 4', site=site, device_type=devicetype, role=role),
|
Device(name='Device 3', site=sites[0], device_type=devicetype, role=role),
|
||||||
|
# Create 'Device 1' assigned to 'Site 2' (allowed since the site is different)
|
||||||
|
Device(name='Device 1', site=sites[1], device_type=devicetype, role=role),
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
@@ -3327,13 +3337,15 @@ class CableTestCase(
|
|||||||
'tags': [t.pk for t in tags],
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Ensure that CSV bulk import supports assigning terminations from parent devices that share
|
||||||
|
# the same device name, provided those devices belong to different sites.
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"side_a_device,side_a_type,side_a_name,side_b_device,side_b_type,side_b_name",
|
"side_a_site,side_a_device,side_a_type,side_a_name,side_b_site,side_b_device,side_b_type,side_b_name",
|
||||||
"Device 3,dcim.interface,Interface 1,Device 4,dcim.interface,Interface 1",
|
"Site 1,Device 3,dcim.interface,Interface 1,Site 2,Device 1,dcim.interface,Interface 1",
|
||||||
"Device 3,dcim.interface,Interface 2,Device 4,dcim.interface,Interface 2",
|
"Site 1,Device 3,dcim.interface,Interface 2,Site 2,Device 1,dcim.interface,Interface 2",
|
||||||
"Device 3,dcim.interface,Interface 3,Device 4,dcim.interface,Interface 3",
|
"Site 1,Device 3,dcim.interface,Interface 3,Site 2,Device 1,dcim.interface,Interface 3",
|
||||||
"Device 1,dcim.interface,Device 2 Interface,Device 4,dcim.interface,Interface 4",
|
"Site 1,Device 1,dcim.interface,Device 2 Interface,Site 2,Device 1,dcim.interface,Interface 4",
|
||||||
"Device 1,dcim.interface,Device 3 Interface,Device 4,dcim.interface,Interface 5",
|
"Site 1,Device 1,dcim.interface,Device 3 Interface,Site 2,Device 1,dcim.interface,Interface 5",
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.csv_update_data = (
|
cls.csv_update_data = (
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from circuits.models import Circuit, CircuitTermination
|
|||||||
from extras.views import ObjectConfigContextView, ObjectRenderConfigView
|
from extras.views import ObjectConfigContextView, ObjectRenderConfigView
|
||||||
from ipam.models import ASN, IPAddress, Prefix, VLANGroup, VLAN
|
from ipam.models import ASN, IPAddress, Prefix, VLANGroup, VLAN
|
||||||
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
|
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
|
||||||
from netbox.object_actions import *
|
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||||
@@ -34,7 +34,6 @@ from wireless.models import WirelessLAN
|
|||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
from .choices import DeviceFaceChoices, InterfaceModeChoices
|
from .choices import DeviceFaceChoices, InterfaceModeChoices
|
||||||
from .models import *
|
from .models import *
|
||||||
from .object_actions import BulkAddComponents, BulkDisconnect
|
|
||||||
|
|
||||||
CABLE_TERMINATION_TYPES = {
|
CABLE_TERMINATION_TYPES = {
|
||||||
'dcim.consoleport': ConsolePort,
|
'dcim.consoleport': ConsolePort,
|
||||||
@@ -50,6 +49,11 @@ CABLE_TERMINATION_TYPES = {
|
|||||||
|
|
||||||
|
|
||||||
class DeviceComponentsView(generic.ObjectChildrenView):
|
class DeviceComponentsView(generic.ObjectChildrenView):
|
||||||
|
actions = {
|
||||||
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
|
'bulk_rename': {'change'},
|
||||||
|
'bulk_disconnect': {'change'},
|
||||||
|
}
|
||||||
queryset = Device.objects.all()
|
queryset = Device.objects.all()
|
||||||
|
|
||||||
def get_children(self, request, parent):
|
def get_children(self, request, parent):
|
||||||
@@ -57,8 +61,12 @@ class DeviceComponentsView(generic.ObjectChildrenView):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceTypeComponentsView(generic.ObjectChildrenView):
|
class DeviceTypeComponentsView(generic.ObjectChildrenView):
|
||||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
|
actions = {
|
||||||
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
|
'bulk_rename': {'change'},
|
||||||
|
}
|
||||||
queryset = DeviceType.objects.all()
|
queryset = DeviceType.objects.all()
|
||||||
|
template_name = 'dcim/devicetype/component_templates.html'
|
||||||
viewname = None # Used for return_url resolution
|
viewname = None # Used for return_url resolution
|
||||||
|
|
||||||
def get_children(self, request, parent):
|
def get_children(self, request, parent):
|
||||||
@@ -70,9 +78,9 @@ class DeviceTypeComponentsView(generic.ObjectChildrenView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ModuleTypeComponentsView(generic.ObjectChildrenView):
|
class ModuleTypeComponentsView(DeviceComponentsView):
|
||||||
queryset = ModuleType.objects.all()
|
queryset = ModuleType.objects.all()
|
||||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
|
template_name = 'dcim/moduletype/component_templates.html'
|
||||||
viewname = None # Used for return_url resolution
|
viewname = None # Used for return_url resolution
|
||||||
|
|
||||||
def get_children(self, request, parent):
|
def get_children(self, request, parent):
|
||||||
@@ -292,11 +300,6 @@ class RegionBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.RegionBulkEditForm
|
form = forms.RegionBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Region, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class RegionBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = Region.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Region, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(Region, 'bulk_delete', path='delete', detail=False)
|
||||||
class RegionBulkDeleteView(generic.BulkDeleteView):
|
class RegionBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Region.objects.add_related_count(
|
queryset = Region.objects.add_related_count(
|
||||||
@@ -423,11 +426,6 @@ class SiteGroupBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.SiteGroupBulkEditForm
|
form = forms.SiteGroupBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(SiteGroup, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class SiteGroupBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = SiteGroup.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(SiteGroup, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(SiteGroup, 'bulk_delete', path='delete', detail=False)
|
||||||
class SiteGroupBulkDeleteView(generic.BulkDeleteView):
|
class SiteGroupBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = SiteGroup.objects.add_related_count(
|
queryset = SiteGroup.objects.add_related_count(
|
||||||
@@ -513,11 +511,6 @@ class SiteBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.SiteBulkEditForm
|
form = forms.SiteBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Site, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class SiteBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = Site.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Site, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(Site, 'bulk_delete', path='delete', detail=False)
|
||||||
class SiteBulkDeleteView(generic.BulkDeleteView):
|
class SiteBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Site.objects.all()
|
queryset = Site.objects.all()
|
||||||
@@ -622,11 +615,6 @@ class LocationBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.LocationBulkEditForm
|
form = forms.LocationBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Location, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class LocationBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = Location.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Location, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(Location, 'bulk_delete', path='delete', detail=False)
|
||||||
class LocationBulkDeleteView(generic.BulkDeleteView):
|
class LocationBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Location.objects.add_related_count(
|
queryset = Location.objects.add_related_count(
|
||||||
@@ -692,11 +680,6 @@ class RackRoleBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.RackRoleBulkEditForm
|
form = forms.RackRoleBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(RackRole, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class RackRoleBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = RackRole.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(RackRole, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(RackRole, 'bulk_delete', path='delete', detail=False)
|
||||||
class RackRoleBulkDeleteView(generic.BulkDeleteView):
|
class RackRoleBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = RackRole.objects.annotate(
|
queryset = RackRole.objects.annotate(
|
||||||
@@ -756,12 +739,6 @@ class RackTypeBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.RackTypeBulkEditForm
|
form = forms.RackTypeBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(RackType, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class RackTypeBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = RackType.objects.all()
|
|
||||||
field_name = 'model'
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(RackType, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(RackType, 'bulk_delete', path='delete', detail=False)
|
||||||
class RackTypeBulkDeleteView(generic.BulkDeleteView):
|
class RackTypeBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = RackType.objects.all()
|
queryset = RackType.objects.all()
|
||||||
@@ -941,11 +918,6 @@ class RackBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.RackBulkEditForm
|
form = forms.RackBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Rack, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class RackBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = Rack.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Rack, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(Rack, 'bulk_delete', path='delete', detail=False)
|
||||||
class RackBulkDeleteView(generic.BulkDeleteView):
|
class RackBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Rack.objects.all()
|
queryset = Rack.objects.all()
|
||||||
@@ -963,7 +935,6 @@ class RackReservationListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.RackReservationFilterSet
|
filterset = filtersets.RackReservationFilterSet
|
||||||
filterset_form = forms.RackReservationFilterForm
|
filterset_form = forms.RackReservationFilterForm
|
||||||
table = tables.RackReservationTable
|
table = tables.RackReservationTable
|
||||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(RackReservation)
|
@register_model_view(RackReservation)
|
||||||
@@ -1080,11 +1051,6 @@ class ManufacturerBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.ManufacturerBulkEditForm
|
form = forms.ManufacturerBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Manufacturer, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class ManufacturerBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = Manufacturer.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Manufacturer, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(Manufacturer, 'bulk_delete', path='delete', detail=False)
|
||||||
class ManufacturerBulkDeleteView(generic.BulkDeleteView):
|
class ManufacturerBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Manufacturer.objects.annotate(
|
queryset = Manufacturer.objects.annotate(
|
||||||
@@ -1332,12 +1298,6 @@ class DeviceTypeBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.DeviceTypeBulkEditForm
|
form = forms.DeviceTypeBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(DeviceType, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class DeviceTypeBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = DeviceType.objects.all()
|
|
||||||
field_name = 'model'
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(DeviceType, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(DeviceType, 'bulk_delete', path='delete', detail=False)
|
||||||
class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
|
class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = DeviceType.objects.annotate(
|
queryset = DeviceType.objects.annotate(
|
||||||
@@ -1394,11 +1354,6 @@ class ModuleTypeProfileBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.ModuleTypeProfileBulkEditForm
|
form = forms.ModuleTypeProfileBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ModuleTypeProfile, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class ModuleTypeProfileBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = ModuleTypeProfile.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ModuleTypeProfile, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(ModuleTypeProfile, 'bulk_delete', path='delete', detail=False)
|
||||||
class ModuleTypeProfileBulkDeleteView(generic.BulkDeleteView):
|
class ModuleTypeProfileBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = ModuleTypeProfile.objects.annotate(
|
queryset = ModuleTypeProfile.objects.annotate(
|
||||||
@@ -1609,11 +1564,6 @@ class ModuleTypeBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.ModuleTypeBulkEditForm
|
form = forms.ModuleTypeBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ModuleType, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class ModuleTypeBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = ModuleType.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ModuleType, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(ModuleType, 'bulk_delete', path='delete', detail=False)
|
||||||
class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
|
class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = ModuleType.objects.annotate(
|
queryset = ModuleType.objects.annotate(
|
||||||
@@ -2088,11 +2038,6 @@ class DeviceRoleBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.DeviceRoleBulkEditForm
|
form = forms.DeviceRoleBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(DeviceRole, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class DeviceRoleBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = DeviceRole.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(DeviceRole, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(DeviceRole, 'bulk_delete', path='delete', detail=False)
|
||||||
class DeviceRoleBulkDeleteView(generic.BulkDeleteView):
|
class DeviceRoleBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = DeviceRole.objects.annotate(
|
queryset = DeviceRole.objects.annotate(
|
||||||
@@ -2154,11 +2099,6 @@ class PlatformBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.PlatformBulkEditForm
|
form = forms.PlatformBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Platform, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class PlatformBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = Platform.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Platform, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(Platform, 'bulk_delete', path='delete', detail=False)
|
||||||
class PlatformBulkDeleteView(generic.BulkDeleteView):
|
class PlatformBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Platform.objects.all()
|
queryset = Platform.objects.all()
|
||||||
@@ -2176,7 +2116,7 @@ class DeviceListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.DeviceFilterSet
|
filterset = filtersets.DeviceFilterSet
|
||||||
filterset_form = forms.DeviceFilterForm
|
filterset_form = forms.DeviceFilterForm
|
||||||
table = tables.DeviceTable
|
table = tables.DeviceTable
|
||||||
actions = (AddObject, BulkImport, BulkExport, BulkAddComponents, BulkEdit, BulkRename, BulkDelete)
|
template_name = 'dcim/device_list.html'
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Device)
|
@register_model_view(Device)
|
||||||
@@ -2217,7 +2157,7 @@ class DeviceConsolePortsView(DeviceComponentsView):
|
|||||||
table = tables.DeviceConsolePortTable
|
table = tables.DeviceConsolePortTable
|
||||||
filterset = filtersets.ConsolePortFilterSet
|
filterset = filtersets.ConsolePortFilterSet
|
||||||
filterset_form = forms.ConsolePortFilterForm
|
filterset_form = forms.ConsolePortFilterForm
|
||||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
|
template_name = 'dcim/device/consoleports.html',
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Console Ports'),
|
label=_('Console Ports'),
|
||||||
badge=lambda obj: obj.console_port_count,
|
badge=lambda obj: obj.console_port_count,
|
||||||
@@ -2233,7 +2173,7 @@ class DeviceConsoleServerPortsView(DeviceComponentsView):
|
|||||||
table = tables.DeviceConsoleServerPortTable
|
table = tables.DeviceConsoleServerPortTable
|
||||||
filterset = filtersets.ConsoleServerPortFilterSet
|
filterset = filtersets.ConsoleServerPortFilterSet
|
||||||
filterset_form = forms.ConsoleServerPortFilterForm
|
filterset_form = forms.ConsoleServerPortFilterForm
|
||||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
|
template_name = 'dcim/device/consoleserverports.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Console Server Ports'),
|
label=_('Console Server Ports'),
|
||||||
badge=lambda obj: obj.console_server_port_count,
|
badge=lambda obj: obj.console_server_port_count,
|
||||||
@@ -2249,7 +2189,7 @@ class DevicePowerPortsView(DeviceComponentsView):
|
|||||||
table = tables.DevicePowerPortTable
|
table = tables.DevicePowerPortTable
|
||||||
filterset = filtersets.PowerPortFilterSet
|
filterset = filtersets.PowerPortFilterSet
|
||||||
filterset_form = forms.PowerPortFilterForm
|
filterset_form = forms.PowerPortFilterForm
|
||||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
|
template_name = 'dcim/device/powerports.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Power Ports'),
|
label=_('Power Ports'),
|
||||||
badge=lambda obj: obj.power_port_count,
|
badge=lambda obj: obj.power_port_count,
|
||||||
@@ -2265,7 +2205,7 @@ class DevicePowerOutletsView(DeviceComponentsView):
|
|||||||
table = tables.DevicePowerOutletTable
|
table = tables.DevicePowerOutletTable
|
||||||
filterset = filtersets.PowerOutletFilterSet
|
filterset = filtersets.PowerOutletFilterSet
|
||||||
filterset_form = forms.PowerOutletFilterForm
|
filterset_form = forms.PowerOutletFilterForm
|
||||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
|
template_name = 'dcim/device/poweroutlets.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Power Outlets'),
|
label=_('Power Outlets'),
|
||||||
badge=lambda obj: obj.power_outlet_count,
|
badge=lambda obj: obj.power_outlet_count,
|
||||||
@@ -2281,7 +2221,6 @@ class DeviceInterfacesView(DeviceComponentsView):
|
|||||||
table = tables.DeviceInterfaceTable
|
table = tables.DeviceInterfaceTable
|
||||||
filterset = filtersets.InterfaceFilterSet
|
filterset = filtersets.InterfaceFilterSet
|
||||||
filterset_form = forms.InterfaceFilterForm
|
filterset_form = forms.InterfaceFilterForm
|
||||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
|
|
||||||
template_name = 'dcim/device/interfaces.html'
|
template_name = 'dcim/device/interfaces.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Interfaces'),
|
label=_('Interfaces'),
|
||||||
@@ -2304,7 +2243,7 @@ class DeviceFrontPortsView(DeviceComponentsView):
|
|||||||
table = tables.DeviceFrontPortTable
|
table = tables.DeviceFrontPortTable
|
||||||
filterset = filtersets.FrontPortFilterSet
|
filterset = filtersets.FrontPortFilterSet
|
||||||
filterset_form = forms.FrontPortFilterForm
|
filterset_form = forms.FrontPortFilterForm
|
||||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
|
template_name = 'dcim/device/frontports.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Front Ports'),
|
label=_('Front Ports'),
|
||||||
badge=lambda obj: obj.front_port_count,
|
badge=lambda obj: obj.front_port_count,
|
||||||
@@ -2320,7 +2259,7 @@ class DeviceRearPortsView(DeviceComponentsView):
|
|||||||
table = tables.DeviceRearPortTable
|
table = tables.DeviceRearPortTable
|
||||||
filterset = filtersets.RearPortFilterSet
|
filterset = filtersets.RearPortFilterSet
|
||||||
filterset_form = forms.RearPortFilterForm
|
filterset_form = forms.RearPortFilterForm
|
||||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
|
template_name = 'dcim/device/rearports.html'
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Rear Ports'),
|
label=_('Rear Ports'),
|
||||||
badge=lambda obj: obj.rear_port_count,
|
badge=lambda obj: obj.rear_port_count,
|
||||||
@@ -2336,7 +2275,11 @@ class DeviceModuleBaysView(DeviceComponentsView):
|
|||||||
table = tables.DeviceModuleBayTable
|
table = tables.DeviceModuleBayTable
|
||||||
filterset = filtersets.ModuleBayFilterSet
|
filterset = filtersets.ModuleBayFilterSet
|
||||||
filterset_form = forms.ModuleBayFilterForm
|
filterset_form = forms.ModuleBayFilterForm
|
||||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
|
template_name = 'dcim/device/modulebays.html'
|
||||||
|
actions = {
|
||||||
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
|
'bulk_rename': {'change'},
|
||||||
|
}
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Module Bays'),
|
label=_('Module Bays'),
|
||||||
badge=lambda obj: obj.module_bay_count,
|
badge=lambda obj: obj.module_bay_count,
|
||||||
@@ -2352,7 +2295,11 @@ class DeviceDeviceBaysView(DeviceComponentsView):
|
|||||||
table = tables.DeviceDeviceBayTable
|
table = tables.DeviceDeviceBayTable
|
||||||
filterset = filtersets.DeviceBayFilterSet
|
filterset = filtersets.DeviceBayFilterSet
|
||||||
filterset_form = forms.DeviceBayFilterForm
|
filterset_form = forms.DeviceBayFilterForm
|
||||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
|
template_name = 'dcim/device/devicebays.html'
|
||||||
|
actions = {
|
||||||
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
|
'bulk_rename': {'change'},
|
||||||
|
}
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Device Bays'),
|
label=_('Device Bays'),
|
||||||
badge=lambda obj: obj.device_bay_count,
|
badge=lambda obj: obj.device_bay_count,
|
||||||
@@ -2368,7 +2315,11 @@ class DeviceInventoryView(DeviceComponentsView):
|
|||||||
table = tables.DeviceInventoryItemTable
|
table = tables.DeviceInventoryItemTable
|
||||||
filterset = filtersets.InventoryItemFilterSet
|
filterset = filtersets.InventoryItemFilterSet
|
||||||
filterset_form = forms.InventoryItemFilterForm
|
filterset_form = forms.InventoryItemFilterForm
|
||||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
|
template_name = 'dcim/device/inventory.html'
|
||||||
|
actions = {
|
||||||
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
|
'bulk_rename': {'change'},
|
||||||
|
}
|
||||||
tab = ViewTab(
|
tab = ViewTab(
|
||||||
label=_('Inventory Items'),
|
label=_('Inventory Items'),
|
||||||
badge=lambda obj: obj.inventory_item_count,
|
badge=lambda obj: obj.inventory_item_count,
|
||||||
@@ -2442,16 +2393,16 @@ class DeviceBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.DeviceBulkEditForm
|
form = forms.DeviceBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Device, 'bulk_rename', path='rename', detail=False)
|
@register_model_view(Device, 'bulk_delete', path='delete', detail=False)
|
||||||
class DeviceBulkRenameView(generic.BulkRenameView):
|
class DeviceBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Device.objects.all()
|
queryset = Device.objects.prefetch_related('device_type__manufacturer')
|
||||||
filterset = filtersets.DeviceFilterSet
|
filterset = filtersets.DeviceFilterSet
|
||||||
table = tables.DeviceTable
|
table = tables.DeviceTable
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Device, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(Device, 'bulk_rename', path='rename', detail=False)
|
||||||
class DeviceBulkDeleteView(generic.BulkDeleteView):
|
class DeviceBulkRenameView(generic.BulkRenameView):
|
||||||
queryset = Device.objects.prefetch_related('device_type__manufacturer')
|
queryset = Device.objects.all()
|
||||||
filterset = filtersets.DeviceFilterSet
|
filterset = filtersets.DeviceFilterSet
|
||||||
table = tables.DeviceTable
|
table = tables.DeviceTable
|
||||||
|
|
||||||
@@ -2466,7 +2417,6 @@ class ModuleListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.ModuleFilterSet
|
filterset = filtersets.ModuleFilterSet
|
||||||
filterset_form = forms.ModuleFilterForm
|
filterset_form = forms.ModuleFilterForm
|
||||||
table = tables.ModuleTable
|
table = tables.ModuleTable
|
||||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Module)
|
@register_model_view(Module)
|
||||||
@@ -2522,6 +2472,11 @@ class ConsolePortListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.ConsolePortFilterSet
|
filterset = filtersets.ConsolePortFilterSet
|
||||||
filterset_form = forms.ConsolePortFilterForm
|
filterset_form = forms.ConsolePortFilterForm
|
||||||
table = tables.ConsolePortTable
|
table = tables.ConsolePortTable
|
||||||
|
template_name = 'dcim/component_list.html'
|
||||||
|
actions = {
|
||||||
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
|
'bulk_rename': {'change'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ConsolePort)
|
@register_model_view(ConsolePort)
|
||||||
@@ -2592,6 +2547,11 @@ class ConsoleServerPortListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.ConsoleServerPortFilterSet
|
filterset = filtersets.ConsoleServerPortFilterSet
|
||||||
filterset_form = forms.ConsoleServerPortFilterForm
|
filterset_form = forms.ConsoleServerPortFilterForm
|
||||||
table = tables.ConsoleServerPortTable
|
table = tables.ConsoleServerPortTable
|
||||||
|
template_name = 'dcim/component_list.html'
|
||||||
|
actions = {
|
||||||
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
|
'bulk_rename': {'change'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ConsoleServerPort)
|
@register_model_view(ConsoleServerPort)
|
||||||
@@ -2662,6 +2622,11 @@ class PowerPortListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.PowerPortFilterSet
|
filterset = filtersets.PowerPortFilterSet
|
||||||
filterset_form = forms.PowerPortFilterForm
|
filterset_form = forms.PowerPortFilterForm
|
||||||
table = tables.PowerPortTable
|
table = tables.PowerPortTable
|
||||||
|
template_name = 'dcim/component_list.html'
|
||||||
|
actions = {
|
||||||
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
|
'bulk_rename': {'change'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(PowerPort)
|
@register_model_view(PowerPort)
|
||||||
@@ -2732,6 +2697,11 @@ class PowerOutletListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.PowerOutletFilterSet
|
filterset = filtersets.PowerOutletFilterSet
|
||||||
filterset_form = forms.PowerOutletFilterForm
|
filterset_form = forms.PowerOutletFilterForm
|
||||||
table = tables.PowerOutletTable
|
table = tables.PowerOutletTable
|
||||||
|
template_name = 'dcim/component_list.html'
|
||||||
|
actions = {
|
||||||
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
|
'bulk_rename': {'change'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(PowerOutlet)
|
@register_model_view(PowerOutlet)
|
||||||
@@ -2802,6 +2772,11 @@ class InterfaceListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.InterfaceFilterSet
|
filterset = filtersets.InterfaceFilterSet
|
||||||
filterset_form = forms.InterfaceFilterForm
|
filterset_form = forms.InterfaceFilterForm
|
||||||
table = tables.InterfaceTable
|
table = tables.InterfaceTable
|
||||||
|
template_name = 'dcim/component_list.html'
|
||||||
|
actions = {
|
||||||
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
|
'bulk_rename': {'change'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Interface)
|
@register_model_view(Interface)
|
||||||
@@ -2945,6 +2920,11 @@ class FrontPortListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.FrontPortFilterSet
|
filterset = filtersets.FrontPortFilterSet
|
||||||
filterset_form = forms.FrontPortFilterForm
|
filterset_form = forms.FrontPortFilterForm
|
||||||
table = tables.FrontPortTable
|
table = tables.FrontPortTable
|
||||||
|
template_name = 'dcim/component_list.html'
|
||||||
|
actions = {
|
||||||
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
|
'bulk_rename': {'change'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(FrontPort)
|
@register_model_view(FrontPort)
|
||||||
@@ -3015,6 +2995,11 @@ class RearPortListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.RearPortFilterSet
|
filterset = filtersets.RearPortFilterSet
|
||||||
filterset_form = forms.RearPortFilterForm
|
filterset_form = forms.RearPortFilterForm
|
||||||
table = tables.RearPortTable
|
table = tables.RearPortTable
|
||||||
|
template_name = 'dcim/component_list.html'
|
||||||
|
actions = {
|
||||||
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
|
'bulk_rename': {'change'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(RearPort)
|
@register_model_view(RearPort)
|
||||||
@@ -3085,6 +3070,11 @@ class ModuleBayListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.ModuleBayFilterSet
|
filterset = filtersets.ModuleBayFilterSet
|
||||||
filterset_form = forms.ModuleBayFilterForm
|
filterset_form = forms.ModuleBayFilterForm
|
||||||
table = tables.ModuleBayTable
|
table = tables.ModuleBayTable
|
||||||
|
template_name = 'dcim/component_list.html'
|
||||||
|
actions = {
|
||||||
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
|
'bulk_rename': {'change'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ModuleBay)
|
@register_model_view(ModuleBay)
|
||||||
@@ -3146,6 +3136,11 @@ class DeviceBayListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.DeviceBayFilterSet
|
filterset = filtersets.DeviceBayFilterSet
|
||||||
filterset_form = forms.DeviceBayFilterForm
|
filterset_form = forms.DeviceBayFilterForm
|
||||||
table = tables.DeviceBayTable
|
table = tables.DeviceBayTable
|
||||||
|
template_name = 'dcim/component_list.html'
|
||||||
|
actions = {
|
||||||
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
|
'bulk_rename': {'change'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(DeviceBay)
|
@register_model_view(DeviceBay)
|
||||||
@@ -3288,6 +3283,11 @@ class InventoryItemListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.InventoryItemFilterSet
|
filterset = filtersets.InventoryItemFilterSet
|
||||||
filterset_form = forms.InventoryItemFilterForm
|
filterset_form = forms.InventoryItemFilterForm
|
||||||
table = tables.InventoryItemTable
|
table = tables.InventoryItemTable
|
||||||
|
template_name = 'dcim/component_list.html'
|
||||||
|
actions = {
|
||||||
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
|
'bulk_rename': {'change'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(InventoryItem)
|
@register_model_view(InventoryItem)
|
||||||
@@ -3410,11 +3410,6 @@ class InventoryItemRoleBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.InventoryItemRoleBulkEditForm
|
form = forms.InventoryItemRoleBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(InventoryItemRole, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class InventoryItemRoleBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = InventoryItemRole.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(InventoryItemRole, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(InventoryItemRole, 'bulk_delete', path='delete', detail=False)
|
||||||
class InventoryItemRoleBulkDeleteView(generic.BulkDeleteView):
|
class InventoryItemRoleBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = InventoryItemRole.objects.annotate(
|
queryset = InventoryItemRole.objects.annotate(
|
||||||
@@ -3612,12 +3607,6 @@ class CableBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.CableBulkEditForm
|
form = forms.CableBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Cable, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class CableBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = Cable.objects.all()
|
|
||||||
field_name = 'label'
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Cable, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(Cable, 'bulk_delete', path='delete', detail=False)
|
||||||
class CableBulkDeleteView(generic.BulkDeleteView):
|
class CableBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Cable.objects.prefetch_related(
|
queryset = Cable.objects.prefetch_related(
|
||||||
@@ -3638,7 +3627,9 @@ class ConsoleConnectionsListView(generic.ObjectListView):
|
|||||||
filterset_form = forms.ConsoleConnectionFilterForm
|
filterset_form = forms.ConsoleConnectionFilterForm
|
||||||
table = tables.ConsoleConnectionTable
|
table = tables.ConsoleConnectionTable
|
||||||
template_name = 'dcim/connections_list.html'
|
template_name = 'dcim/connections_list.html'
|
||||||
actions = (BulkExport,)
|
actions = {
|
||||||
|
'export': {'view'},
|
||||||
|
}
|
||||||
|
|
||||||
def get_extra_context(self, request):
|
def get_extra_context(self, request):
|
||||||
return {
|
return {
|
||||||
@@ -3652,7 +3643,9 @@ class PowerConnectionsListView(generic.ObjectListView):
|
|||||||
filterset_form = forms.PowerConnectionFilterForm
|
filterset_form = forms.PowerConnectionFilterForm
|
||||||
table = tables.PowerConnectionTable
|
table = tables.PowerConnectionTable
|
||||||
template_name = 'dcim/connections_list.html'
|
template_name = 'dcim/connections_list.html'
|
||||||
actions = (BulkExport,)
|
actions = {
|
||||||
|
'export': {'view'},
|
||||||
|
}
|
||||||
|
|
||||||
def get_extra_context(self, request):
|
def get_extra_context(self, request):
|
||||||
return {
|
return {
|
||||||
@@ -3666,7 +3659,9 @@ class InterfaceConnectionsListView(generic.ObjectListView):
|
|||||||
filterset_form = forms.InterfaceConnectionFilterForm
|
filterset_form = forms.InterfaceConnectionFilterForm
|
||||||
table = tables.InterfaceConnectionTable
|
table = tables.InterfaceConnectionTable
|
||||||
template_name = 'dcim/connections_list.html'
|
template_name = 'dcim/connections_list.html'
|
||||||
actions = (BulkExport,)
|
actions = {
|
||||||
|
'export': {'view'},
|
||||||
|
}
|
||||||
|
|
||||||
def get_extra_context(self, request):
|
def get_extra_context(self, request):
|
||||||
return {
|
return {
|
||||||
@@ -3910,11 +3905,6 @@ class VirtualChassisBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.VirtualChassisBulkEditForm
|
form = forms.VirtualChassisBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VirtualChassis, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class VirtualChassisBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = VirtualChassis.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VirtualChassis, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(VirtualChassis, 'bulk_delete', path='delete', detail=False)
|
||||||
class VirtualChassisBulkDeleteView(generic.BulkDeleteView):
|
class VirtualChassisBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = VirtualChassis.objects.all()
|
queryset = VirtualChassis.objects.all()
|
||||||
@@ -3972,11 +3962,6 @@ class PowerPanelBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.PowerPanelBulkEditForm
|
form = forms.PowerPanelBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(PowerPanel, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class PowerPanelBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = PowerPanel.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(PowerPanel, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(PowerPanel, 'bulk_delete', path='delete', detail=False)
|
||||||
class PowerPanelBulkDeleteView(generic.BulkDeleteView):
|
class PowerPanelBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = PowerPanel.objects.annotate(
|
queryset = PowerPanel.objects.annotate(
|
||||||
@@ -4029,11 +4014,6 @@ class PowerFeedBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.PowerFeedBulkEditForm
|
form = forms.PowerFeedBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(PowerFeed, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class PowerFeedBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = PowerFeed.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(PowerFeed, 'bulk_disconnect', path='disconnect', detail=False)
|
@register_model_view(PowerFeed, 'bulk_disconnect', path='disconnect', detail=False)
|
||||||
class PowerFeedBulkDisconnectView(BulkDisconnectView):
|
class PowerFeedBulkDisconnectView(BulkDisconnectView):
|
||||||
queryset = PowerFeed.objects.all()
|
queryset = PowerFeed.objects.all()
|
||||||
@@ -4062,7 +4042,6 @@ class VirtualDeviceContextListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.VirtualDeviceContextFilterSet
|
filterset = filtersets.VirtualDeviceContextFilterSet
|
||||||
filterset_form = forms.VirtualDeviceContextFilterForm
|
filterset_form = forms.VirtualDeviceContextFilterForm
|
||||||
table = tables.VirtualDeviceContextTable
|
table = tables.VirtualDeviceContextTable
|
||||||
actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VirtualDeviceContext)
|
@register_model_view(VirtualDeviceContext)
|
||||||
@@ -4107,11 +4086,6 @@ class VirtualDeviceContextBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.VirtualDeviceContextBulkEditForm
|
form = forms.VirtualDeviceContextBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VirtualDeviceContext, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class VirtualDeviceContextBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = VirtualDeviceContext.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VirtualDeviceContext, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(VirtualDeviceContext, 'bulk_delete', path='delete', detail=False)
|
||||||
class VirtualDeviceContextBulkDeleteView(generic.BulkDeleteView):
|
class VirtualDeviceContextBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = VirtualDeviceContext.objects.all()
|
queryset = VirtualDeviceContext.objects.all()
|
||||||
@@ -4129,7 +4103,6 @@ class MACAddressListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.MACAddressFilterSet
|
filterset = filtersets.MACAddressFilterSet
|
||||||
filterset_form = forms.MACAddressFilterForm
|
filterset_form = forms.MACAddressFilterForm
|
||||||
table = tables.MACAddressTable
|
table = tables.MACAddressTable
|
||||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(MACAddress)
|
@register_model_view(MACAddress)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from .serializers_.objecttypes import *
|
||||||
from .serializers_.attachments import *
|
from .serializers_.attachments import *
|
||||||
from .serializers_.bookmarks import *
|
from .serializers_.bookmarks import *
|
||||||
from .serializers_.customfields import *
|
from .serializers_.customfields import *
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ImageAttachment
|
model = ImageAttachment
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'object_type', 'object_id', 'parent', 'name', 'image', 'description',
|
'id', 'url', 'display', 'object_type', 'object_id', 'parent', 'name', 'image',
|
||||||
'image_height', 'image_width', 'created', 'last_updated',
|
'image_height', 'image_width', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'image', 'description')
|
brief_fields = ('id', 'url', 'display', 'name', 'image')
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
|
|
||||||
|
|||||||
16
netbox/extras/api/serializers_/objecttypes.py
Normal file
16
netbox/extras/api/serializers_/objecttypes.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
|
from netbox.api.serializers import BaseModelSerializer
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ObjectTypeSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectTypeSerializer(BaseModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:objecttype-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ObjectType
|
||||||
|
fields = ['id', 'url', 'display', 'app_label', 'model']
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
from core.api.views import ObjectTypeViewSet
|
|
||||||
from netbox.api.routers import NetBoxRouter
|
from netbox.api.routers import NetBoxRouter
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
@@ -27,9 +26,7 @@ router.register('journal-entries', views.JournalEntryViewSet)
|
|||||||
router.register('config-contexts', views.ConfigContextViewSet)
|
router.register('config-contexts', views.ConfigContextViewSet)
|
||||||
router.register('config-templates', views.ConfigTemplateViewSet)
|
router.register('config-templates', views.ConfigTemplateViewSet)
|
||||||
router.register('scripts', views.ScriptViewSet, basename='script')
|
router.register('scripts', views.ScriptViewSet, basename='script')
|
||||||
|
router.register('object-types', views.ObjectTypeViewSet)
|
||||||
# TODO: Remove in NetBox v4.5
|
|
||||||
router.register('object-types', ObjectTypeViewSet)
|
|
||||||
|
|
||||||
app_name = 'extras-api'
|
app_name = 'extras-api'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
from django.conf import settings
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.views.static import serve
|
||||||
from django_rq.queues import get_connection
|
from django_rq.queues import get_connection
|
||||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@@ -10,9 +12,10 @@ from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
|
|||||||
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.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||||
from rq import Worker
|
from rq import Worker
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from extras import filtersets
|
from extras import filtersets
|
||||||
from extras.jobs import ScriptJob
|
from extras.jobs import ScriptJob
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
@@ -199,6 +202,17 @@ class ImageAttachmentViewSet(NetBoxModelViewSet):
|
|||||||
serializer_class = serializers.ImageAttachmentSerializer
|
serializer_class = serializers.ImageAttachmentSerializer
|
||||||
filterset_class = filtersets.ImageAttachmentFilterSet
|
filterset_class = filtersets.ImageAttachmentFilterSet
|
||||||
|
|
||||||
|
@action(
|
||||||
|
methods=['GET'],
|
||||||
|
detail=True,
|
||||||
|
url_path='download',
|
||||||
|
url_name='download',
|
||||||
|
)
|
||||||
|
def download(self, request, pk, *args, **kwargs):
|
||||||
|
obj = get_object_or_404(self.queryset, pk=pk)
|
||||||
|
# Render and return the elevation as an SVG drawing with the correct content type
|
||||||
|
return serve(request, obj.image.name, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Journal entries
|
# Journal entries
|
||||||
@@ -313,6 +327,20 @@ class ScriptViewSet(ModelViewSet):
|
|||||||
return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Object types
|
||||||
|
#
|
||||||
|
|
||||||
|
class ObjectTypeViewSet(ReadOnlyModelViewSet):
|
||||||
|
"""
|
||||||
|
Read-only list of ObjectTypes.
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||||
|
queryset = ObjectType.objects.order_by('app_label', 'model')
|
||||||
|
serializer_class = serializers.ObjectTypeSerializer
|
||||||
|
filterset_class = filtersets.ObjectTypeFilterSet
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# User dashboard
|
# User dashboard
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ __all__ = (
|
|||||||
'JournalEntryFilterSet',
|
'JournalEntryFilterSet',
|
||||||
'LocalConfigContextFilterSet',
|
'LocalConfigContextFilterSet',
|
||||||
'NotificationGroupFilterSet',
|
'NotificationGroupFilterSet',
|
||||||
|
'ObjectTypeFilterSet',
|
||||||
'SavedFilterFilterSet',
|
'SavedFilterFilterSet',
|
||||||
'ScriptFilterSet',
|
'ScriptFilterSet',
|
||||||
'TableConfigFilterSet',
|
'TableConfigFilterSet',
|
||||||
@@ -451,15 +452,12 @@ class ImageAttachmentFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ImageAttachment
|
model = ImageAttachment
|
||||||
fields = ('id', 'object_type_id', 'object_id', 'name', 'description', 'image_width', 'image_height')
|
fields = ('id', 'object_type_id', 'object_id', 'name', 'image_width', 'image_height')
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(name__icontains=value)
|
||||||
Q(name__icontains=value) |
|
|
||||||
Q(description__icontains=value)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryFilterSet(NetBoxModelFilterSet):
|
class JournalEntryFilterSet(NetBoxModelFilterSet):
|
||||||
@@ -790,3 +788,26 @@ class LocalConfigContextFilterSet(django_filters.FilterSet):
|
|||||||
|
|
||||||
def _local_context_data(self, queryset, name, value):
|
def _local_context_data(self, queryset, name, value):
|
||||||
return queryset.exclude(local_context_data__isnull=value)
|
return queryset.exclude(local_context_data__isnull=value)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# ContentTypes
|
||||||
|
#
|
||||||
|
|
||||||
|
class ObjectTypeFilterSet(django_filters.FilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label=_('Search'),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ObjectType
|
||||||
|
fields = ('id', 'app_label', 'model')
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(app_label__icontains=value) |
|
||||||
|
Q(model__icontains=value)
|
||||||
|
)
|
||||||
|
|||||||
@@ -744,17 +744,14 @@ class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class ImageAttachmentForm(forms.ModelForm):
|
class ImageAttachmentForm(forms.ModelForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet(ObjectAttribute('parent'), 'image', 'name', 'description'),
|
FieldSet(ObjectAttribute('parent'), 'name', 'image'),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ImageAttachment
|
model = ImageAttachment
|
||||||
fields = [
|
fields = [
|
||||||
'image', 'name', 'description',
|
'name', 'image',
|
||||||
]
|
]
|
||||||
help_texts = {
|
|
||||||
'name': _("If no name is specified, the file name will be used.")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryForm(NetBoxModelForm):
|
class JournalEntryForm(NetBoxModelForm):
|
||||||
|
|||||||
@@ -90,10 +90,7 @@ class ScriptJob(JobRunner):
|
|||||||
request: The WSGI request associated with this execution (if any)
|
request: The WSGI request associated with this execution (if any)
|
||||||
commit: Passed through to Script.run()
|
commit: Passed through to Script.run()
|
||||||
"""
|
"""
|
||||||
script_model = ScriptModel.objects.get(pk=self.job.object_id)
|
script = ScriptModel.objects.get(pk=self.job.object_id).python_class()
|
||||||
self.logger.debug(f"Found ScriptModel ID {script_model.pk}")
|
|
||||||
script = script_model.python_class()
|
|
||||||
self.logger.debug(f"Loaded script {script.full_name}")
|
|
||||||
|
|
||||||
# Add files to form data
|
# Add files to form data
|
||||||
if request:
|
if request:
|
||||||
@@ -103,7 +100,6 @@ class ScriptJob(JobRunner):
|
|||||||
|
|
||||||
# Add the current request as a property of the script
|
# Add the current request as a property of the script
|
||||||
script.request = request
|
script.request = request
|
||||||
self.logger.debug(f"Request ID: {request.id}")
|
|
||||||
|
|
||||||
# Execute the script. If commit is True, wrap it with the event_tracking context manager to ensure we process
|
# Execute the script. If commit is True, wrap it with the event_tracking context manager to ensure we process
|
||||||
# change logging, event rules, etc.
|
# change logging, event rules, etc.
|
||||||
|
|||||||
@@ -14,16 +14,9 @@ from utilities.proxy import resolve_proxies
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Perform nightly housekeeping tasks [DEPRECATED]"
|
help = "Perform nightly housekeeping tasks. (This command can be run at any time.)"
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
self.stdout.write(
|
|
||||||
"Running this command is no longer necessary: All housekeeping tasks\n"
|
|
||||||
"are addressed automatically via NetBox's built-in job scheduler. It\n"
|
|
||||||
"will be removed in a future release.",
|
|
||||||
self.style.WARNING
|
|
||||||
)
|
|
||||||
|
|
||||||
config = Config()
|
config = Config()
|
||||||
|
|
||||||
# Clear expired authentication sessions (essentially replicating the `clearsessions` command)
|
# Clear expired authentication sessions (essentially replicating the `clearsessions` command)
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('extras', '0129_fix_script_paths'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='imageattachment',
|
|
||||||
name='description',
|
|
||||||
field=models.CharField(blank=True, max_length=200),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -679,11 +678,6 @@ class ImageAttachment(ChangeLoggedModel):
|
|||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
description = models.CharField(
|
|
||||||
verbose_name=_('description'),
|
|
||||||
max_length=200,
|
|
||||||
blank=True
|
|
||||||
)
|
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
@@ -698,10 +692,10 @@ class ImageAttachment(ChangeLoggedModel):
|
|||||||
verbose_name_plural = _('image attachments')
|
verbose_name_plural = _('image attachments')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name or self.filename
|
if self.name:
|
||||||
|
return self.name
|
||||||
def get_absolute_url(self):
|
filename = self.image.name.rsplit('/', 1)[-1]
|
||||||
return reverse('extras:imageattachment', args=[self.pk])
|
return filename.split('_', 2)[2]
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
@@ -725,10 +719,6 @@ class ImageAttachment(ChangeLoggedModel):
|
|||||||
# before the request finishes. (For example, to display a message indicating the ImageAttachment was deleted.)
|
# before the request finishes. (For example, to display a message indicating the ImageAttachment was deleted.)
|
||||||
self.image.name = _name
|
self.image.name = _name
|
||||||
|
|
||||||
@property
|
|
||||||
def filename(self):
|
|
||||||
return os.path.basename(self.image.name).split('_', 2)[2]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self):
|
def size(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -14,16 +14,6 @@ class CustomFieldIndex(SearchIndex):
|
|||||||
display_attrs = ('description',)
|
display_attrs = ('description',)
|
||||||
|
|
||||||
|
|
||||||
@register_search
|
|
||||||
class ImageAttachmentIndex(SearchIndex):
|
|
||||||
model = models.ImageAttachment
|
|
||||||
fields = (
|
|
||||||
('name', 100),
|
|
||||||
('description', 500),
|
|
||||||
)
|
|
||||||
display_attrs = ('description',)
|
|
||||||
|
|
||||||
|
|
||||||
@register_search
|
@register_search
|
||||||
class JournalEntryIndex(SearchIndex):
|
class JournalEntryIndex(SearchIndex):
|
||||||
model = models.JournalEntry
|
model = models.JournalEntry
|
||||||
|
|||||||
@@ -249,10 +249,10 @@ class ImageAttachmentTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = ImageAttachment
|
model = ImageAttachment
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'object_type', 'parent', 'image', 'name', 'description', 'image_height', 'image_width', 'size',
|
'pk', 'object_type', 'parent', 'image', 'name', 'image_height', 'image_width', 'size', 'created',
|
||||||
'created', 'last_updated',
|
'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('object_type', 'parent', 'image', 'name', 'description', 'size', 'created')
|
default_columns = ('object_type', 'parent', 'image', 'name', 'size', 'created')
|
||||||
|
|
||||||
|
|
||||||
class SavedFilterTable(NetBoxTable):
|
class SavedFilterTable(NetBoxTable):
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.files.base import File
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
from core.choices import ManagedFileRootPathChoices
|
from core.choices import ManagedFileRootPathChoices
|
||||||
from core.events import *
|
from core.events import *
|
||||||
@@ -579,7 +582,7 @@ class ImageAttachmentTest(
|
|||||||
APIViewTestCases.GraphQLTestCase
|
APIViewTestCases.GraphQLTestCase
|
||||||
):
|
):
|
||||||
model = ImageAttachment
|
model = ImageAttachment
|
||||||
brief_fields = ['description', 'display', 'id', 'image', 'name', 'url']
|
brief_fields = ['display', 'id', 'image', 'name', 'url']
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
@@ -615,6 +618,38 @@ class ImageAttachmentTest(
|
|||||||
)
|
)
|
||||||
ImageAttachment.objects.bulk_create(image_attachments)
|
ImageAttachment.objects.bulk_create(image_attachments)
|
||||||
|
|
||||||
|
def test_image_download(self):
|
||||||
|
self.add_permissions('extras.view_imageattachment')
|
||||||
|
ct = ContentType.objects.get_for_model(Site)
|
||||||
|
site = Site.objects.get(name='Site 1', slug='site-1')
|
||||||
|
|
||||||
|
image = Image.new('RGB', size=(1, 1), color=(255, 0, 0))
|
||||||
|
image.save('test_image_download.png', format='PNG')
|
||||||
|
image_file = File(open('test_image_download.png', 'rb'))
|
||||||
|
content = image_file.read()
|
||||||
|
|
||||||
|
attachment = ImageAttachment(
|
||||||
|
object_type=ct,
|
||||||
|
object_id=site.pk,
|
||||||
|
name='Image Attachment 4',
|
||||||
|
image_height=1,
|
||||||
|
image_width=1
|
||||||
|
)
|
||||||
|
attachment.image.save('test_image_download.png', image_file, save=True)
|
||||||
|
attachment.save()
|
||||||
|
|
||||||
|
image = ImageAttachment.objects.get(name='Image Attachment 4')
|
||||||
|
url = reverse('extras-api:imageattachment-download', kwargs={'pk': image.pk})
|
||||||
|
response = self.client.get(url, **self.header)
|
||||||
|
downloaded_content = b''.join(response.streaming_content)
|
||||||
|
|
||||||
|
self.assertEqual(response.headers.get('Content-Type'), 'image/png')
|
||||||
|
self.assertEqual(response.headers.get('Content-Length'), '69')
|
||||||
|
self.assertEqual(
|
||||||
|
response.headers.get('Content-Disposition'), f'inline; filename="site_{site.pk}_Image_Attachment_4.png"'
|
||||||
|
)
|
||||||
|
self.assertEqual(content, downloaded_content)
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryTest(APIViewTestCases.APIViewTestCase):
|
class JournalEntryTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = JournalEntry
|
model = JournalEntry
|
||||||
@@ -920,6 +955,22 @@ class CreatedUpdatedFilterTest(APITestCase):
|
|||||||
self.assertEqual(response.data['results'][0]['id'], rack2.pk)
|
self.assertEqual(response.data['results'][0]['id'], rack2.pk)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectTypeTest(APITestCase):
|
||||||
|
|
||||||
|
def test_list_objects(self):
|
||||||
|
object_type_count = ObjectType.objects.count()
|
||||||
|
|
||||||
|
response = self.client.get(reverse('extras-api:objecttype-list'), **self.header)
|
||||||
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data['count'], object_type_count)
|
||||||
|
|
||||||
|
def test_get_object(self):
|
||||||
|
object_type = ObjectType.objects.first()
|
||||||
|
|
||||||
|
url = reverse('extras-api:objecttype-detail', kwargs={'pk': object_type.pk})
|
||||||
|
self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionTest(APIViewTestCases.APIViewTestCase):
|
class SubscriptionTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = Subscription
|
model = Subscription
|
||||||
brief_fields = ['display', 'id', 'object_id', 'object_type', 'url', 'user']
|
brief_fields = ['display', 'id', 'object_id', 'object_type', 'url', 'user']
|
||||||
|
|||||||
@@ -14,13 +14,12 @@ from jinja2.exceptions import TemplateError
|
|||||||
|
|
||||||
from core.choices import ManagedFileRootPathChoices
|
from core.choices import ManagedFileRootPathChoices
|
||||||
from core.models import Job
|
from core.models import Job
|
||||||
from core.object_actions import BulkSync
|
|
||||||
from dcim.models import Device, DeviceRole, Platform
|
from dcim.models import Device, DeviceRole, Platform
|
||||||
from extras.choices import LogLevelChoices
|
from extras.choices import LogLevelChoices
|
||||||
from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
|
from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
|
||||||
from extras.dashboard.utils import get_widget_class
|
from extras.dashboard.utils import get_widget_class
|
||||||
from extras.utils import SharedObjectViewMixin
|
from extras.utils import SharedObjectViewMixin
|
||||||
from netbox.object_actions import *
|
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from netbox.views.generic.mixins import TableMixin
|
from netbox.views.generic.mixins import TableMixin
|
||||||
from utilities.forms import ConfirmationForm, get_field_value
|
from utilities.forms import ConfirmationForm, get_field_value
|
||||||
@@ -97,11 +96,6 @@ class CustomFieldBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.CustomFieldBulkEditForm
|
form = forms.CustomFieldBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(CustomField, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class CustomFieldBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = CustomField.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(CustomField, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(CustomField, 'bulk_delete', path='delete', detail=False)
|
||||||
class CustomFieldBulkDeleteView(generic.BulkDeleteView):
|
class CustomFieldBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = CustomField.objects.select_related('choice_set')
|
queryset = CustomField.objects.select_related('choice_set')
|
||||||
@@ -171,11 +165,6 @@ class CustomFieldChoiceSetBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.CustomFieldChoiceSetBulkEditForm
|
form = forms.CustomFieldChoiceSetBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(CustomFieldChoiceSet, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class CustomFieldChoiceSetBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = CustomFieldChoiceSet.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(CustomFieldChoiceSet, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(CustomFieldChoiceSet, 'bulk_delete', path='delete', detail=False)
|
||||||
class CustomFieldChoiceSetBulkDeleteView(generic.BulkDeleteView):
|
class CustomFieldChoiceSetBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = CustomFieldChoiceSet.objects.all()
|
queryset = CustomFieldChoiceSet.objects.all()
|
||||||
@@ -226,11 +215,6 @@ class CustomLinkBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.CustomLinkBulkEditForm
|
form = forms.CustomLinkBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(CustomLink, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class CustomLinkBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = CustomLink.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(CustomLink, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(CustomLink, 'bulk_delete', path='delete', detail=False)
|
||||||
class CustomLinkBulkDeleteView(generic.BulkDeleteView):
|
class CustomLinkBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = CustomLink.objects.all()
|
queryset = CustomLink.objects.all()
|
||||||
@@ -248,7 +232,11 @@ class ExportTemplateListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.ExportTemplateFilterSet
|
filterset = filtersets.ExportTemplateFilterSet
|
||||||
filterset_form = forms.ExportTemplateFilterForm
|
filterset_form = forms.ExportTemplateFilterForm
|
||||||
table = tables.ExportTemplateTable
|
table = tables.ExportTemplateTable
|
||||||
actions = (AddObject, BulkImport, BulkSync, BulkExport, BulkEdit, BulkRename, BulkDelete)
|
template_name = 'extras/exporttemplate_list.html'
|
||||||
|
actions = {
|
||||||
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
|
'bulk_sync': {'sync'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ExportTemplate)
|
@register_model_view(ExportTemplate)
|
||||||
@@ -282,11 +270,6 @@ class ExportTemplateBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.ExportTemplateBulkEditForm
|
form = forms.ExportTemplateBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ExportTemplate, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class ExportTemplateBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = ExportTemplate.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ExportTemplate, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(ExportTemplate, 'bulk_delete', path='delete', detail=False)
|
||||||
class ExportTemplateBulkDeleteView(generic.BulkDeleteView):
|
class ExportTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = ExportTemplate.objects.all()
|
queryset = ExportTemplate.objects.all()
|
||||||
@@ -347,11 +330,6 @@ class SavedFilterBulkEditView(SharedObjectViewMixin, generic.BulkEditView):
|
|||||||
form = forms.SavedFilterBulkEditForm
|
form = forms.SavedFilterBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(SavedFilter, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class SavedFilterBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = SavedFilter.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(SavedFilter, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(SavedFilter, 'bulk_delete', path='delete', detail=False)
|
||||||
class SavedFilterBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView):
|
class SavedFilterBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView):
|
||||||
queryset = SavedFilter.objects.all()
|
queryset = SavedFilter.objects.all()
|
||||||
@@ -369,7 +347,9 @@ class TableConfigListView(SharedObjectViewMixin, generic.ObjectListView):
|
|||||||
filterset = filtersets.TableConfigFilterSet
|
filterset = filtersets.TableConfigFilterSet
|
||||||
filterset_form = forms.TableConfigFilterForm
|
filterset_form = forms.TableConfigFilterForm
|
||||||
table = tables.TableConfigTable
|
table = tables.TableConfigTable
|
||||||
actions = (BulkExport, BulkEdit, BulkRename, BulkDelete)
|
actions = {
|
||||||
|
'export': {'view'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(TableConfig)
|
@register_model_view(TableConfig)
|
||||||
@@ -409,11 +389,6 @@ class TableConfigBulkEditView(SharedObjectViewMixin, generic.BulkEditView):
|
|||||||
form = forms.TableConfigBulkEditForm
|
form = forms.TableConfigBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(TableConfig, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class TableConfigBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = TableConfig.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(TableConfig, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(TableConfig, 'bulk_delete', path='delete', detail=False)
|
||||||
class TableConfigBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView):
|
class TableConfigBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView):
|
||||||
queryset = TableConfig.objects.all()
|
queryset = TableConfig.objects.all()
|
||||||
@@ -495,11 +470,6 @@ class NotificationGroupBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.NotificationGroupBulkEditForm
|
form = forms.NotificationGroupBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(NotificationGroup, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class NotificationGroupBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = NotificationGroup.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(NotificationGroup, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(NotificationGroup, 'bulk_delete', path='delete', detail=False)
|
||||||
class NotificationGroupBulkDeleteView(generic.BulkDeleteView):
|
class NotificationGroupBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = NotificationGroup.objects.all()
|
queryset = NotificationGroup.objects.all()
|
||||||
@@ -646,11 +616,6 @@ class WebhookBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.WebhookBulkEditForm
|
form = forms.WebhookBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Webhook, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class WebhookBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = Webhook.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Webhook, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(Webhook, 'bulk_delete', path='delete', detail=False)
|
||||||
class WebhookBulkDeleteView(generic.BulkDeleteView):
|
class WebhookBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Webhook.objects.all()
|
queryset = Webhook.objects.all()
|
||||||
@@ -701,11 +666,6 @@ class EventRuleBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.EventRuleBulkEditForm
|
form = forms.EventRuleBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(EventRule, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class EventRuleBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = EventRule.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(EventRule, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(EventRule, 'bulk_delete', path='delete', detail=False)
|
||||||
class EventRuleBulkDeleteView(generic.BulkDeleteView):
|
class EventRuleBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = EventRule.objects.all()
|
queryset = EventRule.objects.all()
|
||||||
@@ -780,11 +740,6 @@ class TagBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.TagBulkEditForm
|
form = forms.TagBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Tag, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class TagBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = Tag.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Tag, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(Tag, 'bulk_delete', path='delete', detail=False)
|
||||||
class TagBulkDeleteView(generic.BulkDeleteView):
|
class TagBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Tag.objects.annotate(
|
queryset = Tag.objects.annotate(
|
||||||
@@ -803,7 +758,13 @@ class ConfigContextListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.ConfigContextFilterSet
|
filterset = filtersets.ConfigContextFilterSet
|
||||||
filterset_form = forms.ConfigContextFilterForm
|
filterset_form = forms.ConfigContextFilterForm
|
||||||
table = tables.ConfigContextTable
|
table = tables.ConfigContextTable
|
||||||
actions = (AddObject, BulkSync, BulkEdit, BulkRename, BulkDelete)
|
template_name = 'extras/configcontext_list.html'
|
||||||
|
actions = {
|
||||||
|
'add': {'add'},
|
||||||
|
'bulk_edit': {'change'},
|
||||||
|
'bulk_delete': {'delete'},
|
||||||
|
'bulk_sync': {'sync'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ConfigContext)
|
@register_model_view(ConfigContext)
|
||||||
@@ -864,11 +825,6 @@ class ConfigContextBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.ConfigContextBulkEditForm
|
form = forms.ConfigContextBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ConfigContext, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class ConfigContextBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = ConfigContext.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ConfigContext, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(ConfigContext, 'bulk_delete', path='delete', detail=False)
|
||||||
class ConfigContextBulkDeleteView(generic.BulkDeleteView):
|
class ConfigContextBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = ConfigContext.objects.all()
|
queryset = ConfigContext.objects.all()
|
||||||
@@ -921,7 +877,11 @@ class ConfigTemplateListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.ConfigTemplateFilterSet
|
filterset = filtersets.ConfigTemplateFilterSet
|
||||||
filterset_form = forms.ConfigTemplateFilterForm
|
filterset_form = forms.ConfigTemplateFilterForm
|
||||||
table = tables.ConfigTemplateTable
|
table = tables.ConfigTemplateTable
|
||||||
actions = (AddObject, BulkImport, BulkExport, BulkSync, BulkEdit, BulkRename, BulkDelete)
|
template_name = 'extras/configtemplate_list.html'
|
||||||
|
actions = {
|
||||||
|
**DEFAULT_ACTION_PERMISSIONS,
|
||||||
|
'bulk_sync': {'sync'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ConfigTemplate)
|
@register_model_view(ConfigTemplate)
|
||||||
@@ -955,11 +915,6 @@ class ConfigTemplateBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.ConfigTemplateBulkEditForm
|
form = forms.ConfigTemplateBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ConfigTemplate, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class ConfigTemplateBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = ConfigTemplate.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ConfigTemplate, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(ConfigTemplate, 'bulk_delete', path='delete', detail=False)
|
||||||
class ConfigTemplateBulkDeleteView(generic.BulkDeleteView):
|
class ConfigTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = ConfigTemplate.objects.all()
|
queryset = ConfigTemplate.objects.all()
|
||||||
@@ -1037,12 +992,9 @@ class ImageAttachmentListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.ImageAttachmentFilterSet
|
filterset = filtersets.ImageAttachmentFilterSet
|
||||||
filterset_form = forms.ImageAttachmentFilterForm
|
filterset_form = forms.ImageAttachmentFilterForm
|
||||||
table = tables.ImageAttachmentTable
|
table = tables.ImageAttachmentTable
|
||||||
actions = (BulkExport,)
|
actions = {
|
||||||
|
'export': {'view'},
|
||||||
|
}
|
||||||
@register_model_view(ImageAttachment)
|
|
||||||
class ImageAttachmentView(generic.ObjectView):
|
|
||||||
queryset = ImageAttachment.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ImageAttachment, 'add', detail=False)
|
@register_model_view(ImageAttachment, 'add', detail=False)
|
||||||
@@ -1058,6 +1010,9 @@ class ImageAttachmentEditView(generic.ObjectEditView):
|
|||||||
instance.parent = get_object_or_404(object_type.model_class(), pk=request.GET.get('object_id'))
|
instance.parent = get_object_or_404(object_type.model_class(), pk=request.GET.get('object_id'))
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
def get_return_url(self, request, obj=None):
|
||||||
|
return obj.parent.get_absolute_url() if obj else super().get_return_url(request)
|
||||||
|
|
||||||
def get_extra_addanother_params(self, request):
|
def get_extra_addanother_params(self, request):
|
||||||
return {
|
return {
|
||||||
'object_type': request.GET.get('object_type'),
|
'object_type': request.GET.get('object_type'),
|
||||||
@@ -1069,6 +1024,9 @@ class ImageAttachmentEditView(generic.ObjectEditView):
|
|||||||
class ImageAttachmentDeleteView(generic.ObjectDeleteView):
|
class ImageAttachmentDeleteView(generic.ObjectDeleteView):
|
||||||
queryset = ImageAttachment.objects.all()
|
queryset = ImageAttachment.objects.all()
|
||||||
|
|
||||||
|
def get_return_url(self, request, obj=None):
|
||||||
|
return obj.parent.get_absolute_url() if obj else super().get_return_url(request)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Journal entries
|
# Journal entries
|
||||||
@@ -1080,7 +1038,12 @@ class JournalEntryListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.JournalEntryFilterSet
|
filterset = filtersets.JournalEntryFilterSet
|
||||||
filterset_form = forms.JournalEntryFilterForm
|
filterset_form = forms.JournalEntryFilterForm
|
||||||
table = tables.JournalEntryTable
|
table = tables.JournalEntryTable
|
||||||
actions = (BulkImport, BulkEdit, BulkDelete)
|
actions = {
|
||||||
|
'export': {'view'},
|
||||||
|
'bulk_import': {'add'},
|
||||||
|
'bulk_edit': {'change'},
|
||||||
|
'bulk_delete': {'delete'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(JournalEntry)
|
@register_model_view(JournalEntry)
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from utilities.forms.fields import (
|
|||||||
from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups
|
from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups
|
||||||
from utilities.forms.utils import get_field_value
|
from utilities.forms.utils import get_field_value
|
||||||
from utilities.forms.widgets import DatePicker, HTMXSelect
|
from utilities.forms.widgets import DatePicker, HTMXSelect
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
from utilities.templatetags.builtins.filters import bettertitle
|
from utilities.templatetags.builtins.filters import bettertitle
|
||||||
from virtualization.models import VMInterface
|
from virtualization.models import VMInterface
|
||||||
|
|
||||||
@@ -681,15 +680,7 @@ class VLANForm(TenancyForm, NetBoxModelForm):
|
|||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
null_option='None',
|
null_option='None',
|
||||||
selector=True,
|
selector=True
|
||||||
help_text=mark_safe(
|
|
||||||
'<span class="text-warning"><i class="mdi mdi-alert"></i> {text}</span>'.format(
|
|
||||||
text=_(
|
|
||||||
'The direct assignment of VLANs to a site is deprecated and will be removed in a future release. '
|
|
||||||
'Users are encouraged to utilize VLAN groups for this purpose.'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
role = DynamicModelChoiceField(
|
role = DynamicModelChoiceField(
|
||||||
label=_('Role'),
|
label=_('Role'),
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from dcim.filtersets import InterfaceFilterSet
|
|||||||
from dcim.forms import InterfaceFilterForm
|
from dcim.forms import InterfaceFilterForm
|
||||||
from dcim.models import Device, Interface, Site
|
from dcim.models import Device, Interface, Site
|
||||||
from ipam.tables import VLANTranslationRuleTable
|
from ipam.tables import VLANTranslationRuleTable
|
||||||
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
|
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from utilities.query import count_related
|
from utilities.query import count_related
|
||||||
from utilities.tables import get_table_ordering
|
from utilities.tables import get_table_ordering
|
||||||
@@ -87,11 +86,6 @@ class VRFBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.VRFBulkEditForm
|
form = forms.VRFBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VRF, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class VRFBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = VRF.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VRF, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(VRF, 'bulk_delete', path='delete', detail=False)
|
||||||
class VRFBulkDeleteView(generic.BulkDeleteView):
|
class VRFBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = VRF.objects.all()
|
queryset = VRF.objects.all()
|
||||||
@@ -142,11 +136,6 @@ class RouteTargetBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.RouteTargetBulkEditForm
|
form = forms.RouteTargetBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(RouteTarget, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class RouteTargetBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = RouteTarget.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(RouteTarget, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(RouteTarget, 'bulk_delete', path='delete', detail=False)
|
||||||
class RouteTargetBulkDeleteView(generic.BulkDeleteView):
|
class RouteTargetBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = RouteTarget.objects.all()
|
queryset = RouteTarget.objects.all()
|
||||||
@@ -206,11 +195,6 @@ class RIRBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.RIRBulkEditForm
|
form = forms.RIRBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(RIR, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class RIRBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = RIR.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(RIR, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(RIR, 'bulk_delete', path='delete', detail=False)
|
||||||
class RIRBulkDeleteView(generic.BulkDeleteView):
|
class RIRBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = RIR.objects.annotate(
|
queryset = RIR.objects.annotate(
|
||||||
@@ -284,11 +268,6 @@ class ASNRangeBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.ASNRangeBulkEditForm
|
form = forms.ASNRangeBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ASNRange, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class ASNRangeBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = ASNRange.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ASNRange, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(ASNRange, 'bulk_delete', path='delete', detail=False)
|
||||||
class ASNRangeBulkDeleteView(generic.BulkDeleteView):
|
class ASNRangeBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = ASNRange.objects.annotate_asn_counts()
|
queryset = ASNRange.objects.annotate_asn_counts()
|
||||||
@@ -356,11 +335,6 @@ class ASNBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.ASNBulkEditForm
|
form = forms.ASNBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ASN, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class ASNBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = ASN.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ASN, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(ASN, 'bulk_delete', path='delete', detail=False)
|
||||||
class ASNBulkDeleteView(generic.BulkDeleteView):
|
class ASNBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = ASN.objects.annotate(
|
queryset = ASN.objects.annotate(
|
||||||
@@ -382,7 +356,6 @@ class AggregateListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.AggregateFilterSet
|
filterset = filtersets.AggregateFilterSet
|
||||||
filterset_form = forms.AggregateFilterForm
|
filterset_form = forms.AggregateFilterForm
|
||||||
table = tables.AggregateTable
|
table = tables.AggregateTable
|
||||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Aggregate)
|
@register_model_view(Aggregate)
|
||||||
@@ -515,11 +488,6 @@ class RoleBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.RoleBulkEditForm
|
form = forms.RoleBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Role, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class RoleBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = Role.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Role, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(Role, 'bulk_delete', path='delete', detail=False)
|
||||||
class RoleBulkDeleteView(generic.BulkDeleteView):
|
class RoleBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Role.objects.all()
|
queryset = Role.objects.all()
|
||||||
@@ -538,7 +506,6 @@ class PrefixListView(generic.ObjectListView):
|
|||||||
filterset_form = forms.PrefixFilterForm
|
filterset_form = forms.PrefixFilterForm
|
||||||
table = tables.PrefixTable
|
table = tables.PrefixTable
|
||||||
template_name = 'ipam/prefix_list.html'
|
template_name = 'ipam/prefix_list.html'
|
||||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Prefix)
|
@register_model_view(Prefix)
|
||||||
@@ -799,11 +766,6 @@ class IPRangeBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.IPRangeBulkEditForm
|
form = forms.IPRangeBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(IPRange, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class IPRangeBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = IPRange.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(IPRange, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(IPRange, 'bulk_delete', path='delete', detail=False)
|
||||||
class IPRangeBulkDeleteView(generic.BulkDeleteView):
|
class IPRangeBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = IPRange.objects.all()
|
queryset = IPRange.objects.all()
|
||||||
@@ -821,7 +783,6 @@ class IPAddressListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.IPAddressFilterSet
|
filterset = filtersets.IPAddressFilterSet
|
||||||
filterset_form = forms.IPAddressFilterForm
|
filterset_form = forms.IPAddressFilterForm
|
||||||
table = tables.IPAddressTable
|
table = tables.IPAddressTable
|
||||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(IPAddress)
|
@register_model_view(IPAddress)
|
||||||
@@ -1045,11 +1006,6 @@ class VLANGroupBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.VLANGroupBulkEditForm
|
form = forms.VLANGroupBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VLANGroup, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class VLANGroupBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = VLANGroup.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VLANGroup, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(VLANGroup, 'bulk_delete', path='delete', detail=False)
|
||||||
class VLANGroupBulkDeleteView(generic.BulkDeleteView):
|
class VLANGroupBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
|
queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
|
||||||
@@ -1139,11 +1095,6 @@ class VLANTranslationPolicyBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.VLANTranslationPolicyBulkEditForm
|
form = forms.VLANTranslationPolicyBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VLANTranslationPolicy, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class VLANTranslationPolicyBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = VLANTranslationPolicy.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VLANTranslationPolicy, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(VLANTranslationPolicy, 'bulk_delete', path='delete', detail=False)
|
||||||
class VLANTranslationPolicyBulkDeleteView(generic.BulkDeleteView):
|
class VLANTranslationPolicyBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = VLANTranslationPolicy.objects.all()
|
queryset = VLANTranslationPolicy.objects.all()
|
||||||
@@ -1161,7 +1112,6 @@ class VLANTranslationRuleListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.VLANTranslationRuleFilterSet
|
filterset = filtersets.VLANTranslationRuleFilterSet
|
||||||
filterset_form = forms.VLANTranslationRuleFilterForm
|
filterset_form = forms.VLANTranslationRuleFilterForm
|
||||||
table = tables.VLANTranslationRuleTable
|
table = tables.VLANTranslationRuleTable
|
||||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VLANTranslationRule)
|
@register_model_view(VLANTranslationRule)
|
||||||
@@ -1294,11 +1244,6 @@ class FHRPGroupBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.FHRPGroupBulkEditForm
|
form = forms.FHRPGroupBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(FHRPGroup, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class FHRPGroupBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = FHRPGroup.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(FHRPGroup, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(FHRPGroup, 'bulk_delete', path='delete', detail=False)
|
||||||
class FHRPGroupBulkDeleteView(generic.BulkDeleteView):
|
class FHRPGroupBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = FHRPGroup.objects.all()
|
queryset = FHRPGroup.objects.all()
|
||||||
@@ -1426,11 +1371,6 @@ class VLANBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.VLANBulkEditForm
|
form = forms.VLANBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VLAN, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class VLANBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = VLAN.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VLAN, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(VLAN, 'bulk_delete', path='delete', detail=False)
|
||||||
class VLANBulkDeleteView(generic.BulkDeleteView):
|
class VLANBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = VLAN.objects.all()
|
queryset = VLAN.objects.all()
|
||||||
@@ -1481,11 +1421,6 @@ class ServiceTemplateBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.ServiceTemplateBulkEditForm
|
form = forms.ServiceTemplateBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ServiceTemplate, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class ServiceTemplateBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = ServiceTemplate.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ServiceTemplate, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(ServiceTemplate, 'bulk_delete', path='delete', detail=False)
|
||||||
class ServiceTemplateBulkDeleteView(generic.BulkDeleteView):
|
class ServiceTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = ServiceTemplate.objects.all()
|
queryset = ServiceTemplate.objects.all()
|
||||||
@@ -1553,11 +1488,6 @@ class ServiceBulkEditView(generic.BulkEditView):
|
|||||||
form = forms.ServiceBulkEditForm
|
form = forms.ServiceBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Service, 'bulk_rename', path='rename', detail=False)
|
|
||||||
class ServiceBulkRenameView(generic.BulkRenameView):
|
|
||||||
queryset = Service.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Service, 'bulk_delete', path='delete', detail=False)
|
@register_model_view(Service, 'bulk_delete', path='delete', detail=False)
|
||||||
class ServiceBulkDeleteView(generic.BulkDeleteView):
|
class ServiceBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Service.objects.prefetch_related('parent')
|
queryset = Service.objects.prefetch_related('parent')
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ ADVISORY_LOCK_KEYS = {
|
|||||||
'job-schedules': 110100,
|
'job-schedules': 110100,
|
||||||
}
|
}
|
||||||
|
|
||||||
# TODO: Remove in NetBox v4.6
|
# Default view action permission mapping
|
||||||
# Legacy default view action permission mapping
|
|
||||||
DEFAULT_ACTION_PERMISSIONS = {
|
DEFAULT_ACTION_PERMISSIONS = {
|
||||||
'add': {'add'},
|
'add': {'add'},
|
||||||
'export': {'view'},
|
'export': {'view'},
|
||||||
@@ -44,10 +43,3 @@ CENSOR_TOKEN_CHANGED = '***CHANGED***'
|
|||||||
|
|
||||||
# Placeholder text for empty tables
|
# Placeholder text for empty tables
|
||||||
EMPTY_TABLE_TEXT = 'No results found'
|
EMPTY_TABLE_TEXT = 'No results found'
|
||||||
|
|
||||||
# CSV delimiters
|
|
||||||
CSV_DELIMITERS = {
|
|
||||||
'comma': ',',
|
|
||||||
'semicolon': ';',
|
|
||||||
'pipe': '|',
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import CustomField, Tag
|
from extras.models import CustomField, Tag
|
||||||
from utilities.forms import BulkEditForm, CSVModelForm
|
from utilities.forms import CSVModelForm
|
||||||
from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField
|
from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField
|
||||||
from utilities.forms.mixins import CheckLastUpdatedMixin
|
from utilities.forms.mixins import CheckLastUpdatedMixin
|
||||||
from .mixins import CustomFieldsMixin, SavedFiltersMixin, TagsMixin
|
from .mixins import CustomFieldsMixin, SavedFiltersMixin, TagsMixin
|
||||||
@@ -100,7 +100,7 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
|
|||||||
return customfield.to_form_field(for_csv_import=True)
|
return customfield.to_form_field(for_csv_import=True)
|
||||||
|
|
||||||
|
|
||||||
class NetBoxModelBulkEditForm(CustomFieldsMixin, BulkEditForm):
|
class NetBoxModelBulkEditForm(CustomFieldsMixin, forms.Form):
|
||||||
"""
|
"""
|
||||||
Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom
|
Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom
|
||||||
fields and adding/removing tags.
|
fields and adding/removing tags.
|
||||||
@@ -108,8 +108,9 @@ class NetBoxModelBulkEditForm(CustomFieldsMixin, BulkEditForm):
|
|||||||
Attributes:
|
Attributes:
|
||||||
fieldsets: An iterable of two-tuples which define a heading and field set to display per section of
|
fieldsets: An iterable of two-tuples which define a heading and field set to display per section of
|
||||||
the rendered form (optional). If not defined, the all fields will be rendered as a single section.
|
the rendered form (optional). If not defined, the all fields will be rendered as a single section.
|
||||||
|
nullable_fields: A list of field names indicating which fields support being set to null/empty
|
||||||
"""
|
"""
|
||||||
fieldsets = None
|
nullable_fields = ()
|
||||||
|
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=None, # Set from self.model on init
|
queryset=None, # Set from self.model on init
|
||||||
|
|||||||
@@ -8,16 +8,12 @@ from django_pglocks import advisory_lock
|
|||||||
from rq.timeouts import JobTimeoutException
|
from rq.timeouts import JobTimeoutException
|
||||||
|
|
||||||
from core.choices import JobStatusChoices
|
from core.choices import JobStatusChoices
|
||||||
from core.events import JOB_COMPLETED, JOB_FAILED
|
|
||||||
from core.exceptions import JobFailed
|
from core.exceptions import JobFailed
|
||||||
from core.models import Job, ObjectType
|
from core.models import Job, ObjectType
|
||||||
from extras.models import Notification
|
|
||||||
from netbox.constants import ADVISORY_LOCK_KEYS
|
from netbox.constants import ADVISORY_LOCK_KEYS
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from utilities.request import apply_request_processors
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'AsyncViewJob',
|
|
||||||
'JobRunner',
|
'JobRunner',
|
||||||
'system_job',
|
'system_job',
|
||||||
)
|
)
|
||||||
@@ -39,19 +35,6 @@ def system_job(interval):
|
|||||||
return _wrapper
|
return _wrapper
|
||||||
|
|
||||||
|
|
||||||
class JobLogHandler(logging.Handler):
|
|
||||||
"""
|
|
||||||
A logging handler which records entries on a Job.
|
|
||||||
"""
|
|
||||||
def __init__(self, job, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.job = job
|
|
||||||
|
|
||||||
def emit(self, record):
|
|
||||||
# Enter the record in the log of the associated Job
|
|
||||||
self.job.log(record)
|
|
||||||
|
|
||||||
|
|
||||||
class JobRunner(ABC):
|
class JobRunner(ABC):
|
||||||
"""
|
"""
|
||||||
Background Job helper class.
|
Background Job helper class.
|
||||||
@@ -70,11 +53,6 @@ class JobRunner(ABC):
|
|||||||
"""
|
"""
|
||||||
self.job = job
|
self.job = job
|
||||||
|
|
||||||
# Initiate the system logger
|
|
||||||
self.logger = logging.getLogger(f"netbox.jobs.{self.__class__.__name__}")
|
|
||||||
self.logger.setLevel(logging.DEBUG)
|
|
||||||
self.logger.addHandler(JobLogHandler(job))
|
|
||||||
|
|
||||||
@classproperty
|
@classproperty
|
||||||
def name(cls):
|
def name(cls):
|
||||||
return getattr(cls.Meta, 'name', cls.__name__)
|
return getattr(cls.Meta, 'name', cls.__name__)
|
||||||
@@ -183,34 +161,3 @@ class JobRunner(ABC):
|
|||||||
job.delete()
|
job.delete()
|
||||||
|
|
||||||
return cls.enqueue(instance=instance, schedule_at=schedule_at, interval=interval, *args, **kwargs)
|
return cls.enqueue(instance=instance, schedule_at=schedule_at, interval=interval, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class AsyncViewJob(JobRunner):
|
|
||||||
"""
|
|
||||||
Execute a view as a background job.
|
|
||||||
"""
|
|
||||||
class Meta:
|
|
||||||
name = 'Async View'
|
|
||||||
|
|
||||||
def run(self, view_cls, request, **kwargs):
|
|
||||||
view = view_cls.as_view()
|
|
||||||
|
|
||||||
# Apply all registered request processors (e.g. event_tracking)
|
|
||||||
with apply_request_processors(request):
|
|
||||||
data = view(request)
|
|
||||||
|
|
||||||
self.job.data = {
|
|
||||||
'log': data.log,
|
|
||||||
'errors': data.errors,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Notify the user
|
|
||||||
notification = Notification(
|
|
||||||
user=request.user,
|
|
||||||
object=self.job,
|
|
||||||
event_type=JOB_COMPLETED if not data.errors else JOB_FAILED,
|
|
||||||
)
|
|
||||||
notification.save()
|
|
||||||
|
|
||||||
if data.errors:
|
|
||||||
raise JobFailed()
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
from contextlib import ExitStack
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
import warnings
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import auth, messages
|
from django.contrib import auth, messages
|
||||||
@@ -10,10 +13,10 @@ from django.db.utils import InternalError
|
|||||||
from django.http import Http404, HttpResponseRedirect
|
from django.http import Http404, HttpResponseRedirect
|
||||||
|
|
||||||
from netbox.config import clear_config, get_config
|
from netbox.config import clear_config, get_config
|
||||||
|
from netbox.registry import registry
|
||||||
from netbox.views import handler_500
|
from netbox.views import handler_500
|
||||||
from utilities.api import is_api_request
|
from utilities.api import is_api_request
|
||||||
from utilities.error_handlers import handle_rest_api_exception
|
from utilities.error_handlers import handle_rest_api_exception
|
||||||
from utilities.request import apply_request_processors
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CoreMiddleware',
|
'CoreMiddleware',
|
||||||
@@ -33,7 +36,12 @@ class CoreMiddleware:
|
|||||||
request.id = uuid.uuid4()
|
request.id = uuid.uuid4()
|
||||||
|
|
||||||
# Apply all registered request processors
|
# Apply all registered request processors
|
||||||
with apply_request_processors(request):
|
with ExitStack() as stack:
|
||||||
|
for request_processor in registry['request_processors']:
|
||||||
|
try:
|
||||||
|
stack.enter_context(request_processor(request))
|
||||||
|
except Exception as e:
|
||||||
|
warnings.warn(f'Failed to initialize request processor {request_processor}: {e}')
|
||||||
response = self.get_response(request)
|
response = self.get_response(request)
|
||||||
|
|
||||||
# Check if language cookie should be renewed
|
# Check if language cookie should be renewed
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
from django.urls import reverse
|
|
||||||
from django.urls.exceptions import NoReverseMatch
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
|
|
||||||
from core.models import ObjectType
|
|
||||||
from extras.models import ExportTemplate
|
|
||||||
from utilities.querydict import prepare_cloned_fields
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
'AddObject',
|
|
||||||
'BulkDelete',
|
|
||||||
'BulkEdit',
|
|
||||||
'BulkExport',
|
|
||||||
'BulkImport',
|
|
||||||
'BulkRename',
|
|
||||||
'CloneObject',
|
|
||||||
'DeleteObject',
|
|
||||||
'EditObject',
|
|
||||||
'ObjectAction',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ObjectAction:
|
|
||||||
"""
|
|
||||||
Base class for single- and multi-object operations.
|
|
||||||
|
|
||||||
Params:
|
|
||||||
name: The action name appended to the module for view resolution
|
|
||||||
label: Human-friendly label for the rendered button
|
|
||||||
multi: Set to True if this action is performed by selecting multiple objects (i.e. using a table)
|
|
||||||
permissions_required: The set of permissions a user must have to perform the action
|
|
||||||
url_kwargs: The set of URL keyword arguments to pass when resolving the view's URL
|
|
||||||
"""
|
|
||||||
name = ''
|
|
||||||
label = None
|
|
||||||
multi = False
|
|
||||||
permissions_required = set()
|
|
||||||
url_kwargs = []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_url(cls, obj):
|
|
||||||
viewname = f'{obj._meta.app_label}:{obj._meta.model_name}_{cls.name}'
|
|
||||||
kwargs = {
|
|
||||||
kwarg: getattr(obj, kwarg) for kwarg in cls.url_kwargs
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
return reverse(viewname, kwargs=kwargs)
|
|
||||||
except NoReverseMatch:
|
|
||||||
return
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_context(cls, context, obj):
|
|
||||||
return {
|
|
||||||
'url': cls.get_url(obj),
|
|
||||||
'label': cls.label,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class AddObject(ObjectAction):
|
|
||||||
"""
|
|
||||||
Create a new object.
|
|
||||||
"""
|
|
||||||
name = 'add'
|
|
||||||
label = _('Add')
|
|
||||||
permissions_required = {'add'}
|
|
||||||
template_name = 'buttons/add.html'
|
|
||||||
|
|
||||||
|
|
||||||
class CloneObject(ObjectAction):
|
|
||||||
"""
|
|
||||||
Populate the new object form with select details from an existing object.
|
|
||||||
"""
|
|
||||||
name = 'add'
|
|
||||||
label = _('Clone')
|
|
||||||
permissions_required = {'add'}
|
|
||||||
template_name = 'buttons/clone.html'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_context(cls, context, obj):
|
|
||||||
param_string = prepare_cloned_fields(obj).urlencode()
|
|
||||||
url = f'{cls.get_url(obj)}?{param_string}' if param_string else None
|
|
||||||
return {
|
|
||||||
'url': url,
|
|
||||||
'label': cls.label,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class EditObject(ObjectAction):
|
|
||||||
"""
|
|
||||||
Edit a single object.
|
|
||||||
"""
|
|
||||||
name = 'edit'
|
|
||||||
label = _('Edit')
|
|
||||||
permissions_required = {'change'}
|
|
||||||
url_kwargs = ['pk']
|
|
||||||
template_name = 'buttons/edit.html'
|
|
||||||
|
|
||||||
|
|
||||||
class DeleteObject(ObjectAction):
|
|
||||||
"""
|
|
||||||
Delete a single object.
|
|
||||||
"""
|
|
||||||
name = 'delete'
|
|
||||||
label = _('Delete')
|
|
||||||
permissions_required = {'delete'}
|
|
||||||
url_kwargs = ['pk']
|
|
||||||
template_name = 'buttons/delete.html'
|
|
||||||
|
|
||||||
|
|
||||||
class BulkImport(ObjectAction):
|
|
||||||
"""
|
|
||||||
Import multiple objects at once.
|
|
||||||
"""
|
|
||||||
name = 'bulk_import'
|
|
||||||
label = _('Import')
|
|
||||||
permissions_required = {'add'}
|
|
||||||
template_name = 'buttons/import.html'
|
|
||||||
|
|
||||||
|
|
||||||
class BulkExport(ObjectAction):
|
|
||||||
"""
|
|
||||||
Export multiple objects at once.
|
|
||||||
"""
|
|
||||||
name = 'export'
|
|
||||||
label = _('Export')
|
|
||||||
permissions_required = {'view'}
|
|
||||||
template_name = 'buttons/export.html'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_context(cls, context, model):
|
|
||||||
object_type = ObjectType.objects.get_for_model(model)
|
|
||||||
user = context['request'].user
|
|
||||||
|
|
||||||
# Determine if the "all data" export returns CSV or YAML
|
|
||||||
data_format = 'YAML' if hasattr(object_type.model_class(), 'to_yaml') else 'CSV'
|
|
||||||
|
|
||||||
# Retrieve all export templates for this model
|
|
||||||
export_templates = ExportTemplate.objects.restrict(user, 'view').filter(object_types=object_type)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'label': cls.label,
|
|
||||||
'perms': context['perms'],
|
|
||||||
'object_type': object_type,
|
|
||||||
'url_params': context['request'].GET.urlencode() if context['request'].GET else '',
|
|
||||||
'export_templates': export_templates,
|
|
||||||
'data_format': data_format,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class BulkEdit(ObjectAction):
|
|
||||||
"""
|
|
||||||
Change the value of one or more fields on a set of objects.
|
|
||||||
"""
|
|
||||||
name = 'bulk_edit'
|
|
||||||
label = _('Edit Selected')
|
|
||||||
multi = True
|
|
||||||
permissions_required = {'change'}
|
|
||||||
template_name = 'buttons/bulk_edit.html'
|
|
||||||
|
|
||||||
|
|
||||||
class BulkRename(ObjectAction):
|
|
||||||
"""
|
|
||||||
Rename multiple objects at once.
|
|
||||||
"""
|
|
||||||
name = 'bulk_rename'
|
|
||||||
label = _('Rename Selected')
|
|
||||||
multi = True
|
|
||||||
permissions_required = {'change'}
|
|
||||||
template_name = 'buttons/bulk_rename.html'
|
|
||||||
|
|
||||||
|
|
||||||
class BulkDelete(ObjectAction):
|
|
||||||
"""
|
|
||||||
Delete each of a set of objects.
|
|
||||||
"""
|
|
||||||
name = 'bulk_delete'
|
|
||||||
label = _('Delete Selected')
|
|
||||||
multi = True
|
|
||||||
permissions_required = {'delete'}
|
|
||||||
template_name = 'buttons/bulk_delete.html'
|
|
||||||
@@ -72,16 +72,6 @@ PREFERENCES = {
|
|||||||
),
|
),
|
||||||
description=_('The preferred syntax for displaying generic data within the UI')
|
description=_('The preferred syntax for displaying generic data within the UI')
|
||||||
),
|
),
|
||||||
'csv_delimiter': UserPreference(
|
|
||||||
label=_('CSV delimiter'),
|
|
||||||
choices=(
|
|
||||||
('comma', 'Comma (,)'),
|
|
||||||
('semicolon', 'Semicolon (;)'),
|
|
||||||
('pipe', 'Pipe (|)'),
|
|
||||||
),
|
|
||||||
default='comma',
|
|
||||||
description=_('The character used to separate fields in CSV data')
|
|
||||||
),
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from utilities.string import trailing_slash
|
|||||||
|
|
||||||
RELEASE = load_release_data()
|
RELEASE = load_release_data()
|
||||||
VERSION = RELEASE.full_version # Retained for backward compatibility
|
VERSION = RELEASE.full_version # Retained for backward compatibility
|
||||||
|
HOSTNAME = platform.node()
|
||||||
# Set the base directory two levels up
|
# Set the base directory two levels up
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
@@ -124,7 +125,6 @@ EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
|
|||||||
FIELD_CHOICES = getattr(configuration, 'FIELD_CHOICES', {})
|
FIELD_CHOICES = getattr(configuration, 'FIELD_CHOICES', {})
|
||||||
FILE_UPLOAD_MAX_MEMORY_SIZE = getattr(configuration, 'FILE_UPLOAD_MAX_MEMORY_SIZE', 2621440)
|
FILE_UPLOAD_MAX_MEMORY_SIZE = getattr(configuration, 'FILE_UPLOAD_MAX_MEMORY_SIZE', 2621440)
|
||||||
GRAPHQL_MAX_ALIASES = getattr(configuration, 'GRAPHQL_MAX_ALIASES', 10)
|
GRAPHQL_MAX_ALIASES = getattr(configuration, 'GRAPHQL_MAX_ALIASES', 10)
|
||||||
HOSTNAME = getattr(configuration, 'HOSTNAME', platform.node())
|
|
||||||
HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', {})
|
HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', {})
|
||||||
INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1'))
|
INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1'))
|
||||||
ISOLATED_DEPLOYMENT = getattr(configuration, 'ISOLATED_DEPLOYMENT', False)
|
ISOLATED_DEPLOYMENT = getattr(configuration, 'ISOLATED_DEPLOYMENT', False)
|
||||||
|
|||||||
@@ -16,10 +16,6 @@ class TestJobRunner(JobRunner):
|
|||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
if kwargs.get('make_fail', False):
|
if kwargs.get('make_fail', False):
|
||||||
raise JobFailed()
|
raise JobFailed()
|
||||||
self.logger.debug("Debug message")
|
|
||||||
self.logger.info("Info message")
|
|
||||||
self.logger.warning("Warning message")
|
|
||||||
self.logger.error("Error message")
|
|
||||||
|
|
||||||
|
|
||||||
class JobRunnerTestCase(TestCase):
|
class JobRunnerTestCase(TestCase):
|
||||||
@@ -55,16 +51,8 @@ class JobRunnerTest(JobRunnerTestCase):
|
|||||||
def test_handle(self):
|
def test_handle(self):
|
||||||
job = TestJobRunner.enqueue(immediate=True)
|
job = TestJobRunner.enqueue(immediate=True)
|
||||||
|
|
||||||
# Check job status
|
|
||||||
self.assertEqual(job.status, JobStatusChoices.STATUS_COMPLETED)
|
self.assertEqual(job.status, JobStatusChoices.STATUS_COMPLETED)
|
||||||
|
|
||||||
# Check logging
|
|
||||||
self.assertEqual(len(job.log_entries), 4)
|
|
||||||
self.assertEqual(job.log_entries[0]['message'], "Debug message")
|
|
||||||
self.assertEqual(job.log_entries[1]['message'], "Info message")
|
|
||||||
self.assertEqual(job.log_entries[2]['message'], "Warning message")
|
|
||||||
self.assertEqual(job.log_entries[3]['message'], "Error message")
|
|
||||||
|
|
||||||
def test_handle_failed(self):
|
def test_handle_failed(self):
|
||||||
with disable_warnings('netbox.jobs'):
|
with disable_warnings('netbox.jobs'):
|
||||||
job = TestJobRunner.enqueue(immediate=True, make_fail=True)
|
job = TestJobRunner.enqueue(immediate=True, make_fail=True)
|
||||||
|
|||||||
@@ -15,21 +15,18 @@ from django.shortcuts import get_object_or_404, redirect, render
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from django_tables2.export import TableExport
|
||||||
from mptt.models import MPTTModel
|
from mptt.models import MPTTModel
|
||||||
|
|
||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
from core.signals import clear_events
|
from core.signals import clear_events
|
||||||
from extras.choices import CustomFieldUIEditableChoices
|
from extras.choices import CustomFieldUIEditableChoices
|
||||||
from extras.models import CustomField, ExportTemplate
|
from extras.models import CustomField, ExportTemplate
|
||||||
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename
|
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
|
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
|
||||||
from utilities.export import TableExport
|
|
||||||
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
|
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
|
||||||
from utilities.forms.bulk_import import BulkImportForm
|
from utilities.forms.bulk_import import BulkImportForm
|
||||||
from utilities.forms.mixins import BackgroundJobMixin
|
|
||||||
from utilities.htmx import htmx_partial
|
from utilities.htmx import htmx_partial
|
||||||
from utilities.jobs import AsyncJobData, is_background_request, process_request_as_job
|
|
||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
from utilities.query import reapply_model_ordering
|
from utilities.query import reapply_model_ordering
|
||||||
from utilities.request import safe_for_redirect
|
from utilities.request import safe_for_redirect
|
||||||
@@ -57,12 +54,12 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
|||||||
Attributes:
|
Attributes:
|
||||||
filterset: A django-filter FilterSet that is applied to the queryset
|
filterset: A django-filter FilterSet that is applied to the queryset
|
||||||
filterset_form: The form class used to render filter options
|
filterset_form: The form class used to render filter options
|
||||||
actions: An iterable of ObjectAction subclasses (see ActionsMixin)
|
actions: A mapping of supported actions to their required permissions. When adding custom actions, bulk
|
||||||
|
action names must be prefixed with `bulk_`. (See ActionsMixin.)
|
||||||
"""
|
"""
|
||||||
template_name = 'generic/object_list.html'
|
template_name = 'generic/object_list.html'
|
||||||
filterset = None
|
filterset = None
|
||||||
filterset_form = None
|
filterset_form = None
|
||||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkRename, BulkDelete)
|
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return get_permission_for_model(self.queryset.model, 'view')
|
return get_permission_for_model(self.queryset.model, 'view')
|
||||||
@@ -79,7 +76,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
|||||||
|
|
||||||
return '---\n'.join(yaml_data)
|
return '---\n'.join(yaml_data)
|
||||||
|
|
||||||
def export_table(self, table, columns=None, filename=None, delimiter=None):
|
def export_table(self, table, columns=None, filename=None):
|
||||||
"""
|
"""
|
||||||
Export all table data in CSV format.
|
Export all table data in CSV format.
|
||||||
|
|
||||||
@@ -88,7 +85,6 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
|||||||
columns: A list of specific columns to include. If None, all columns will be exported.
|
columns: A list of specific columns to include. If None, all columns will be exported.
|
||||||
filename: The name of the file attachment sent to the client. If None, will be determined automatically
|
filename: The name of the file attachment sent to the client. If None, will be determined automatically
|
||||||
from the queryset model name.
|
from the queryset model name.
|
||||||
delimiter: The character used to separate columns (a comma is used by default)
|
|
||||||
"""
|
"""
|
||||||
exclude_columns = {'pk', 'actions'}
|
exclude_columns = {'pk', 'actions'}
|
||||||
if columns:
|
if columns:
|
||||||
@@ -99,8 +95,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
|||||||
exporter = TableExport(
|
exporter = TableExport(
|
||||||
export_format=TableExport.CSV,
|
export_format=TableExport.CSV,
|
||||||
table=table,
|
table=table,
|
||||||
exclude_columns=exclude_columns,
|
exclude_columns=exclude_columns
|
||||||
delimiter=delimiter,
|
|
||||||
)
|
)
|
||||||
return exporter.response(
|
return exporter.response(
|
||||||
filename=filename or f'netbox_{self.queryset.model._meta.verbose_name_plural}.csv'
|
filename=filename or f'netbox_{self.queryset.model._meta.verbose_name_plural}.csv'
|
||||||
@@ -155,16 +150,15 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
|||||||
|
|
||||||
# Determine the available actions
|
# Determine the available actions
|
||||||
actions = self.get_permitted_actions(request.user)
|
actions = self.get_permitted_actions(request.user)
|
||||||
has_table_actions = any(action.multi for action in actions)
|
has_bulk_actions = any([a.startswith('bulk_') for a in actions])
|
||||||
|
|
||||||
if 'export' in request.GET:
|
if 'export' in request.GET:
|
||||||
|
|
||||||
# Export the current table view
|
# Export the current table view
|
||||||
if request.GET['export'] == 'table':
|
if request.GET['export'] == 'table':
|
||||||
table = self.get_table(self.queryset, request, has_table_actions)
|
table = self.get_table(self.queryset, request, has_bulk_actions)
|
||||||
columns = [name for name, _ in table.selected_columns]
|
columns = [name for name, _ in table.selected_columns]
|
||||||
delimiter = request.user.config.get('csv_delimiter')
|
return self.export_table(table, columns)
|
||||||
return self.export_table(table, columns, delimiter=delimiter)
|
|
||||||
|
|
||||||
# Render an ExportTemplate
|
# Render an ExportTemplate
|
||||||
elif request.GET['export']:
|
elif request.GET['export']:
|
||||||
@@ -180,12 +174,11 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
|||||||
|
|
||||||
# Fall back to default table/YAML export
|
# Fall back to default table/YAML export
|
||||||
else:
|
else:
|
||||||
table = self.get_table(self.queryset, request, has_table_actions)
|
table = self.get_table(self.queryset, request, has_bulk_actions)
|
||||||
delimiter = request.user.config.get('csv_delimiter')
|
return self.export_table(table)
|
||||||
return self.export_table(table, delimiter=delimiter)
|
|
||||||
|
|
||||||
# Render the objects table
|
# Render the objects table
|
||||||
table = self.get_table(self.queryset, request, has_table_actions)
|
table = self.get_table(self.queryset, request, has_bulk_actions)
|
||||||
|
|
||||||
# If this is an HTMX request, return only the rendered table HTML
|
# If this is an HTMX request, return only the rendered table HTML
|
||||||
if htmx_partial(request):
|
if htmx_partial(request):
|
||||||
@@ -505,27 +498,25 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
logger.debug("Import form validation was successful")
|
logger.debug("Import form validation was successful")
|
||||||
redirect_url = reverse(get_viewname(model, action='list'))
|
|
||||||
new_objects = []
|
|
||||||
|
|
||||||
# If indicated, defer this request to a background job & redirect the user
|
|
||||||
if form.cleaned_data['background_job']:
|
|
||||||
job_name = _('Bulk import {count} {object_type}').format(
|
|
||||||
count=len(form.cleaned_data['data']),
|
|
||||||
object_type=model._meta.verbose_name_plural,
|
|
||||||
)
|
|
||||||
if process_request_as_job(self.__class__, request, name=job_name):
|
|
||||||
return redirect(redirect_url)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Iterate through data and bind each record to a new model form instance.
|
# Iterate through data and bind each record to a new model form instance.
|
||||||
with transaction.atomic(using=router.db_for_write(model)):
|
with transaction.atomic(using=router.db_for_write(model)):
|
||||||
new_objects = self.create_and_update_objects(form, request)
|
new_objs = self.create_and_update_objects(form, request)
|
||||||
|
|
||||||
# Enforce object-level permissions
|
# Enforce object-level permissions
|
||||||
if self.queryset.filter(pk__in=[obj.pk for obj in new_objects]).count() != len(new_objects):
|
if self.queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs):
|
||||||
raise PermissionsViolation
|
raise PermissionsViolation
|
||||||
|
|
||||||
|
if new_objs:
|
||||||
|
msg = f"Imported {len(new_objs)} {model._meta.verbose_name_plural}"
|
||||||
|
logger.info(msg)
|
||||||
|
messages.success(request, msg)
|
||||||
|
|
||||||
|
view_name = get_viewname(model, action='list')
|
||||||
|
results_url = f"{reverse(view_name)}?modified_by_request={request.id}"
|
||||||
|
return redirect(results_url)
|
||||||
|
|
||||||
except (AbortTransaction, ValidationError):
|
except (AbortTransaction, ValidationError):
|
||||||
clear_events.send(sender=self)
|
clear_events.send(sender=self)
|
||||||
|
|
||||||
@@ -534,25 +525,6 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
form.add_error(None, e.message)
|
form.add_error(None, e.message)
|
||||||
clear_events.send(sender=self)
|
clear_events.send(sender=self)
|
||||||
|
|
||||||
# If this request was executed via a background job, return the raw data for logging
|
|
||||||
if is_background_request(request):
|
|
||||||
return AsyncJobData(
|
|
||||||
log=[
|
|
||||||
_('Created {object}').format(object=str(obj))
|
|
||||||
for obj in new_objects
|
|
||||||
],
|
|
||||||
errors=form.errors
|
|
||||||
)
|
|
||||||
|
|
||||||
if new_objects:
|
|
||||||
msg = _("Imported {count} {object_type}").format(
|
|
||||||
count=len(new_objects),
|
|
||||||
object_type=model._meta.verbose_name_plural
|
|
||||||
)
|
|
||||||
logger.info(msg)
|
|
||||||
messages.success(request, msg)
|
|
||||||
return redirect(f"{redirect_url}?modified_by_request={request.id}")
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.debug("Form validation failed")
|
logger.debug("Form validation failed")
|
||||||
|
|
||||||
@@ -708,16 +680,6 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
if '_apply' in request.POST:
|
if '_apply' in request.POST:
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
logger.debug("Form validation was successful")
|
logger.debug("Form validation was successful")
|
||||||
|
|
||||||
# If indicated, defer this request to a background job & redirect the user
|
|
||||||
if form.cleaned_data['background_job']:
|
|
||||||
job_name = _('Bulk edit {count} {object_type}').format(
|
|
||||||
count=len(form.cleaned_data['pk']),
|
|
||||||
object_type=model._meta.verbose_name_plural,
|
|
||||||
)
|
|
||||||
if process_request_as_job(self.__class__, request, name=job_name):
|
|
||||||
return redirect(self.get_return_url(request))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with transaction.atomic(using=router.db_for_write(model)):
|
with transaction.atomic(using=router.db_for_write(model)):
|
||||||
updated_objects = self._update_objects(form, request)
|
updated_objects = self._update_objects(form, request)
|
||||||
@@ -727,16 +689,6 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
if object_count != len(updated_objects):
|
if object_count != len(updated_objects):
|
||||||
raise PermissionsViolation
|
raise PermissionsViolation
|
||||||
|
|
||||||
# If this request was executed via a background job, return the raw data for logging
|
|
||||||
if is_background_request(request):
|
|
||||||
return AsyncJobData(
|
|
||||||
log=[
|
|
||||||
_('Updated {object}').format(object=str(obj))
|
|
||||||
for obj in updated_objects
|
|
||||||
],
|
|
||||||
errors=form.errors
|
|
||||||
)
|
|
||||||
|
|
||||||
if updated_objects:
|
if updated_objects:
|
||||||
msg = f'Updated {len(updated_objects)} {model._meta.verbose_name_plural}'
|
msg = f'Updated {len(updated_objects)} {model._meta.verbose_name_plural}'
|
||||||
logger.info(msg)
|
logger.info(msg)
|
||||||
@@ -777,11 +729,7 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
|
class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
|
||||||
"""
|
"""
|
||||||
An extendable view for renaming objects in bulk.
|
An extendable view for renaming objects in bulk.
|
||||||
|
|
||||||
Attributes:
|
|
||||||
field_name: The name of the object attribute for which the value is being updated (defaults to "name")
|
|
||||||
"""
|
"""
|
||||||
field_name = 'name'
|
|
||||||
template_name = 'generic/bulk_rename.html'
|
template_name = 'generic/bulk_rename.html'
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -811,12 +759,12 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
replace = form.cleaned_data['replace']
|
replace = form.cleaned_data['replace']
|
||||||
if form.cleaned_data['use_regex']:
|
if form.cleaned_data['use_regex']:
|
||||||
try:
|
try:
|
||||||
obj.new_name = re.sub(find, replace, getattr(obj, self.field_name, ''))
|
obj.new_name = re.sub(find, replace, obj.name or '')
|
||||||
# Catch regex group reference errors
|
# Catch regex group reference errors
|
||||||
except re.error:
|
except re.error:
|
||||||
obj.new_name = getattr(obj, self.field_name)
|
obj.new_name = obj.name
|
||||||
else:
|
else:
|
||||||
obj.new_name = getattr(obj, self.field_name, '').replace(find, replace)
|
obj.new_name = (obj.name or '').replace(find, replace)
|
||||||
renamed_pks.append(obj.pk)
|
renamed_pks.append(obj.pk)
|
||||||
|
|
||||||
return renamed_pks
|
return renamed_pks
|
||||||
@@ -835,7 +783,7 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
|
|
||||||
if '_apply' in request.POST:
|
if '_apply' in request.POST:
|
||||||
for obj in selected_objects:
|
for obj in selected_objects:
|
||||||
setattr(obj, self.field_name, obj.new_name)
|
obj.name = obj.new_name
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
# Enforce constrained permissions
|
# Enforce constrained permissions
|
||||||
@@ -865,7 +813,6 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
|
selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'field_name': self.field_name,
|
|
||||||
'form': form,
|
'form': form,
|
||||||
'obj_type_plural': self.queryset.model._meta.verbose_name_plural,
|
'obj_type_plural': self.queryset.model._meta.verbose_name_plural,
|
||||||
'selected_objects': selected_objects,
|
'selected_objects': selected_objects,
|
||||||
@@ -892,7 +839,7 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
"""
|
"""
|
||||||
Provide a standard bulk delete form if none has been specified for the view
|
Provide a standard bulk delete form if none has been specified for the view
|
||||||
"""
|
"""
|
||||||
class BulkDeleteForm(BackgroundJobMixin, ConfirmationForm):
|
class BulkDeleteForm(ConfirmationForm):
|
||||||
pk = ModelMultipleChoiceField(queryset=self.queryset, widget=MultipleHiddenInput)
|
pk = ModelMultipleChoiceField(queryset=self.queryset, widget=MultipleHiddenInput)
|
||||||
|
|
||||||
return BulkDeleteForm
|
return BulkDeleteForm
|
||||||
@@ -924,15 +871,6 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
logger.debug("Form validation was successful")
|
logger.debug("Form validation was successful")
|
||||||
|
|
||||||
# If indicated, defer this request to a background job & redirect the user
|
|
||||||
if form.cleaned_data['background_job']:
|
|
||||||
job_name = _('Bulk delete {count} {object_type}').format(
|
|
||||||
count=len(form.cleaned_data['pk']),
|
|
||||||
object_type=model._meta.verbose_name_plural,
|
|
||||||
)
|
|
||||||
if process_request_as_job(self.__class__, request, name=job_name):
|
|
||||||
return redirect(self.get_return_url(request))
|
|
||||||
|
|
||||||
# Delete objects
|
# Delete objects
|
||||||
queryset = self.queryset.filter(pk__in=pk_list)
|
queryset = self.queryset.filter(pk__in=pk_list)
|
||||||
deleted_count = queryset.count()
|
deleted_count = queryset.count()
|
||||||
@@ -954,16 +892,6 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
messages.error(request, mark_safe(e.message))
|
messages.error(request, mark_safe(e.message))
|
||||||
return redirect(self.get_return_url(request))
|
return redirect(self.get_return_url(request))
|
||||||
|
|
||||||
# If this request was executed via a background job, return the raw data for logging
|
|
||||||
if is_background_request(request):
|
|
||||||
return AsyncJobData(
|
|
||||||
log=[
|
|
||||||
_('Deleted {object}').format(object=str(obj))
|
|
||||||
for obj in queryset
|
|
||||||
],
|
|
||||||
errors=form.errors
|
|
||||||
)
|
|
||||||
|
|
||||||
msg = _("Deleted {count} {object_type}").format(
|
msg = _("Deleted {count} {object_type}").format(
|
||||||
count=deleted_count,
|
count=deleted_count,
|
||||||
object_type=model._meta.verbose_name_plural
|
object_type=model._meta.verbose_name_plural
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from extras.models import TableConfig
|
from extras.models import TableConfig
|
||||||
from netbox import object_actions
|
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@@ -9,18 +9,6 @@ __all__ = (
|
|||||||
'TableMixin',
|
'TableMixin',
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: Remove in NetBox v4.5
|
|
||||||
LEGACY_ACTIONS = {
|
|
||||||
'add': object_actions.AddObject,
|
|
||||||
'edit': object_actions.EditObject,
|
|
||||||
'delete': object_actions.DeleteObject,
|
|
||||||
'export': object_actions.BulkExport,
|
|
||||||
'bulk_import': object_actions.BulkImport,
|
|
||||||
'bulk_edit': object_actions.BulkEdit,
|
|
||||||
'bulk_rename': object_actions.BulkRename,
|
|
||||||
'bulk_delete': object_actions.BulkDelete,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ActionsMixin:
|
class ActionsMixin:
|
||||||
"""
|
"""
|
||||||
@@ -31,24 +19,7 @@ class ActionsMixin:
|
|||||||
Standard actions include: add, import, export, bulk_edit, and bulk_delete. Some views extend this default map
|
Standard actions include: add, import, export, bulk_edit, and bulk_delete. Some views extend this default map
|
||||||
with custom actions, such as bulk_sync.
|
with custom actions, such as bulk_sync.
|
||||||
"""
|
"""
|
||||||
actions = tuple()
|
actions = DEFAULT_ACTION_PERMISSIONS
|
||||||
|
|
||||||
# TODO: Remove in NetBox v4.5
|
|
||||||
def _convert_legacy_actions(self):
|
|
||||||
"""
|
|
||||||
Convert a legacy dictionary mapping action name to required permissions to a list of ObjectAction subclasses.
|
|
||||||
"""
|
|
||||||
if type(self.actions) is not dict:
|
|
||||||
return
|
|
||||||
|
|
||||||
actions = []
|
|
||||||
for name in self.actions.keys():
|
|
||||||
try:
|
|
||||||
actions.append(LEGACY_ACTIONS[name])
|
|
||||||
except KeyError:
|
|
||||||
raise ValueError(f"Unsupported legacy action: {name}")
|
|
||||||
|
|
||||||
self.actions = actions
|
|
||||||
|
|
||||||
def get_permitted_actions(self, user, model=None):
|
def get_permitted_actions(self, user, model=None):
|
||||||
"""
|
"""
|
||||||
@@ -56,15 +27,11 @@ class ActionsMixin:
|
|||||||
"""
|
"""
|
||||||
model = model or self.queryset.model
|
model = model or self.queryset.model
|
||||||
|
|
||||||
# TODO: Remove in NetBox v4.5
|
|
||||||
# Handle legacy action sets
|
|
||||||
self._convert_legacy_actions()
|
|
||||||
|
|
||||||
# Resolve required permissions for each action
|
# Resolve required permissions for each action
|
||||||
permitted_actions = []
|
permitted_actions = []
|
||||||
for action in self.actions:
|
for action in self.actions:
|
||||||
required_permissions = [
|
required_permissions = [
|
||||||
get_permission_for_model(model, perm) for perm in action.permissions_required
|
get_permission_for_model(model, name) for name in self.actions.get(action, set())
|
||||||
]
|
]
|
||||||
if not required_permissions or user.has_perms(required_permissions):
|
if not required_permissions or user.has_perms(required_permissions):
|
||||||
permitted_actions.append(action)
|
permitted_actions.append(action)
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ from django.utils.safestring import mark_safe
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from core.signals import clear_events
|
from core.signals import clear_events
|
||||||
from netbox.object_actions import (
|
|
||||||
AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, CloneObject, DeleteObject, EditObject,
|
|
||||||
)
|
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
from utilities.exceptions import AbortRequest, PermissionsViolation
|
from utilities.exceptions import AbortRequest, PermissionsViolation
|
||||||
from utilities.forms import ConfirmationForm, restrict_form_fields
|
from utilities.forms import ConfirmationForm, restrict_form_fields
|
||||||
@@ -39,7 +36,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ObjectView(ActionsMixin, BaseObjectView):
|
class ObjectView(BaseObjectView):
|
||||||
"""
|
"""
|
||||||
Retrieve a single object for display.
|
Retrieve a single object for display.
|
||||||
|
|
||||||
@@ -47,10 +44,8 @@ class ObjectView(ActionsMixin, BaseObjectView):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
tab: A ViewTab instance for the view
|
tab: A ViewTab instance for the view
|
||||||
actions: An iterable of ObjectAction subclasses (see ActionsMixin)
|
|
||||||
"""
|
"""
|
||||||
tab = None
|
tab = None
|
||||||
actions = (CloneObject, EditObject, DeleteObject)
|
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return get_permission_for_model(self.queryset.model, 'view')
|
return get_permission_for_model(self.queryset.model, 'view')
|
||||||
@@ -77,11 +72,9 @@ class ObjectView(ActionsMixin, BaseObjectView):
|
|||||||
request: The current request
|
request: The current request
|
||||||
"""
|
"""
|
||||||
instance = self.get_object(**kwargs)
|
instance = self.get_object(**kwargs)
|
||||||
actions = self.get_permitted_actions(request.user, model=instance)
|
|
||||||
|
|
||||||
return render(request, self.get_template_name(), {
|
return render(request, self.get_template_name(), {
|
||||||
'object': instance,
|
'object': instance,
|
||||||
'actions': actions,
|
|
||||||
'tab': self.tab,
|
'tab': self.tab,
|
||||||
**self.get_extra_context(request, instance),
|
**self.get_extra_context(request, instance),
|
||||||
})
|
})
|
||||||
@@ -97,13 +90,13 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
|
|||||||
table: The django-tables2 Table class used to render the child objects list
|
table: The django-tables2 Table class used to render the child objects list
|
||||||
filterset: A django-filter FilterSet that is applied to the queryset
|
filterset: A django-filter FilterSet that is applied to the queryset
|
||||||
filterset_form: The form class used to render filter options
|
filterset_form: The form class used to render filter options
|
||||||
actions: An iterable of ObjectAction subclasses (see ActionsMixin)
|
actions: A mapping of supported actions to their required permissions. When adding custom actions, bulk
|
||||||
|
action names must be prefixed with `bulk_`. (See ActionsMixin.)
|
||||||
"""
|
"""
|
||||||
child_model = None
|
child_model = None
|
||||||
table = None
|
table = None
|
||||||
filterset = None
|
filterset = None
|
||||||
filterset_form = None
|
filterset_form = None
|
||||||
actions = (AddObject, BulkImport, BulkEdit, BulkExport, BulkDelete)
|
|
||||||
template_name = 'generic/object_children.html'
|
template_name = 'generic/object_children.html'
|
||||||
|
|
||||||
def get_children(self, request, parent):
|
def get_children(self, request, parent):
|
||||||
@@ -145,10 +138,10 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
|
|||||||
|
|
||||||
# Determine the available actions
|
# Determine the available actions
|
||||||
actions = self.get_permitted_actions(request.user, model=self.child_model)
|
actions = self.get_permitted_actions(request.user, model=self.child_model)
|
||||||
has_table_actions = any(action.multi for action in actions)
|
has_bulk_actions = any([a.startswith('bulk_') for a in actions])
|
||||||
|
|
||||||
table_data = self.prep_table_data(request, child_objects, instance)
|
table_data = self.prep_table_data(request, child_objects, instance)
|
||||||
table = self.get_table(table_data, request, has_table_actions)
|
table = self.get_table(table_data, request, has_bulk_actions)
|
||||||
|
|
||||||
# If this is an HTMX request, return only the rendered table HTML
|
# If this is an HTMX request, return only the rendered table HTML
|
||||||
if htmx_partial(request):
|
if htmx_partial(request):
|
||||||
|
|||||||
2
netbox/project-static/dist/netbox.js
vendored
2
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
6
netbox/project-static/dist/netbox.js.map
vendored
6
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -35,7 +35,7 @@ function showRackElements(
|
|||||||
selector: string,
|
selector: string,
|
||||||
elevation: HTMLObjectElement,
|
elevation: HTMLObjectElement,
|
||||||
): void {
|
): void {
|
||||||
const elements = elevation.contentDocument?.querySelectorAll(selector) ?? [];
|
const elements = elevation.querySelectorAll(selector) ?? [];
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
element.classList.remove('hidden');
|
element.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ function hideRackElements(
|
|||||||
selector: string,
|
selector: string,
|
||||||
elevation: HTMLObjectElement,
|
elevation: HTMLObjectElement,
|
||||||
): void {
|
): void {
|
||||||
const elements = elevation.contentDocument?.querySelectorAll(selector) ?? [];
|
const elements = elevation.querySelectorAll(selector) ?? [];
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
element.classList.add('hidden');
|
element.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<button type="submit" name="_sync" {% formaction %}="{{ url }}" class="btn btn-primary">
|
|
||||||
<i class="mdi mdi-sync" aria-hidden="true"></i> {{ label }}
|
|
||||||
</button>
|
|
||||||
@@ -11,6 +11,12 @@
|
|||||||
<li class="breadcrumb-item"><a href="{% url 'core:datafile_list' %}?source_id={{ object.source.pk }}">{{ object.source }}</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'core:datafile_list' %}?source_id={{ object.source.pk }}">{{ object.source }}</a></li>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block control-buttons %}
|
||||||
|
{% if request.user|can_delete:object %}
|
||||||
|
{% delete_button object %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock control-buttons %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|||||||
@@ -1,6 +1,33 @@
|
|||||||
{% extends 'core/job/base.html' %}
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load perms %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if object.object %}
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ object.object|meta:"verbose_name_plural"|bettertitle }}</a>
|
||||||
|
</li>
|
||||||
|
{% with parent_jobs_viewname=object.object|viewname:"jobs" %}
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url parent_jobs_viewname pk=object.object.pk %}">{{ object.object }}</a>
|
||||||
|
</li>
|
||||||
|
{% endwith %}
|
||||||
|
{% else %}
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'core:job_list' %}?name={{ object.name|urlencode }}">{{ object.name }}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock breadcrumbs %}
|
||||||
|
|
||||||
|
{% block control-buttons %}
|
||||||
|
{% if request.user|can_delete:object %}
|
||||||
|
{% delete_button object %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock control-buttons %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col col-12 col-md-6">
|
<div class="col col-12 col-md-6">
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
{% extends 'generic/object.html' %}
|
|
||||||
{% load buttons %}
|
|
||||||
{% load helpers %}
|
|
||||||
{% load perms %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
|
||||||
{{ block.super }}
|
|
||||||
{% if object.object %}
|
|
||||||
<li class="breadcrumb-item">
|
|
||||||
<a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ object.object|meta:"verbose_name_plural"|bettertitle }}</a>
|
|
||||||
</li>
|
|
||||||
{% with parent_jobs_viewname=object.object|viewname:"jobs" %}
|
|
||||||
<li class="breadcrumb-item">
|
|
||||||
<a href="{% url parent_jobs_viewname pk=object.object.pk %}">{{ object.object }}</a>
|
|
||||||
</li>
|
|
||||||
{% endwith %}
|
|
||||||
{% else %}
|
|
||||||
<li class="breadcrumb-item">
|
|
||||||
<a href="{% url 'core:job_list' %}?name={{ object.name|urlencode }}">{{ object.name }}</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock breadcrumbs %}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{% extends 'core/job/base.html' %}
|
|
||||||
{% load render_table from django_tables2 %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col">
|
|
||||||
<div class="card">
|
|
||||||
{% render_table table %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
{% load i18n %}
|
|
||||||
<div class="btn-group">
|
|
||||||
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
|
||||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {{ label }}
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
{% if perms.dcim.add_consoleport %}
|
|
||||||
<li>
|
|
||||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
|
||||||
{% trans "Console Ports" %}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.dcim.add_consoleserverport %}
|
|
||||||
<li>
|
|
||||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item ">
|
|
||||||
{% trans "Console Server Ports" %}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.dcim.add_powerport %}
|
|
||||||
<li>
|
|
||||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
|
||||||
{% trans "Power Ports" %}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.dcim.add_poweroutlet %}
|
|
||||||
<li>
|
|
||||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
|
||||||
{% trans "Power Outlets" %}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.dcim.add_interface %}
|
|
||||||
<li>
|
|
||||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
|
||||||
{% trans "Interfaces" %}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.dcim.add_rearport %}
|
|
||||||
<li>
|
|
||||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_rearport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
|
||||||
{% trans "Rear Ports" %}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.dcim.add_devicebay %}
|
|
||||||
<li>
|
|
||||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
|
||||||
{% trans "Device Bays" %}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.dcim.add_modulebay %}
|
|
||||||
<li>
|
|
||||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_modulebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
|
||||||
{% trans "Module Bays" %}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.dcim.add_inventoryitem %}
|
|
||||||
<li>
|
|
||||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_inventoryitem' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
|
||||||
{% trans "Inventory Items" %}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<button type="submit" name="_disconnect" {% formaction %}="{{ url }}" class="btn btn-red">
|
|
||||||
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i> {{ label }}
|
|
||||||
</button>
|
|
||||||
22
netbox/templates/dcim/component_list.html
Normal file
22
netbox/templates/dcim/component_list.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{% extends 'generic/object_list.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block bulk_buttons %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
{% if 'bulk_edit' in actions %}
|
||||||
|
{% bulk_edit_button model query_params=request.GET %}
|
||||||
|
{% endif %}
|
||||||
|
{% if 'bulk_rename' in actions %}
|
||||||
|
{% with bulk_rename_view=model|validated_viewname:"bulk_rename" %}
|
||||||
|
<button type="submit" name="_rename" {% formaction %}="{% url bulk_rename_view %}" class="btn btn-outline-warning btn-float">
|
||||||
|
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename Selected" %}
|
||||||
|
</button>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if 'bulk_delete' in actions %}
|
||||||
|
{% bulk_delete_button model query_params=request.GET %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
23
netbox/templates/dcim/device/components_base.html
Normal file
23
netbox/templates/dcim/device/components_base.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{% extends 'generic/object_children.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block bulk_edit_controls %}
|
||||||
|
{% 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">
|
||||||
|
<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"
|
||||||
|
{% formaction %}="{% url bulk_rename_view %}?return_url={{ return_url }}"
|
||||||
|
class="btn btn-outline-warning">
|
||||||
|
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock bulk_edit_controls %}
|
||||||
28
netbox/templates/dcim/device/consoleports.html
Normal file
28
netbox/templates/dcim/device/consoleports.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{% extends 'dcim/device/components_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block bulk_delete_controls %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||||
|
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||||
|
<button type="submit" name="_disconnect"
|
||||||
|
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||||
|
class="btn btn-outline-danger">
|
||||||
|
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock bulk_delete_controls %}
|
||||||
|
|
||||||
|
{% block bulk_extra_controls %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if perms.dcim.add_consoleport %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{% url 'dcim:consoleport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}"
|
||||||
|
class="btn btn-primary">
|
||||||
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Console Ports" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock bulk_extra_controls %}
|
||||||
28
netbox/templates/dcim/device/consoleserverports.html
Normal file
28
netbox/templates/dcim/device/consoleserverports.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{% extends 'dcim/device/components_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block bulk_delete_controls %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||||
|
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||||
|
<button type="submit" name="_disconnect"
|
||||||
|
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||||
|
class="btn btn-outline-danger">
|
||||||
|
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock bulk_delete_controls %}
|
||||||
|
|
||||||
|
{% block bulk_extra_controls %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if perms.dcim.add_consoleserverport %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{% url 'dcim:consoleserverport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}"
|
||||||
|
class="btn btn-primary">
|
||||||
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Console Server Ports" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock bulk_extra_controls %}
|
||||||
14
netbox/templates/dcim/device/devicebays.html
Normal file
14
netbox/templates/dcim/device/devicebays.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{% extends 'dcim/device/components_base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block bulk_extra_controls %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if perms.dcim.add_devicebay %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}"
|
||||||
|
class="btn btn-primary">
|
||||||
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Device Bays" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock bulk_extra_controls %}
|
||||||
28
netbox/templates/dcim/device/frontports.html
Normal file
28
netbox/templates/dcim/device/frontports.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{% extends 'dcim/device/components_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block bulk_delete_controls %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||||
|
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||||
|
<button type="submit" name="_disconnect"
|
||||||
|
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||||
|
class="btn btn-outline-danger">
|
||||||
|
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock bulk_delete_controls %}
|
||||||
|
|
||||||
|
{% block bulk_extra_controls %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if perms.dcim.add_frontport %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{% url 'dcim:frontport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}"
|
||||||
|
class="btn btn-primary">
|
||||||
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Front Ports" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock bulk_extra_controls %}
|
||||||
@@ -1,5 +1,30 @@
|
|||||||
{% extends 'generic/object_children.html' %}
|
{% extends 'dcim/device/components_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block table_controls %}
|
{% block table_controls %}
|
||||||
{% include 'dcim/device/inc/interface_table_controls.html' with table_modal="DeviceInterfaceTable_config" %}
|
{% include 'dcim/device/inc/interface_table_controls.html' with table_modal="DeviceInterfaceTable_config" %}
|
||||||
{% endblock table_controls %}
|
{% endblock table_controls %}
|
||||||
|
|
||||||
|
{% block bulk_delete_controls %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||||
|
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||||
|
<button type="submit" name="_disconnect"
|
||||||
|
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||||
|
class="btn btn-outline-danger">
|
||||||
|
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock bulk_delete_controls %}
|
||||||
|
|
||||||
|
{% block bulk_extra_controls %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if perms.dcim.add_interface %}
|
||||||
|
<a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
|
||||||
|
class="btn btn-primary">
|
||||||
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Interfaces" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock bulk_extra_controls %}
|
||||||
|
|||||||
14
netbox/templates/dcim/device/inventory.html
Normal file
14
netbox/templates/dcim/device/inventory.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{% extends 'dcim/device/components_base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block bulk_extra_controls %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if perms.dcim.add_inventoryitem %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{% url 'dcim:inventoryitem_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}"
|
||||||
|
class="btn btn-primary">
|
||||||
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Inventory Item" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock bulk_extra_controls %}
|
||||||
14
netbox/templates/dcim/device/modulebays.html
Normal file
14
netbox/templates/dcim/device/modulebays.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{% extends 'dcim/device/components_base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block bulk_extra_controls %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if perms.dcim.add_modulebay %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}"
|
||||||
|
class="btn btn-primary">
|
||||||
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Module Bays" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock bulk_extra_controls %}
|
||||||
28
netbox/templates/dcim/device/poweroutlets.html
Normal file
28
netbox/templates/dcim/device/poweroutlets.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{% extends 'dcim/device/components_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block bulk_delete_controls %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||||
|
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||||
|
<button type="submit" name="_disconnect"
|
||||||
|
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||||
|
class="btn btn-outline-danger">
|
||||||
|
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock bulk_delete_controls %}
|
||||||
|
|
||||||
|
{% block bulk_extra_controls %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if perms.dcim.add_poweroutlet %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{% url 'dcim:poweroutlet_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}"
|
||||||
|
class="btn btn-primary">
|
||||||
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Power Outlets" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock bulk_extra_controls %}
|
||||||
28
netbox/templates/dcim/device/powerports.html
Normal file
28
netbox/templates/dcim/device/powerports.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{% extends 'dcim/device/components_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block bulk_delete_controls %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||||
|
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||||
|
<button type="submit" name="_disconnect"
|
||||||
|
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||||
|
class="btn btn-outline-danger">
|
||||||
|
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock bulk_delete_controls %}
|
||||||
|
|
||||||
|
{% block bulk_extra_controls %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if perms.dcim.add_powerport %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{% url 'dcim:powerport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}"
|
||||||
|
class="btn btn-primary">
|
||||||
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Power Port" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock bulk_extra_controls %}
|
||||||
28
netbox/templates/dcim/device/rearports.html
Normal file
28
netbox/templates/dcim/device/rearports.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{% extends 'dcim/device/components_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block bulk_delete_controls %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||||
|
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||||
|
<button type="submit" name="_disconnect"
|
||||||
|
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||||
|
class="btn btn-outline-danger">
|
||||||
|
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock bulk_delete_controls %}
|
||||||
|
|
||||||
|
{% block bulk_extra_controls %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if perms.dcim.add_rearport %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{% url 'dcim:rearport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}"
|
||||||
|
class="btn btn-primary">
|
||||||
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Rear Ports" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock bulk_extra_controls %}
|
||||||
89
netbox/templates/dcim/device_list.html
Normal file
89
netbox/templates/dcim/device_list.html
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
{% extends 'generic/object_list.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block bulk_buttons %}
|
||||||
|
{% if perms.dcim.change_device %}
|
||||||
|
<div class="dropdown">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Components" %}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% if perms.dcim.add_consoleport %}
|
||||||
|
<li>
|
||||||
|
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||||
|
{% trans "Console Ports" %}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_consoleserverport %}
|
||||||
|
<li>
|
||||||
|
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item ">
|
||||||
|
{% trans "Console Server Ports" %}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_powerport %}
|
||||||
|
<li>
|
||||||
|
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||||
|
{% trans "Power Ports" %}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_poweroutlet %}
|
||||||
|
<li>
|
||||||
|
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||||
|
{% trans "Power Outlets" %}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_interface %}
|
||||||
|
<li>
|
||||||
|
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||||
|
{% trans "Interfaces" %}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_rearport %}
|
||||||
|
<li>
|
||||||
|
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_rearport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||||
|
{% trans "Rear Ports" %}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_devicebay %}
|
||||||
|
<li>
|
||||||
|
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||||
|
{% trans "Device Bays" %}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_modulebay %}
|
||||||
|
<li>
|
||||||
|
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_modulebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||||
|
{% trans "Module Bays" %}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_inventoryitem %}
|
||||||
|
<li>
|
||||||
|
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_inventoryitem' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||||
|
{% trans "Inventory Items" %}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if 'bulk_edit' in actions %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
{% bulk_edit_button model query_params=request.GET %}
|
||||||
|
<button type="submit" name="_rename" {% formaction %}="{% url 'dcim:device_bulk_rename' %}?return_url={% url 'dcim:device_list' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-outline-warning btn-float">
|
||||||
|
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if 'bulk_delete' in actions %}
|
||||||
|
{% bulk_delete_button model query_params=request.GET %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
25
netbox/templates/dcim/devicetype/component_templates.html
Normal file
25
netbox/templates/dcim/devicetype/component_templates.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{% extends 'generic/object_children.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load perms %}
|
||||||
|
|
||||||
|
{% block bulk_edit_controls %}
|
||||||
|
{% 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">
|
||||||
|
<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"
|
||||||
|
{% formaction %}="{% url bulk_rename_view %}?return_url={{ return_url }}"
|
||||||
|
class="btn btn-outline-warning">
|
||||||
|
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename Selected
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock bulk_edit_controls %}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div style="margin-left: -30px">
|
<div style="margin-left: -30px" class="rack_elevation">
|
||||||
<div
|
<div
|
||||||
hx-get="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{ face }}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}"
|
hx-get="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{ face }}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}"
|
||||||
hx-trigger="intersect"
|
hx-trigger="intersect"
|
||||||
|
|||||||
30
netbox/templates/dcim/moduletype/component_templates.html
Normal file
30
netbox/templates/dcim/moduletype/component_templates.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{% extends 'generic/object_children.html' %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block extra_controls %}
|
||||||
|
{% include 'dcim/inc/moduletype_buttons.html' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block bulk_edit_controls %}
|
||||||
|
{% 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">
|
||||||
|
<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"
|
||||||
|
{% formaction %}="{% url bulk_rename_view %}?return_url={{ return_url }}"
|
||||||
|
class="btn btn-outline-warning">
|
||||||
|
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename Selected
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock bulk_edit_controls %}
|
||||||
|
|
||||||
@@ -1,8 +1,18 @@
|
|||||||
{% extends 'generic/object.html' %}
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load buttons %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block buttons %}
|
||||||
|
{% if perms.dcim.change_virtualchassis %}
|
||||||
|
{% edit_button object %}
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.delete_virtualchassis %}
|
||||||
|
{% delete_button object %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-12 col-md-4">
|
<div class="col col-12 col-md-4">
|
||||||
|
|||||||
11
netbox/templates/extras/configcontext_list.html
Normal file
11
netbox/templates/extras/configcontext_list.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'generic/object_list.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block bulk_buttons %}
|
||||||
|
{% if perms.extras.sync_configcontext %}
|
||||||
|
<button type="submit" name="_sync" {% formaction %}="{% url 'extras:configcontext_bulk_sync' %}" class="btn btn-primary">
|
||||||
|
<i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync Data" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user