mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-13 07:12:16 -06:00
Compare commits
4 Commits
v4.3.4
...
ae3de95dce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae3de95dce | ||
|
|
a1cd81ff35 | ||
|
|
ce12de8b6d | ||
|
|
601a77ac73 |
@@ -15,7 +15,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.3.4
|
||||
placeholder: v4.3.3
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@@ -27,7 +27,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.3.4
|
||||
placeholder: v4.3.3
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://img.shields.io/github/contributors/netbox-community/netbox?color=blue" alt="Contributors" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/stargazers"><img src="https://img.shields.io/github/stars/netbox-community/netbox?style=flat" alt="GitHub stars" /></a>
|
||||
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-15-blue" alt="Languages supported" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/actions/workflows/ci.yml/badge.svg" alt="CI status" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=main" alt="CI status" /></a>
|
||||
<p>
|
||||
<strong><a href="https://netboxlabs.com/community/">NetBox Community</a></strong> |
|
||||
<strong><a href="https://netboxlabs.com/netbox-cloud/">NetBox Cloud</a></strong> |
|
||||
|
||||
@@ -14,10 +14,6 @@ django-debug-toolbar
|
||||
# https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst
|
||||
django-filter
|
||||
|
||||
# Django Debug Toolbar extension for GraphiQL
|
||||
# https://github.com/flavors/django-graphiql-debug-toolbar/blob/main/CHANGES.rst
|
||||
django-graphiql-debug-toolbar
|
||||
|
||||
# HTMX utilities for Django
|
||||
# https://django-htmx.readthedocs.io/en/latest/changelog.html
|
||||
django-htmx
|
||||
@@ -112,7 +108,6 @@ nh3
|
||||
|
||||
# Fork of PIL (Python Imaging Library) for image processing
|
||||
# https://github.com/python-pillow/Pillow/releases
|
||||
# https://pillow.readthedocs.io/en/stable/releasenotes/
|
||||
Pillow
|
||||
|
||||
# PostgreSQL database adapter for Python
|
||||
@@ -131,14 +126,14 @@ requests
|
||||
# https://github.com/rq/rq/blob/master/CHANGES.md
|
||||
rq
|
||||
|
||||
# Django app for social-auth-core
|
||||
# https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md
|
||||
social-auth-app-django
|
||||
|
||||
# Social authentication framework
|
||||
# https://github.com/python-social-auth/social-core/blob/master/CHANGELOG.md
|
||||
social-auth-core
|
||||
|
||||
# Django app for social-auth-core
|
||||
# https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md
|
||||
social-auth-app-django
|
||||
|
||||
# Strawberry GraphQL
|
||||
# https://github.com/strawberry-graphql/strawberry/blob/main/CHANGELOG.md
|
||||
strawberry-graphql
|
||||
|
||||
@@ -158,7 +158,6 @@ LOGGING = {
|
||||
* `netbox.<app>.<model>` - Generic form for model-specific log messages
|
||||
* `netbox.auth.*` - Authentication events
|
||||
* `netbox.api.views.*` - Views which handle business logic for the REST API
|
||||
* `netbox.event_rules` - Event rules
|
||||
* `netbox.reports.*` - Report execution (`module.name`)
|
||||
* `netbox.scripts.*` - Custom script execution (`module.name`)
|
||||
* `netbox.views.*` - Views which handle business logic for the web UI
|
||||
|
||||
@@ -147,7 +147,7 @@ For UI development you will need to review the [Web UI Development Guide](web-ui
|
||||
|
||||
## Populating Demo Data
|
||||
|
||||
Once you have your development environment up and running, it might be helpful to populate some "dummy" data to make interacting with the UI and APIs more convenient. Check out the [netbox-demo-data](https://github.com/netbox-community/netbox-demo-data) repo on GitHub, which houses a collection of sample data that can be easily imported to any new NetBox deployment. This sample data is used to populate the [public demo instance](https://demo.netbox.dev).
|
||||
Once you have your development environment up and running, it might be helpful to populate some "dummy" data to make interacting with the UI and APIs more convenient. Check out the [netbox-demo-data](https://github.com/netbox-community/netbox-demo-data) repo on GitHub, which houses a collection of sample data that can be easily imported to any new NetBox deployment. (This sample data is used to populate the public demo instance at <https://demo.netbox.dev>.)
|
||||
|
||||
The demo data is provided in JSON format and loaded into an empty database using Django's `loaddata` management command. Consult the demo data repo's `README` file for complete instructions on populating the data.
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
NetBox includes the ability to execute certain functions as background tasks. These include:
|
||||
|
||||
* [Report](../customization/reports.md) execution
|
||||
* [Custom script](../customization/custom-scripts.md) execution
|
||||
* Synchronization of [remote data sources](../integrations/synchronized-data.md)
|
||||
* Housekeeping tasks
|
||||
|
||||
Additionally, NetBox plugins can enqueue their own background tasks. This is accomplished using the [Job model](../models/core/job.md). Background tasks are executed by the `rqworker` process(es).
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ Check out the desired release by specifying its tag. For example:
|
||||
|
||||
```
|
||||
cd /opt/netbox && \
|
||||
sudo git fetch --tags && \
|
||||
sudo git fetch && \
|
||||
sudo git checkout v4.2.7
|
||||
```
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 15 KiB |
@@ -10,11 +10,11 @@ The assignment of platforms to devices is an optional feature, and may be disreg
|
||||
|
||||
### Name
|
||||
|
||||
A unique human-friendly name.
|
||||
A human-friendly name for the platform. Must be unique per manufacturer.
|
||||
|
||||
### Slug
|
||||
|
||||
A unique URL-friendly identifier. (This value can be used for filtering.)
|
||||
A URL-friendly identifier; must be unique per manufacturer. (This value can be used for filtering.)
|
||||
|
||||
### Manufacturer
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ A background job implements a basic [Job](../../models/core/job.md) executor for
|
||||
```python title="jobs.py"
|
||||
from netbox.jobs import JobRunner
|
||||
|
||||
|
||||
class MyTestJob(JobRunner):
|
||||
class Meta:
|
||||
name = "My Test Job"
|
||||
@@ -24,8 +25,6 @@ class MyTestJob(JobRunner):
|
||||
# your logic goes here
|
||||
```
|
||||
|
||||
Completed jobs will have their status updated to "completed" by default, or "errored" if an unhandled exception was raised by the `run()` method. To intentionally mark a job as failed, raise the `core.exceptions.JobFailed` exception. (Note that "failed" differs from "errored" in that a failure may be expected under certain conditions, whereas an error is not.)
|
||||
|
||||
You can schedule the background job from within your code (e.g. from a model's `save()` method or a view) by calling `MyTestJob.enqueue()`. This method passes through all arguments to `Job.enqueue()`. However, no `name` argument must be passed, as the background job name will be used instead.
|
||||
|
||||
!!! tip
|
||||
|
||||
@@ -64,6 +64,7 @@ Generic view classes (documented below) facilitate common operations, such as cr
|
||||
| `ObjectListView` | View a list of objects |
|
||||
| `BulkImportView` | Import a set of new objects |
|
||||
| `BulkEditView` | Edit multiple objects |
|
||||
| `BulkRenameView` | Rename multiple objects |
|
||||
| `BulkDeleteView` | Delete multiple objects |
|
||||
|
||||
!!! warning
|
||||
@@ -171,6 +172,10 @@ Below are the class definitions for NetBox's multi-object views. These views han
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.views.generic.BulkRenameView
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.views.generic.BulkDeleteView
|
||||
options:
|
||||
members:
|
||||
|
||||
@@ -1,27 +1,5 @@
|
||||
# NetBox v4.3
|
||||
|
||||
## v4.3.4 (2025-07-15)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#18811](https://github.com/netbox-community/netbox/issues/18811) - Match expanded form IPv6 addresses in global search
|
||||
* [#19550](https://github.com/netbox-community/netbox/issues/19550) - Enable lazy loading for rack elevations
|
||||
* [#19571](https://github.com/netbox-community/netbox/issues/19571) - Add a default module type profile for expansion cards
|
||||
* [#19793](https://github.com/netbox-community/netbox/issues/19793) - Support custom dynamic navigation menu links
|
||||
* [#19828](https://github.com/netbox-community/netbox/issues/19828) - Expose L2VPN termination in interface GraphQL response
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#19413](https://github.com/netbox-community/netbox/issues/19413) - Custom fields should be grouped in filter forms
|
||||
* [#19633](https://github.com/netbox-community/netbox/issues/19633) - Introduce InvalidCondition exception and log all evaluations of invalid event rule conditions
|
||||
* [#19800](https://github.com/netbox-community/netbox/issues/19800) - Module type bulk import should support profile assignment
|
||||
* [#19806](https://github.com/netbox-community/netbox/issues/19806) - Introduce JobFailed exception to allow marking background jobs as failed
|
||||
* [#19827](https://github.com/netbox-community/netbox/issues/19827) - Enforce uniqueness for device role names & slugs
|
||||
* [#19839](https://github.com/netbox-community/netbox/issues/19839) - Enable export of parent assignment for recursively nested objects
|
||||
* [#19876](https://github.com/netbox-community/netbox/issues/19876) - Remove Markdown rendering from CustomFieldChoiceSet description field
|
||||
|
||||
---
|
||||
|
||||
## v4.3.3 (2025-06-26)
|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.views import PathTraceView
|
||||
from ipam.models import ASN
|
||||
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
|
||||
from netbox.views import generic
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.query import count_related
|
||||
@@ -79,6 +80,11 @@ class ProviderBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class ProviderBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Provider.objects.annotate(
|
||||
@@ -141,6 +147,11 @@ class ProviderAccountBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ProviderAccount.objects.annotate(
|
||||
@@ -212,6 +223,11 @@ class ProviderNetworkBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class ProviderNetworkBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
@@ -271,6 +287,11 @@ class CircuitTypeBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = CircuitType.objects.annotate(
|
||||
@@ -337,6 +358,12 @@ class CircuitBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class CircuitBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
@@ -432,6 +459,7 @@ class CircuitTerminationListView(generic.ObjectListView):
|
||||
filterset = filtersets.CircuitTerminationFilterSet
|
||||
filterset_form = forms.CircuitTerminationFilterForm
|
||||
table = tables.CircuitTerminationTable
|
||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
||||
|
||||
|
||||
@register_model_view(CircuitTermination)
|
||||
@@ -526,6 +554,11 @@ class CircuitGroupBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class CircuitGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
@@ -543,6 +576,7 @@ class CircuitGroupAssignmentListView(generic.ObjectListView):
|
||||
filterset = filtersets.CircuitGroupAssignmentFilterSet
|
||||
filterset_form = forms.CircuitGroupAssignmentFilterForm
|
||||
table = tables.CircuitGroupAssignmentTable
|
||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
||||
|
||||
|
||||
@register_model_view(CircuitGroupAssignment)
|
||||
@@ -635,6 +669,11 @@ class VirtualCircuitTypeBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class VirtualCircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VirtualCircuitType.objects.annotate(
|
||||
@@ -697,6 +736,12 @@ class VirtualCircuitBulkEditView(generic.BulkEditView):
|
||||
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):
|
||||
queryset = VirtualCircuit.objects.annotate(
|
||||
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
|
||||
@@ -714,6 +759,7 @@ class VirtualCircuitTerminationListView(generic.ObjectListView):
|
||||
filterset = filtersets.VirtualCircuitTerminationFilterSet
|
||||
filterset_form = forms.VirtualCircuitTerminationFilterForm
|
||||
table = tables.VirtualCircuitTerminationTable
|
||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitTermination)
|
||||
|
||||
@@ -23,6 +23,6 @@ class JobSerializer(BaseModelSerializer):
|
||||
model = Job
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled',
|
||||
'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id',
|
||||
'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'log_entries',
|
||||
]
|
||||
brief_fields = ('url', 'created', 'completed', 'user', 'status')
|
||||
|
||||
19
netbox/core/dataclasses.py
Normal file
19
netbox/core/dataclasses.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import logging
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
|
||||
__all__ = (
|
||||
'JobLogEntry',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class JobLogEntry:
|
||||
level: str
|
||||
message: str
|
||||
timestamp: datetime = field(default_factory=datetime.now)
|
||||
|
||||
@classmethod
|
||||
def from_logrecord(cls, record: logging.LogRecord):
|
||||
return cls(record.levelname.lower(), record.msg)
|
||||
@@ -1,19 +1,9 @@
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
__all__ = (
|
||||
'IncompatiblePluginError',
|
||||
'JobFailed',
|
||||
'SyncError',
|
||||
)
|
||||
|
||||
|
||||
class IncompatiblePluginError(ImproperlyConfigured):
|
||||
pass
|
||||
|
||||
|
||||
class JobFailed(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SyncError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class IncompatiblePluginError(ImproperlyConfigured):
|
||||
pass
|
||||
|
||||
25
netbox/core/migrations/0016_job_log_entries.py
Normal file
25
netbox/core/migrations/0016_job_log_entries.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import django.contrib.postgres.fields
|
||||
import django.core.serializers.json
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
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(
|
||||
encoder=django.core.serializers.json.DjangoJSONEncoder
|
||||
),
|
||||
blank=True,
|
||||
default=list,
|
||||
size=None
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,9 +1,12 @@
|
||||
import logging
|
||||
import uuid
|
||||
from dataclasses import asdict
|
||||
from functools import partial
|
||||
|
||||
import django_rq
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.core.validators import MinValueValidator
|
||||
@@ -14,6 +17,7 @@ from django.utils.translation import gettext as _
|
||||
from rq.exceptions import InvalidJobOperation
|
||||
|
||||
from core.choices import JobStatusChoices
|
||||
from core.dataclasses import JobLogEntry
|
||||
from core.models import ObjectType
|
||||
from core.signals import job_end, job_start
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
@@ -104,6 +108,14 @@ class Job(models.Model):
|
||||
verbose_name=_('job ID'),
|
||||
unique=True
|
||||
)
|
||||
log_entries = ArrayField(
|
||||
base_field=models.JSONField(
|
||||
encoder=DjangoJSONEncoder,
|
||||
# TODO: Specify a decoder to handle ISO 8601 timestamps
|
||||
),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
@@ -187,14 +199,15 @@ class Job(models.Model):
|
||||
"""
|
||||
Mark the job as completed, optionally specifying a particular termination status.
|
||||
"""
|
||||
if status not in JobStatusChoices.TERMINAL_STATE_CHOICES:
|
||||
valid_statuses = JobStatusChoices.TERMINAL_STATE_CHOICES
|
||||
if status not in valid_statuses:
|
||||
raise ValueError(
|
||||
_("Invalid status for job termination. Choices are: {choices}").format(
|
||||
choices=', '.join(JobStatusChoices.TERMINAL_STATE_CHOICES)
|
||||
choices=', '.join(valid_statuses)
|
||||
)
|
||||
)
|
||||
|
||||
# Set the job's status and completion time
|
||||
# Mark the job as completed
|
||||
self.status = status
|
||||
if error:
|
||||
self.error = error
|
||||
@@ -270,3 +283,10 @@ class Job(models.Model):
|
||||
transaction.on_commit(callback)
|
||||
|
||||
return job
|
||||
|
||||
def log(self, record: logging.LogRecord):
|
||||
"""
|
||||
Record a Python LogRecord in the job's log.
|
||||
"""
|
||||
entry = JobLogEntry.from_logrecord(record)
|
||||
self.log_entries.append(asdict(entry))
|
||||
|
||||
18
netbox/core/object_actions.py
Normal file
18
netbox/core/object_actions.py
Normal file
@@ -0,0 +1,18 @@
|
||||
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,7 +1,7 @@
|
||||
import django_tables2 as tables
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from netbox.tables import BaseTable, NetBoxTable, columns
|
||||
from ..models import Job
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ class JobTable(NetBoxTable):
|
||||
completed = columns.DateTimeColumn(
|
||||
verbose_name=_('Completed'),
|
||||
)
|
||||
log_entries = tables.Column(
|
||||
verbose_name=_('Log Entries'),
|
||||
)
|
||||
actions = columns.ActionsColumn(
|
||||
actions=('delete',)
|
||||
)
|
||||
@@ -53,3 +56,22 @@ class JobTable(NetBoxTable):
|
||||
default_columns = (
|
||||
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user',
|
||||
)
|
||||
|
||||
def render_log_entries(self, value):
|
||||
return len(value)
|
||||
|
||||
|
||||
class JobLogEntryTable(BaseTable):
|
||||
timestamp = tables.Column(
|
||||
verbose_name=_('Time'),
|
||||
)
|
||||
level = tables.Column(
|
||||
verbose_name=_('Level'),
|
||||
)
|
||||
message = tables.Column(
|
||||
verbose_name=_('Message'),
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
empty_text = _('No log entries')
|
||||
fields = ('timestamp', 'level', 'message')
|
||||
|
||||
@@ -22,6 +22,7 @@ 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 netbox.config import get_config, PARAMS
|
||||
from netbox.object_actions import AddObject, BulkDelete, BulkExport, DeleteObject
|
||||
from netbox.registry import registry
|
||||
from netbox.views import generic
|
||||
from netbox.views.generic.base import BaseObjectView
|
||||
@@ -31,13 +32,13 @@ from utilities.forms import ConfirmationForm
|
||||
from utilities.htmx import htmx_partial
|
||||
from utilities.json import ConfigJSONEncoder
|
||||
from utilities.query import count_related
|
||||
from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, register_model_view
|
||||
from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, ViewTab, register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
from .choices import DataSourceStatusChoices
|
||||
from .jobs import SyncDataSourceJob
|
||||
from .models import *
|
||||
from .plugins import get_catalog_plugins, get_local_plugins
|
||||
from .tables import CatalogPluginTable, PluginVersionTable
|
||||
from .tables import CatalogPluginTable, JobLogEntryTable, PluginVersionTable
|
||||
|
||||
|
||||
#
|
||||
@@ -119,6 +120,11 @@ class DataSourceBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class DataSourceBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = DataSource.objects.annotate(
|
||||
@@ -138,14 +144,13 @@ class DataFileListView(generic.ObjectListView):
|
||||
filterset = filtersets.DataFileFilterSet
|
||||
filterset_form = forms.DataFileFilterForm
|
||||
table = tables.DataFileTable
|
||||
actions = {
|
||||
'bulk_delete': {'delete'},
|
||||
}
|
||||
actions = (BulkDelete,)
|
||||
|
||||
|
||||
@register_model_view(DataFile)
|
||||
class DataFileView(generic.ObjectView):
|
||||
queryset = DataFile.objects.all()
|
||||
actions = (DeleteObject,)
|
||||
|
||||
|
||||
@register_model_view(DataFile, 'delete')
|
||||
@@ -170,15 +175,32 @@ class JobListView(generic.ObjectListView):
|
||||
filterset = filtersets.JobFilterSet
|
||||
filterset_form = forms.JobFilterForm
|
||||
table = tables.JobTable
|
||||
actions = {
|
||||
'export': {'view'},
|
||||
'bulk_delete': {'delete'},
|
||||
}
|
||||
actions = (BulkExport, BulkDelete)
|
||||
|
||||
|
||||
@register_model_view(Job)
|
||||
class JobView(generic.ObjectView):
|
||||
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')
|
||||
@@ -204,9 +226,7 @@ class ObjectChangeListView(generic.ObjectListView):
|
||||
filterset_form = forms.ObjectChangeFilterForm
|
||||
table = tables.ObjectChangeTable
|
||||
template_name = 'core/objectchange_list.html'
|
||||
actions = {
|
||||
'export': {'view'},
|
||||
}
|
||||
actions = (BulkExport,)
|
||||
|
||||
|
||||
@register_model_view(ObjectChange)
|
||||
@@ -274,6 +294,7 @@ class ConfigRevisionListView(generic.ObjectListView):
|
||||
filterset = filtersets.ConfigRevisionFilterSet
|
||||
filterset_form = forms.ConfigRevisionFilterForm
|
||||
table = tables.ConfigRevisionTable
|
||||
actions = (AddObject, BulkExport)
|
||||
|
||||
|
||||
@register_model_view(ConfigRevision)
|
||||
|
||||
@@ -470,8 +470,8 @@ class ModuleTypeImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = ModuleType
|
||||
fields = [
|
||||
'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'profile',
|
||||
'comments', 'tags'
|
||||
'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'comments',
|
||||
'tags',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ if TYPE_CHECKING:
|
||||
from tenancy.graphql.types import TenantType
|
||||
from users.graphql.types import UserType
|
||||
from virtualization.graphql.types import ClusterType, VMInterfaceType, VirtualMachineType
|
||||
from vpn.graphql.types import L2VPNTerminationType
|
||||
from wireless.graphql.types import WirelessLANType, WirelessLinkType
|
||||
|
||||
__all__ = (
|
||||
@@ -441,7 +440,6 @@ class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, P
|
||||
primary_mac_address: Annotated["MACAddressType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
qinq_svlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
vlan_translation_policy: Annotated["VLANTranslationPolicyType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
l2vpn_termination: Annotated["L2VPNTerminationType", strawberry.lazy('vpn.graphql.types')] | None
|
||||
|
||||
vdcs: List[Annotated["VirtualDeviceContextType", strawberry.lazy('dcim.graphql.types')]]
|
||||
tagged_vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]
|
||||
|
||||
@@ -19,8 +19,7 @@ def load_initial_data(apps, schema_editor):
|
||||
'gpu',
|
||||
'hard_disk',
|
||||
'memory',
|
||||
'power_supply',
|
||||
'expansion_card'
|
||||
'power_supply'
|
||||
)
|
||||
|
||||
for name in initial_profiles:
|
||||
|
||||
@@ -1,44 +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.AddConstraint(
|
||||
model_name='devicerole',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('parent', 'name'),
|
||||
name='dcim_devicerole_parent_name'
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='devicerole',
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(('parent__isnull', True)),
|
||||
fields=('name',),
|
||||
name='dcim_devicerole_name',
|
||||
violation_error_message='A top-level device role with this name already exists.'
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='devicerole',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('parent', 'slug'),
|
||||
name='dcim_devicerole_parent_slug'
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='devicerole',
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(('parent__isnull', True)),
|
||||
fields=('slug',),
|
||||
name='dcim_devicerole_slug',
|
||||
violation_error_message='A top-level device role with this slug already exists.'
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
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,15 +0,0 @@
|
||||
{
|
||||
"name": "Expansion card",
|
||||
"schema": {
|
||||
"properties": {
|
||||
"connector_type": {
|
||||
"type": "string",
|
||||
"description": "Connector type e.g. PCIe x4"
|
||||
},
|
||||
"bandwidth": {
|
||||
"type": "integer",
|
||||
"description": "Total Bandwidth for this module"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -398,28 +398,6 @@ class DeviceRole(NestedGroupModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('parent', 'name'),
|
||||
name='%(app_label)s_%(class)s_parent_name'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('name',),
|
||||
name='%(app_label)s_%(class)s_name',
|
||||
condition=Q(parent__isnull=True),
|
||||
violation_error_message=_("A top-level device role with this name already exists.")
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('parent', 'slug'),
|
||||
name='%(app_label)s_%(class)s_parent_slug'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('slug',),
|
||||
name='%(app_label)s_%(class)s_slug',
|
||||
condition=Q(parent__isnull=True),
|
||||
violation_error_message=_("A top-level device role with this slug already exists.")
|
||||
),
|
||||
)
|
||||
verbose_name = _('device role')
|
||||
verbose_name_plural = _('device roles')
|
||||
|
||||
@@ -437,6 +415,15 @@ class Platform(OrganizationalModel):
|
||||
null=True,
|
||||
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(
|
||||
to='extras.ConfigTemplate',
|
||||
on_delete=models.PROTECT,
|
||||
@@ -449,6 +436,28 @@ class Platform(OrganizationalModel):
|
||||
ordering = ('name',)
|
||||
verbose_name = _('platform')
|
||||
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(
|
||||
|
||||
38
netbox/dcim/object_actions.py
Normal file
38
netbox/dcim/object_actions.py
Normal file
@@ -0,0 +1,38 @@
|
||||
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'
|
||||
@@ -63,10 +63,6 @@ class DeviceRoleTable(NetBoxTable):
|
||||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
)
|
||||
parent = tables.Column(
|
||||
verbose_name=_('Parent'),
|
||||
linkify=True,
|
||||
)
|
||||
device_count = columns.LinkedCountColumn(
|
||||
viewname='dcim:device_list',
|
||||
url_params={'role_id': 'pk'},
|
||||
@@ -92,8 +88,8 @@ class DeviceRoleTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = models.DeviceRole
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'parent', 'device_count', 'vm_count', 'color', 'vm_role', 'config_template',
|
||||
'description', 'slug', 'tags', 'actions', 'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'config_template', 'description',
|
||||
'slug', 'tags', 'actions', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description')
|
||||
|
||||
|
||||
@@ -24,10 +24,6 @@ class RegionTable(ContactsColumnMixin, NetBoxTable):
|
||||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
)
|
||||
parent = tables.Column(
|
||||
verbose_name=_('Parent'),
|
||||
linkify=True,
|
||||
)
|
||||
site_count = columns.LinkedCountColumn(
|
||||
viewname='dcim:site_list',
|
||||
url_params={'region_id': 'pk'},
|
||||
@@ -43,7 +39,7 @@ class RegionTable(ContactsColumnMixin, NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Region
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'parent', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags',
|
||||
'pk', 'id', 'name', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags',
|
||||
'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'site_count', 'description')
|
||||
@@ -58,10 +54,6 @@ class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
|
||||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
)
|
||||
parent = tables.Column(
|
||||
verbose_name=_('Parent'),
|
||||
linkify=True,
|
||||
)
|
||||
site_count = columns.LinkedCountColumn(
|
||||
viewname='dcim:site_list',
|
||||
url_params={'group_id': 'pk'},
|
||||
@@ -77,7 +69,7 @@ class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = SiteGroup
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'parent', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags',
|
||||
'pk', 'id', 'name', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags',
|
||||
'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'site_count', 'description')
|
||||
@@ -143,10 +135,6 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
)
|
||||
parent = tables.Column(
|
||||
verbose_name=_('Parent'),
|
||||
linkify=True,
|
||||
)
|
||||
site = tables.Column(
|
||||
verbose_name=_('Site'),
|
||||
linkify=True
|
||||
@@ -182,8 +170,8 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Location
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'parent', 'site', 'status', 'facility', 'tenant', 'tenant_group', 'rack_count',
|
||||
'device_count', 'description', 'slug', 'comments', 'contacts', 'tags', 'actions', 'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'site', 'status', 'facility', 'tenant', 'tenant_group', 'rack_count', 'device_count',
|
||||
'description', 'slug', 'comments', 'contacts', 'tags', 'actions', 'created', 'last_updated',
|
||||
'vlangroup_count',
|
||||
)
|
||||
default_columns = (
|
||||
|
||||
@@ -3,7 +3,7 @@ from decimal import Decimal
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import yaml
|
||||
from django.test import override_settings, tag
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from netaddr import EUI
|
||||
|
||||
@@ -1000,7 +1000,18 @@ inventory-items:
|
||||
self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8')
|
||||
|
||||
|
||||
class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
# TODO: Change base class to PrimaryObjectViewTestCase
|
||||
# Blocked by absence of bulk import view for ModuleTypes
|
||||
class ModuleTypeTestCase(
|
||||
ViewTestCases.GetObjectViewTestCase,
|
||||
ViewTestCases.GetObjectChangelogViewTestCase,
|
||||
ViewTestCases.CreateObjectViewTestCase,
|
||||
ViewTestCases.EditObjectViewTestCase,
|
||||
ViewTestCases.DeleteObjectViewTestCase,
|
||||
ViewTestCases.ListObjectsViewTestCase,
|
||||
ViewTestCases.BulkEditObjectsViewTestCase,
|
||||
ViewTestCases.BulkDeleteObjectsViewTestCase
|
||||
):
|
||||
model = ModuleType
|
||||
|
||||
@classmethod
|
||||
@@ -1012,7 +1023,7 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
Manufacturer.objects.bulk_create(manufacturers)
|
||||
|
||||
module_types = ModuleType.objects.bulk_create([
|
||||
ModuleType.objects.bulk_create([
|
||||
ModuleType(model='Module Type 1', manufacturer=manufacturers[0]),
|
||||
ModuleType(model='Module Type 2', manufacturer=manufacturers[0]),
|
||||
ModuleType(model='Module Type 3', manufacturer=manufacturers[0]),
|
||||
@@ -1020,8 +1031,6 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
fan_module_type_profile = ModuleTypeProfile.objects.get(name='Fan')
|
||||
|
||||
cls.form_data = {
|
||||
'manufacturer': manufacturers[1].pk,
|
||||
'model': 'Device Type X',
|
||||
@@ -1035,70 +1044,6 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'part_number': '456DEF',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"manufacturer,model,part_number,comments,profile",
|
||||
f"Manufacturer 1,fan0,generic-fan,,{fan_module_type_profile.name}"
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,model",
|
||||
f"{module_types[0].id},test model",
|
||||
)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_bulk_update_objects_with_permission(self):
|
||||
self.add_permissions(
|
||||
'dcim.add_consoleporttemplate',
|
||||
'dcim.add_consoleserverporttemplate',
|
||||
'dcim.add_powerporttemplate',
|
||||
'dcim.add_poweroutlettemplate',
|
||||
'dcim.add_interfacetemplate',
|
||||
'dcim.add_frontporttemplate',
|
||||
'dcim.add_rearporttemplate',
|
||||
'dcim.add_modulebaytemplate',
|
||||
)
|
||||
|
||||
# run base test
|
||||
super().test_bulk_update_objects_with_permission()
|
||||
|
||||
@tag('regression')
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
|
||||
def test_bulk_import_objects_with_permission(self):
|
||||
self.add_permissions(
|
||||
'dcim.add_consoleporttemplate',
|
||||
'dcim.add_consoleserverporttemplate',
|
||||
'dcim.add_powerporttemplate',
|
||||
'dcim.add_poweroutlettemplate',
|
||||
'dcim.add_interfacetemplate',
|
||||
'dcim.add_frontporttemplate',
|
||||
'dcim.add_rearporttemplate',
|
||||
'dcim.add_modulebaytemplate',
|
||||
)
|
||||
|
||||
# run base test
|
||||
super().test_bulk_import_objects_with_permission()
|
||||
|
||||
# TODO: remove extra regression asserts once parent test supports testing all import fields
|
||||
fan_module_type = ModuleType.objects.get(part_number='generic-fan')
|
||||
fan_module_type_profile = ModuleTypeProfile.objects.get(name='Fan')
|
||||
|
||||
assert fan_module_type.profile == fan_module_type_profile
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
|
||||
def test_bulk_import_objects_with_constrained_permission(self):
|
||||
self.add_permissions(
|
||||
'dcim.add_consoleporttemplate',
|
||||
'dcim.add_consoleserverporttemplate',
|
||||
'dcim.add_powerporttemplate',
|
||||
'dcim.add_poweroutlettemplate',
|
||||
'dcim.add_interfacetemplate',
|
||||
'dcim.add_frontporttemplate',
|
||||
'dcim.add_rearporttemplate',
|
||||
'dcim.add_modulebaytemplate',
|
||||
)
|
||||
|
||||
super().test_bulk_import_objects_with_constrained_permission()
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_moduletype_consoleports(self):
|
||||
moduletype = ModuleType.objects.first()
|
||||
@@ -1859,9 +1804,9 @@ class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
|
||||
cls.csv_data = (
|
||||
"name,slug,color",
|
||||
"Device Role 6,device-role-6,ff0000",
|
||||
"Device Role 7,device-role-7,00ff00",
|
||||
"Device Role 8,device-role-8,0000ff",
|
||||
"Device Role 4,device-role-4,ff0000",
|
||||
"Device Role 5,device-role-5,00ff00",
|
||||
"Device Role 6,device-role-6,0000ff",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
|
||||
@@ -15,7 +15,7 @@ from circuits.models import Circuit, CircuitTermination
|
||||
from extras.views import ObjectConfigContextView, ObjectRenderConfigView
|
||||
from ipam.models import ASN, IPAddress, Prefix, VLANGroup, VLAN
|
||||
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
|
||||
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
||||
from netbox.object_actions import *
|
||||
from netbox.views import generic
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||
@@ -34,6 +34,7 @@ from wireless.models import WirelessLAN
|
||||
from . import filtersets, forms, tables
|
||||
from .choices import DeviceFaceChoices, InterfaceModeChoices
|
||||
from .models import *
|
||||
from .object_actions import BulkAddComponents, BulkDisconnect
|
||||
|
||||
CABLE_TERMINATION_TYPES = {
|
||||
'dcim.consoleport': ConsolePort,
|
||||
@@ -49,11 +50,6 @@ CABLE_TERMINATION_TYPES = {
|
||||
|
||||
|
||||
class DeviceComponentsView(generic.ObjectChildrenView):
|
||||
actions = {
|
||||
**DEFAULT_ACTION_PERMISSIONS,
|
||||
'bulk_rename': {'change'},
|
||||
'bulk_disconnect': {'change'},
|
||||
}
|
||||
queryset = Device.objects.all()
|
||||
|
||||
def get_children(self, request, parent):
|
||||
@@ -61,12 +57,8 @@ class DeviceComponentsView(generic.ObjectChildrenView):
|
||||
|
||||
|
||||
class DeviceTypeComponentsView(generic.ObjectChildrenView):
|
||||
actions = {
|
||||
**DEFAULT_ACTION_PERMISSIONS,
|
||||
'bulk_rename': {'change'},
|
||||
}
|
||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
|
||||
queryset = DeviceType.objects.all()
|
||||
template_name = 'dcim/devicetype/component_templates.html'
|
||||
viewname = None # Used for return_url resolution
|
||||
|
||||
def get_children(self, request, parent):
|
||||
@@ -78,9 +70,9 @@ class DeviceTypeComponentsView(generic.ObjectChildrenView):
|
||||
}
|
||||
|
||||
|
||||
class ModuleTypeComponentsView(DeviceComponentsView):
|
||||
class ModuleTypeComponentsView(generic.ObjectChildrenView):
|
||||
queryset = ModuleType.objects.all()
|
||||
template_name = 'dcim/moduletype/component_templates.html'
|
||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
|
||||
viewname = None # Used for return_url resolution
|
||||
|
||||
def get_children(self, request, parent):
|
||||
@@ -300,6 +292,11 @@ class RegionBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class RegionBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Region.objects.add_related_count(
|
||||
@@ -426,6 +423,11 @@ class SiteGroupBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class SiteGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SiteGroup.objects.add_related_count(
|
||||
@@ -511,6 +513,11 @@ class SiteBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class SiteBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Site.objects.all()
|
||||
@@ -615,6 +622,11 @@ class LocationBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class LocationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Location.objects.add_related_count(
|
||||
@@ -680,6 +692,11 @@ class RackRoleBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class RackRoleBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RackRole.objects.annotate(
|
||||
@@ -739,6 +756,12 @@ class RackTypeBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class RackTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RackType.objects.all()
|
||||
@@ -918,6 +941,11 @@ class RackBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class RackBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Rack.objects.all()
|
||||
@@ -935,6 +963,7 @@ class RackReservationListView(generic.ObjectListView):
|
||||
filterset = filtersets.RackReservationFilterSet
|
||||
filterset_form = forms.RackReservationFilterForm
|
||||
table = tables.RackReservationTable
|
||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
||||
|
||||
|
||||
@register_model_view(RackReservation)
|
||||
@@ -1051,6 +1080,11 @@ class ManufacturerBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class ManufacturerBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Manufacturer.objects.annotate(
|
||||
@@ -1298,6 +1332,12 @@ class DeviceTypeBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = DeviceType.objects.annotate(
|
||||
@@ -1354,6 +1394,11 @@ class ModuleTypeProfileBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class ModuleTypeProfileBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ModuleTypeProfile.objects.annotate(
|
||||
@@ -1564,6 +1609,11 @@ class ModuleTypeBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ModuleType.objects.annotate(
|
||||
@@ -2038,6 +2088,11 @@ class DeviceRoleBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class DeviceRoleBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = DeviceRole.objects.annotate(
|
||||
@@ -2099,6 +2154,11 @@ class PlatformBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class PlatformBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Platform.objects.all()
|
||||
@@ -2116,7 +2176,7 @@ class DeviceListView(generic.ObjectListView):
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
filterset_form = forms.DeviceFilterForm
|
||||
table = tables.DeviceTable
|
||||
template_name = 'dcim/device_list.html'
|
||||
actions = (AddObject, BulkImport, BulkExport, BulkAddComponents, BulkEdit, BulkRename, BulkDelete)
|
||||
|
||||
|
||||
@register_model_view(Device)
|
||||
@@ -2157,7 +2217,7 @@ class DeviceConsolePortsView(DeviceComponentsView):
|
||||
table = tables.DeviceConsolePortTable
|
||||
filterset = filtersets.ConsolePortFilterSet
|
||||
filterset_form = forms.ConsolePortFilterForm
|
||||
template_name = 'dcim/device/consoleports.html',
|
||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
|
||||
tab = ViewTab(
|
||||
label=_('Console Ports'),
|
||||
badge=lambda obj: obj.console_port_count,
|
||||
@@ -2173,7 +2233,7 @@ class DeviceConsoleServerPortsView(DeviceComponentsView):
|
||||
table = tables.DeviceConsoleServerPortTable
|
||||
filterset = filtersets.ConsoleServerPortFilterSet
|
||||
filterset_form = forms.ConsoleServerPortFilterForm
|
||||
template_name = 'dcim/device/consoleserverports.html'
|
||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
|
||||
tab = ViewTab(
|
||||
label=_('Console Server Ports'),
|
||||
badge=lambda obj: obj.console_server_port_count,
|
||||
@@ -2189,7 +2249,7 @@ class DevicePowerPortsView(DeviceComponentsView):
|
||||
table = tables.DevicePowerPortTable
|
||||
filterset = filtersets.PowerPortFilterSet
|
||||
filterset_form = forms.PowerPortFilterForm
|
||||
template_name = 'dcim/device/powerports.html'
|
||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
|
||||
tab = ViewTab(
|
||||
label=_('Power Ports'),
|
||||
badge=lambda obj: obj.power_port_count,
|
||||
@@ -2205,7 +2265,7 @@ class DevicePowerOutletsView(DeviceComponentsView):
|
||||
table = tables.DevicePowerOutletTable
|
||||
filterset = filtersets.PowerOutletFilterSet
|
||||
filterset_form = forms.PowerOutletFilterForm
|
||||
template_name = 'dcim/device/poweroutlets.html'
|
||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
|
||||
tab = ViewTab(
|
||||
label=_('Power Outlets'),
|
||||
badge=lambda obj: obj.power_outlet_count,
|
||||
@@ -2221,6 +2281,7 @@ class DeviceInterfacesView(DeviceComponentsView):
|
||||
table = tables.DeviceInterfaceTable
|
||||
filterset = filtersets.InterfaceFilterSet
|
||||
filterset_form = forms.InterfaceFilterForm
|
||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
|
||||
template_name = 'dcim/device/interfaces.html'
|
||||
tab = ViewTab(
|
||||
label=_('Interfaces'),
|
||||
@@ -2243,7 +2304,7 @@ class DeviceFrontPortsView(DeviceComponentsView):
|
||||
table = tables.DeviceFrontPortTable
|
||||
filterset = filtersets.FrontPortFilterSet
|
||||
filterset_form = forms.FrontPortFilterForm
|
||||
template_name = 'dcim/device/frontports.html'
|
||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
|
||||
tab = ViewTab(
|
||||
label=_('Front Ports'),
|
||||
badge=lambda obj: obj.front_port_count,
|
||||
@@ -2259,7 +2320,7 @@ class DeviceRearPortsView(DeviceComponentsView):
|
||||
table = tables.DeviceRearPortTable
|
||||
filterset = filtersets.RearPortFilterSet
|
||||
filterset_form = forms.RearPortFilterForm
|
||||
template_name = 'dcim/device/rearports.html'
|
||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
|
||||
tab = ViewTab(
|
||||
label=_('Rear Ports'),
|
||||
badge=lambda obj: obj.rear_port_count,
|
||||
@@ -2275,11 +2336,7 @@ class DeviceModuleBaysView(DeviceComponentsView):
|
||||
table = tables.DeviceModuleBayTable
|
||||
filterset = filtersets.ModuleBayFilterSet
|
||||
filterset_form = forms.ModuleBayFilterForm
|
||||
template_name = 'dcim/device/modulebays.html'
|
||||
actions = {
|
||||
**DEFAULT_ACTION_PERMISSIONS,
|
||||
'bulk_rename': {'change'},
|
||||
}
|
||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
|
||||
tab = ViewTab(
|
||||
label=_('Module Bays'),
|
||||
badge=lambda obj: obj.module_bay_count,
|
||||
@@ -2295,11 +2352,7 @@ class DeviceDeviceBaysView(DeviceComponentsView):
|
||||
table = tables.DeviceDeviceBayTable
|
||||
filterset = filtersets.DeviceBayFilterSet
|
||||
filterset_form = forms.DeviceBayFilterForm
|
||||
template_name = 'dcim/device/devicebays.html'
|
||||
actions = {
|
||||
**DEFAULT_ACTION_PERMISSIONS,
|
||||
'bulk_rename': {'change'},
|
||||
}
|
||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
|
||||
tab = ViewTab(
|
||||
label=_('Device Bays'),
|
||||
badge=lambda obj: obj.device_bay_count,
|
||||
@@ -2315,11 +2368,7 @@ class DeviceInventoryView(DeviceComponentsView):
|
||||
table = tables.DeviceInventoryItemTable
|
||||
filterset = filtersets.InventoryItemFilterSet
|
||||
filterset_form = forms.InventoryItemFilterForm
|
||||
template_name = 'dcim/device/inventory.html'
|
||||
actions = {
|
||||
**DEFAULT_ACTION_PERMISSIONS,
|
||||
'bulk_rename': {'change'},
|
||||
}
|
||||
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
|
||||
tab = ViewTab(
|
||||
label=_('Inventory Items'),
|
||||
badge=lambda obj: obj.inventory_item_count,
|
||||
@@ -2393,16 +2442,16 @@ class DeviceBulkEditView(generic.BulkEditView):
|
||||
form = forms.DeviceBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(Device, 'bulk_delete', path='delete', detail=False)
|
||||
class DeviceBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Device.objects.prefetch_related('device_type__manufacturer')
|
||||
@register_model_view(Device, 'bulk_rename', path='rename', detail=False)
|
||||
class DeviceBulkRenameView(generic.BulkRenameView):
|
||||
queryset = Device.objects.all()
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
|
||||
|
||||
@register_model_view(Device, 'bulk_rename', path='rename', detail=False)
|
||||
class DeviceBulkRenameView(generic.BulkRenameView):
|
||||
queryset = Device.objects.all()
|
||||
@register_model_view(Device, 'bulk_delete', path='delete', detail=False)
|
||||
class DeviceBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Device.objects.prefetch_related('device_type__manufacturer')
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
|
||||
@@ -2417,6 +2466,7 @@ class ModuleListView(generic.ObjectListView):
|
||||
filterset = filtersets.ModuleFilterSet
|
||||
filterset_form = forms.ModuleFilterForm
|
||||
table = tables.ModuleTable
|
||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
||||
|
||||
|
||||
@register_model_view(Module)
|
||||
@@ -2472,11 +2522,6 @@ class ConsolePortListView(generic.ObjectListView):
|
||||
filterset = filtersets.ConsolePortFilterSet
|
||||
filterset_form = forms.ConsolePortFilterForm
|
||||
table = tables.ConsolePortTable
|
||||
template_name = 'dcim/component_list.html'
|
||||
actions = {
|
||||
**DEFAULT_ACTION_PERMISSIONS,
|
||||
'bulk_rename': {'change'},
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(ConsolePort)
|
||||
@@ -2547,11 +2592,6 @@ class ConsoleServerPortListView(generic.ObjectListView):
|
||||
filterset = filtersets.ConsoleServerPortFilterSet
|
||||
filterset_form = forms.ConsoleServerPortFilterForm
|
||||
table = tables.ConsoleServerPortTable
|
||||
template_name = 'dcim/component_list.html'
|
||||
actions = {
|
||||
**DEFAULT_ACTION_PERMISSIONS,
|
||||
'bulk_rename': {'change'},
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(ConsoleServerPort)
|
||||
@@ -2622,11 +2662,6 @@ class PowerPortListView(generic.ObjectListView):
|
||||
filterset = filtersets.PowerPortFilterSet
|
||||
filterset_form = forms.PowerPortFilterForm
|
||||
table = tables.PowerPortTable
|
||||
template_name = 'dcim/component_list.html'
|
||||
actions = {
|
||||
**DEFAULT_ACTION_PERMISSIONS,
|
||||
'bulk_rename': {'change'},
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(PowerPort)
|
||||
@@ -2697,11 +2732,6 @@ class PowerOutletListView(generic.ObjectListView):
|
||||
filterset = filtersets.PowerOutletFilterSet
|
||||
filterset_form = forms.PowerOutletFilterForm
|
||||
table = tables.PowerOutletTable
|
||||
template_name = 'dcim/component_list.html'
|
||||
actions = {
|
||||
**DEFAULT_ACTION_PERMISSIONS,
|
||||
'bulk_rename': {'change'},
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(PowerOutlet)
|
||||
@@ -2772,11 +2802,6 @@ class InterfaceListView(generic.ObjectListView):
|
||||
filterset = filtersets.InterfaceFilterSet
|
||||
filterset_form = forms.InterfaceFilterForm
|
||||
table = tables.InterfaceTable
|
||||
template_name = 'dcim/component_list.html'
|
||||
actions = {
|
||||
**DEFAULT_ACTION_PERMISSIONS,
|
||||
'bulk_rename': {'change'},
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(Interface)
|
||||
@@ -2920,11 +2945,6 @@ class FrontPortListView(generic.ObjectListView):
|
||||
filterset = filtersets.FrontPortFilterSet
|
||||
filterset_form = forms.FrontPortFilterForm
|
||||
table = tables.FrontPortTable
|
||||
template_name = 'dcim/component_list.html'
|
||||
actions = {
|
||||
**DEFAULT_ACTION_PERMISSIONS,
|
||||
'bulk_rename': {'change'},
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(FrontPort)
|
||||
@@ -2995,11 +3015,6 @@ class RearPortListView(generic.ObjectListView):
|
||||
filterset = filtersets.RearPortFilterSet
|
||||
filterset_form = forms.RearPortFilterForm
|
||||
table = tables.RearPortTable
|
||||
template_name = 'dcim/component_list.html'
|
||||
actions = {
|
||||
**DEFAULT_ACTION_PERMISSIONS,
|
||||
'bulk_rename': {'change'},
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(RearPort)
|
||||
@@ -3070,11 +3085,6 @@ class ModuleBayListView(generic.ObjectListView):
|
||||
filterset = filtersets.ModuleBayFilterSet
|
||||
filterset_form = forms.ModuleBayFilterForm
|
||||
table = tables.ModuleBayTable
|
||||
template_name = 'dcim/component_list.html'
|
||||
actions = {
|
||||
**DEFAULT_ACTION_PERMISSIONS,
|
||||
'bulk_rename': {'change'},
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(ModuleBay)
|
||||
@@ -3136,11 +3146,6 @@ class DeviceBayListView(generic.ObjectListView):
|
||||
filterset = filtersets.DeviceBayFilterSet
|
||||
filterset_form = forms.DeviceBayFilterForm
|
||||
table = tables.DeviceBayTable
|
||||
template_name = 'dcim/component_list.html'
|
||||
actions = {
|
||||
**DEFAULT_ACTION_PERMISSIONS,
|
||||
'bulk_rename': {'change'},
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(DeviceBay)
|
||||
@@ -3283,11 +3288,6 @@ class InventoryItemListView(generic.ObjectListView):
|
||||
filterset = filtersets.InventoryItemFilterSet
|
||||
filterset_form = forms.InventoryItemFilterForm
|
||||
table = tables.InventoryItemTable
|
||||
template_name = 'dcim/component_list.html'
|
||||
actions = {
|
||||
**DEFAULT_ACTION_PERMISSIONS,
|
||||
'bulk_rename': {'change'},
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(InventoryItem)
|
||||
@@ -3410,6 +3410,11 @@ class InventoryItemRoleBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class InventoryItemRoleBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = InventoryItemRole.objects.annotate(
|
||||
@@ -3607,6 +3612,12 @@ class CableBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class CableBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Cable.objects.prefetch_related(
|
||||
@@ -3627,9 +3638,7 @@ class ConsoleConnectionsListView(generic.ObjectListView):
|
||||
filterset_form = forms.ConsoleConnectionFilterForm
|
||||
table = tables.ConsoleConnectionTable
|
||||
template_name = 'dcim/connections_list.html'
|
||||
actions = {
|
||||
'export': {'view'},
|
||||
}
|
||||
actions = (BulkExport,)
|
||||
|
||||
def get_extra_context(self, request):
|
||||
return {
|
||||
@@ -3643,9 +3652,7 @@ class PowerConnectionsListView(generic.ObjectListView):
|
||||
filterset_form = forms.PowerConnectionFilterForm
|
||||
table = tables.PowerConnectionTable
|
||||
template_name = 'dcim/connections_list.html'
|
||||
actions = {
|
||||
'export': {'view'},
|
||||
}
|
||||
actions = (BulkExport,)
|
||||
|
||||
def get_extra_context(self, request):
|
||||
return {
|
||||
@@ -3659,9 +3666,7 @@ class InterfaceConnectionsListView(generic.ObjectListView):
|
||||
filterset_form = forms.InterfaceConnectionFilterForm
|
||||
table = tables.InterfaceConnectionTable
|
||||
template_name = 'dcim/connections_list.html'
|
||||
actions = {
|
||||
'export': {'view'},
|
||||
}
|
||||
actions = (BulkExport,)
|
||||
|
||||
def get_extra_context(self, request):
|
||||
return {
|
||||
@@ -3905,6 +3910,11 @@ class VirtualChassisBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class VirtualChassisBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VirtualChassis.objects.all()
|
||||
@@ -3962,6 +3972,11 @@ class PowerPanelBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class PowerPanelBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = PowerPanel.objects.annotate(
|
||||
@@ -4014,6 +4029,11 @@ class PowerFeedBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class PowerFeedBulkDisconnectView(BulkDisconnectView):
|
||||
queryset = PowerFeed.objects.all()
|
||||
@@ -4042,6 +4062,7 @@ class VirtualDeviceContextListView(generic.ObjectListView):
|
||||
filterset = filtersets.VirtualDeviceContextFilterSet
|
||||
filterset_form = forms.VirtualDeviceContextFilterForm
|
||||
table = tables.VirtualDeviceContextTable
|
||||
actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
|
||||
|
||||
|
||||
@register_model_view(VirtualDeviceContext)
|
||||
@@ -4086,6 +4107,11 @@ class VirtualDeviceContextBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class VirtualDeviceContextBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VirtualDeviceContext.objects.all()
|
||||
@@ -4103,6 +4129,7 @@ class MACAddressListView(generic.ObjectListView):
|
||||
filterset = filtersets.MACAddressFilterSet
|
||||
filterset_form = forms.MACAddressFilterForm
|
||||
table = tables.MACAddressTable
|
||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
||||
|
||||
|
||||
@register_model_view(MACAddress)
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import functools
|
||||
import operator
|
||||
import re
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
__all__ = (
|
||||
'Condition',
|
||||
'ConditionSet',
|
||||
'InvalidCondition',
|
||||
)
|
||||
|
||||
|
||||
AND = 'and'
|
||||
OR = 'or'
|
||||
|
||||
@@ -20,10 +19,6 @@ def is_ruleset(data):
|
||||
return type(data) is dict and len(data) == 1 and list(data.keys())[0] in (AND, OR)
|
||||
|
||||
|
||||
class InvalidCondition(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Condition:
|
||||
"""
|
||||
An individual conditional rule that evaluates a single attribute and its value.
|
||||
@@ -66,7 +61,6 @@ class Condition:
|
||||
|
||||
self.attr = attr
|
||||
self.value = value
|
||||
self.op = op
|
||||
self.eval_func = getattr(self, f'eval_{op}')
|
||||
self.negate = negate
|
||||
|
||||
@@ -76,17 +70,16 @@ class Condition:
|
||||
"""
|
||||
def _get(obj, key):
|
||||
if isinstance(obj, list):
|
||||
return [operator.getitem(item or {}, key) for item in obj]
|
||||
return operator.getitem(obj or {}, key)
|
||||
return [dict.get(i, key) for i in obj]
|
||||
|
||||
return dict.get(obj, key)
|
||||
|
||||
try:
|
||||
value = functools.reduce(_get, self.attr.split('.'), data)
|
||||
except KeyError:
|
||||
raise InvalidCondition(f"Invalid key path: {self.attr}")
|
||||
try:
|
||||
result = self.eval_func(value)
|
||||
except TypeError as e:
|
||||
raise InvalidCondition(f"Invalid data type at '{self.attr}' for '{self.op}' evaluation: {e}")
|
||||
except TypeError:
|
||||
# Invalid key path
|
||||
value = None
|
||||
result = self.eval_func(value)
|
||||
|
||||
if self.negate:
|
||||
return not result
|
||||
|
||||
@@ -192,5 +192,5 @@ def flush_events(events):
|
||||
try:
|
||||
func = import_string(name)
|
||||
func(events)
|
||||
except ImportError as e:
|
||||
except Exception as e:
|
||||
logger.error(_("Cannot import events pipeline {name} error: {error}").format(name=name, error=e))
|
||||
|
||||
@@ -90,7 +90,10 @@ class ScriptJob(JobRunner):
|
||||
request: The WSGI request associated with this execution (if any)
|
||||
commit: Passed through to Script.run()
|
||||
"""
|
||||
script = ScriptModel.objects.get(pk=self.job.object_id).python_class()
|
||||
script_model = ScriptModel.objects.get(pk=self.job.object_id)
|
||||
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
|
||||
if request:
|
||||
@@ -100,6 +103,7 @@ class ScriptJob(JobRunner):
|
||||
|
||||
# Add the current request as a property of the script
|
||||
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
|
||||
# change logging, event rules, etc.
|
||||
|
||||
@@ -18,22 +18,9 @@ class Empty(Lookup):
|
||||
return f"CAST(LENGTH({sql}) AS BOOLEAN) IS TRUE", params
|
||||
|
||||
|
||||
class NetHost(Lookup):
|
||||
"""
|
||||
Similar to ipam.lookups.NetHost, but casts the field to INET.
|
||||
"""
|
||||
lookup_name = 'net_host'
|
||||
|
||||
def as_sql(self, qn, connection):
|
||||
lhs, lhs_params = self.process_lhs(qn, connection)
|
||||
rhs, rhs_params = self.process_rhs(qn, connection)
|
||||
params = lhs_params + rhs_params
|
||||
return 'HOST(CAST(%s AS INET)) = HOST(%s)' % (lhs, rhs), params
|
||||
|
||||
|
||||
class NetContainsOrEquals(Lookup):
|
||||
"""
|
||||
Similar to ipam.lookups.NetContainsOrEquals, but casts the field to INET.
|
||||
This lookup has the same functionality as the one from the ipam app except lhs is cast to inet
|
||||
"""
|
||||
lookup_name = 'net_contains_or_equals'
|
||||
|
||||
@@ -45,5 +32,4 @@ class NetContainsOrEquals(Lookup):
|
||||
|
||||
|
||||
CharField.register_lookup(Empty)
|
||||
CachedValueField.register_lookup(NetHost)
|
||||
CachedValueField.register_lookup(NetContainsOrEquals)
|
||||
|
||||
@@ -13,7 +13,7 @@ from rest_framework.utils.encoders import JSONEncoder
|
||||
|
||||
from core.models import ObjectType
|
||||
from extras.choices import *
|
||||
from extras.conditions import ConditionSet, InvalidCondition
|
||||
from extras.conditions import ConditionSet
|
||||
from extras.constants import *
|
||||
from extras.utils import image_upload
|
||||
from extras.models.mixins import RenderTemplateMixin
|
||||
@@ -142,15 +142,7 @@ class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLogged
|
||||
if not self.conditions:
|
||||
return True
|
||||
|
||||
logger = logging.getLogger('netbox.event_rules')
|
||||
|
||||
try:
|
||||
result = ConditionSet(self.conditions).eval(data)
|
||||
logger.debug(f'{self.name}: Evaluated as {result}')
|
||||
return result
|
||||
except InvalidCondition as e:
|
||||
logger.error(f"{self.name}: Evaluation failed. {e}")
|
||||
return False
|
||||
return ConditionSet(self.conditions).eval(data)
|
||||
|
||||
|
||||
class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.test import TestCase
|
||||
from core.events import *
|
||||
from dcim.choices import SiteStatusChoices
|
||||
from dcim.models import Site
|
||||
from extras.conditions import Condition, ConditionSet, InvalidCondition
|
||||
from extras.conditions import Condition, ConditionSet
|
||||
from extras.events import serialize_for_event
|
||||
from extras.forms import EventRuleForm
|
||||
from extras.models import EventRule, Webhook
|
||||
@@ -12,11 +12,16 @@ from extras.models import EventRule, Webhook
|
||||
|
||||
class ConditionTestCase(TestCase):
|
||||
|
||||
def test_dotted_path_access(self):
|
||||
c = Condition('a.b.c', 1, 'eq')
|
||||
self.assertTrue(c.eval({'a': {'b': {'c': 1}}}))
|
||||
self.assertFalse(c.eval({'a': {'b': {'c': 2}}}))
|
||||
self.assertFalse(c.eval({'a': {'b': {'x': 1}}}))
|
||||
|
||||
def test_undefined_attr(self):
|
||||
c = Condition('x', 1, 'eq')
|
||||
self.assertFalse(c.eval({}))
|
||||
self.assertTrue(c.eval({'x': 1}))
|
||||
with self.assertRaises(InvalidCondition):
|
||||
c.eval({})
|
||||
|
||||
#
|
||||
# Validation tests
|
||||
@@ -32,13 +37,10 @@ class ConditionTestCase(TestCase):
|
||||
# dict type is unsupported
|
||||
Condition('x', 1, dict())
|
||||
|
||||
def test_invalid_op_types(self):
|
||||
def test_invalid_op_type(self):
|
||||
with self.assertRaises(ValueError):
|
||||
# 'gt' supports only numeric values
|
||||
Condition('x', 'foo', 'gt')
|
||||
with self.assertRaises(ValueError):
|
||||
# 'in' supports only iterable values
|
||||
Condition('x', 123, 'in')
|
||||
|
||||
#
|
||||
# Nested attrs tests
|
||||
@@ -48,10 +50,7 @@ class ConditionTestCase(TestCase):
|
||||
c = Condition('x.y.z', 1)
|
||||
self.assertTrue(c.eval({'x': {'y': {'z': 1}}}))
|
||||
self.assertFalse(c.eval({'x': {'y': {'z': 2}}}))
|
||||
with self.assertRaises(InvalidCondition):
|
||||
c.eval({'x': {'y': None}})
|
||||
with self.assertRaises(InvalidCondition):
|
||||
c.eval({'x': {'y': {'a': 1}}})
|
||||
self.assertFalse(c.eval({'a': {'b': {'c': 1}}}))
|
||||
|
||||
#
|
||||
# Operator tests
|
||||
@@ -75,31 +74,23 @@ class ConditionTestCase(TestCase):
|
||||
c = Condition('x', 1, 'gt')
|
||||
self.assertTrue(c.eval({'x': 2}))
|
||||
self.assertFalse(c.eval({'x': 1}))
|
||||
with self.assertRaises(InvalidCondition):
|
||||
c.eval({'x': 'foo'}) # Invalid type
|
||||
|
||||
def test_gte(self):
|
||||
c = Condition('x', 1, 'gte')
|
||||
self.assertTrue(c.eval({'x': 2}))
|
||||
self.assertTrue(c.eval({'x': 1}))
|
||||
self.assertFalse(c.eval({'x': 0}))
|
||||
with self.assertRaises(InvalidCondition):
|
||||
c.eval({'x': 'foo'}) # Invalid type
|
||||
|
||||
def test_lt(self):
|
||||
c = Condition('x', 2, 'lt')
|
||||
self.assertTrue(c.eval({'x': 1}))
|
||||
self.assertFalse(c.eval({'x': 2}))
|
||||
with self.assertRaises(InvalidCondition):
|
||||
c.eval({'x': 'foo'}) # Invalid type
|
||||
|
||||
def test_lte(self):
|
||||
c = Condition('x', 2, 'lte')
|
||||
self.assertTrue(c.eval({'x': 1}))
|
||||
self.assertTrue(c.eval({'x': 2}))
|
||||
self.assertFalse(c.eval({'x': 3}))
|
||||
with self.assertRaises(InvalidCondition):
|
||||
c.eval({'x': 'foo'}) # Invalid type
|
||||
|
||||
def test_in(self):
|
||||
c = Condition('x', [1, 2, 3], 'in')
|
||||
@@ -115,8 +106,6 @@ class ConditionTestCase(TestCase):
|
||||
c = Condition('x', 1, 'contains')
|
||||
self.assertTrue(c.eval({'x': [1, 2, 3]}))
|
||||
self.assertFalse(c.eval({'x': [2, 3, 4]}))
|
||||
with self.assertRaises(InvalidCondition):
|
||||
c.eval({'x': 123}) # Invalid type
|
||||
|
||||
def test_contains_negated(self):
|
||||
c = Condition('x', 1, 'contains', negate=True)
|
||||
|
||||
@@ -14,12 +14,13 @@ from jinja2.exceptions import TemplateError
|
||||
|
||||
from core.choices import ManagedFileRootPathChoices
|
||||
from core.models import Job
|
||||
from core.object_actions import BulkSync
|
||||
from dcim.models import Device, DeviceRole, Platform
|
||||
from extras.choices import LogLevelChoices
|
||||
from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
|
||||
from extras.dashboard.utils import get_widget_class
|
||||
from extras.utils import SharedObjectViewMixin
|
||||
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
||||
from netbox.object_actions import *
|
||||
from netbox.views import generic
|
||||
from netbox.views.generic.mixins import TableMixin
|
||||
from utilities.forms import ConfirmationForm, get_field_value
|
||||
@@ -96,6 +97,11 @@ class CustomFieldBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class CustomFieldBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = CustomField.objects.select_related('choice_set')
|
||||
@@ -165,6 +171,11 @@ class CustomFieldChoiceSetBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class CustomFieldChoiceSetBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = CustomFieldChoiceSet.objects.all()
|
||||
@@ -215,6 +226,11 @@ class CustomLinkBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class CustomLinkBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = CustomLink.objects.all()
|
||||
@@ -232,11 +248,7 @@ class ExportTemplateListView(generic.ObjectListView):
|
||||
filterset = filtersets.ExportTemplateFilterSet
|
||||
filterset_form = forms.ExportTemplateFilterForm
|
||||
table = tables.ExportTemplateTable
|
||||
template_name = 'extras/exporttemplate_list.html'
|
||||
actions = {
|
||||
**DEFAULT_ACTION_PERMISSIONS,
|
||||
'bulk_sync': {'sync'},
|
||||
}
|
||||
actions = (AddObject, BulkImport, BulkSync, BulkExport, BulkEdit, BulkRename, BulkDelete)
|
||||
|
||||
|
||||
@register_model_view(ExportTemplate)
|
||||
@@ -270,6 +282,11 @@ class ExportTemplateBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class ExportTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ExportTemplate.objects.all()
|
||||
@@ -330,6 +347,11 @@ class SavedFilterBulkEditView(SharedObjectViewMixin, generic.BulkEditView):
|
||||
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)
|
||||
class SavedFilterBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView):
|
||||
queryset = SavedFilter.objects.all()
|
||||
@@ -347,9 +369,7 @@ class TableConfigListView(SharedObjectViewMixin, generic.ObjectListView):
|
||||
filterset = filtersets.TableConfigFilterSet
|
||||
filterset_form = forms.TableConfigFilterForm
|
||||
table = tables.TableConfigTable
|
||||
actions = {
|
||||
'export': {'view'},
|
||||
}
|
||||
actions = (BulkExport, BulkEdit, BulkRename, BulkDelete)
|
||||
|
||||
|
||||
@register_model_view(TableConfig)
|
||||
@@ -389,6 +409,11 @@ class TableConfigBulkEditView(SharedObjectViewMixin, generic.BulkEditView):
|
||||
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)
|
||||
class TableConfigBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView):
|
||||
queryset = TableConfig.objects.all()
|
||||
@@ -470,6 +495,11 @@ class NotificationGroupBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class NotificationGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = NotificationGroup.objects.all()
|
||||
@@ -616,6 +646,11 @@ class WebhookBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class WebhookBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Webhook.objects.all()
|
||||
@@ -666,6 +701,11 @@ class EventRuleBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class EventRuleBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = EventRule.objects.all()
|
||||
@@ -740,6 +780,11 @@ class TagBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class TagBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Tag.objects.annotate(
|
||||
@@ -758,13 +803,7 @@ class ConfigContextListView(generic.ObjectListView):
|
||||
filterset = filtersets.ConfigContextFilterSet
|
||||
filterset_form = forms.ConfigContextFilterForm
|
||||
table = tables.ConfigContextTable
|
||||
template_name = 'extras/configcontext_list.html'
|
||||
actions = {
|
||||
'add': {'add'},
|
||||
'bulk_edit': {'change'},
|
||||
'bulk_delete': {'delete'},
|
||||
'bulk_sync': {'sync'},
|
||||
}
|
||||
actions = (AddObject, BulkSync, BulkEdit, BulkRename, BulkDelete)
|
||||
|
||||
|
||||
@register_model_view(ConfigContext)
|
||||
@@ -825,6 +864,11 @@ class ConfigContextBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class ConfigContextBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
@@ -877,11 +921,7 @@ class ConfigTemplateListView(generic.ObjectListView):
|
||||
filterset = filtersets.ConfigTemplateFilterSet
|
||||
filterset_form = forms.ConfigTemplateFilterForm
|
||||
table = tables.ConfigTemplateTable
|
||||
template_name = 'extras/configtemplate_list.html'
|
||||
actions = {
|
||||
**DEFAULT_ACTION_PERMISSIONS,
|
||||
'bulk_sync': {'sync'},
|
||||
}
|
||||
actions = (AddObject, BulkImport, BulkExport, BulkSync, BulkEdit, BulkRename, BulkDelete)
|
||||
|
||||
|
||||
@register_model_view(ConfigTemplate)
|
||||
@@ -915,6 +955,11 @@ class ConfigTemplateBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class ConfigTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ConfigTemplate.objects.all()
|
||||
@@ -992,9 +1037,7 @@ class ImageAttachmentListView(generic.ObjectListView):
|
||||
filterset = filtersets.ImageAttachmentFilterSet
|
||||
filterset_form = forms.ImageAttachmentFilterForm
|
||||
table = tables.ImageAttachmentTable
|
||||
actions = {
|
||||
'export': {'view'},
|
||||
}
|
||||
actions = (BulkExport,)
|
||||
|
||||
|
||||
@register_model_view(ImageAttachment, 'add', detail=False)
|
||||
@@ -1038,12 +1081,7 @@ class JournalEntryListView(generic.ObjectListView):
|
||||
filterset = filtersets.JournalEntryFilterSet
|
||||
filterset_form = forms.JournalEntryFilterForm
|
||||
table = tables.JournalEntryTable
|
||||
actions = {
|
||||
'export': {'view'},
|
||||
'bulk_import': {'add'},
|
||||
'bulk_edit': {'change'},
|
||||
'bulk_delete': {'delete'},
|
||||
}
|
||||
actions = (BulkImport, BulkEdit, BulkDelete)
|
||||
|
||||
|
||||
@register_model_view(JournalEntry)
|
||||
|
||||
@@ -162,11 +162,6 @@ class Aggregate(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
|
||||
return self.prefix.version
|
||||
return None
|
||||
|
||||
@property
|
||||
def ipv6_full(self):
|
||||
if self.prefix and self.prefix.version == 6:
|
||||
return netaddr.IPAddress(self.prefix).format(netaddr.ipv6_full)
|
||||
|
||||
def get_child_prefixes(self):
|
||||
"""
|
||||
Return all Prefixes within this Aggregate
|
||||
@@ -335,11 +330,6 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
||||
def mask_length(self):
|
||||
return self.prefix.prefixlen if self.prefix else None
|
||||
|
||||
@property
|
||||
def ipv6_full(self):
|
||||
if self.prefix and self.prefix.version == 6:
|
||||
return netaddr.IPAddress(self.prefix).format(netaddr.ipv6_full)
|
||||
|
||||
@property
|
||||
def depth(self):
|
||||
return self._depth
|
||||
@@ -818,11 +808,6 @@ class IPAddress(ContactsMixin, PrimaryModel):
|
||||
self._original_assigned_object_id = self.__dict__.get('assigned_object_id')
|
||||
self._original_assigned_object_type_id = self.__dict__.get('assigned_object_type_id')
|
||||
|
||||
@property
|
||||
def ipv6_full(self):
|
||||
if self.address and self.address.version == 6:
|
||||
return netaddr.IPAddress(self.address).format(netaddr.ipv6_full)
|
||||
|
||||
def get_duplicates(self):
|
||||
return IPAddress.objects.filter(
|
||||
vrf=self.vrf,
|
||||
|
||||
@@ -10,6 +10,7 @@ from dcim.filtersets import InterfaceFilterSet
|
||||
from dcim.forms import InterfaceFilterForm
|
||||
from dcim.models import Device, Interface, Site
|
||||
from ipam.tables import VLANTranslationRuleTable
|
||||
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
|
||||
from netbox.views import generic
|
||||
from utilities.query import count_related
|
||||
from utilities.tables import get_table_ordering
|
||||
@@ -86,6 +87,11 @@ class VRFBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class VRFBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VRF.objects.all()
|
||||
@@ -136,6 +142,11 @@ class RouteTargetBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class RouteTargetBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RouteTarget.objects.all()
|
||||
@@ -195,6 +206,11 @@ class RIRBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class RIRBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RIR.objects.annotate(
|
||||
@@ -268,6 +284,11 @@ class ASNRangeBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class ASNRangeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ASNRange.objects.annotate_asn_counts()
|
||||
@@ -335,6 +356,11 @@ class ASNBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class ASNBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ASN.objects.annotate(
|
||||
@@ -356,6 +382,7 @@ class AggregateListView(generic.ObjectListView):
|
||||
filterset = filtersets.AggregateFilterSet
|
||||
filterset_form = forms.AggregateFilterForm
|
||||
table = tables.AggregateTable
|
||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
||||
|
||||
|
||||
@register_model_view(Aggregate)
|
||||
@@ -488,6 +515,11 @@ class RoleBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class RoleBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Role.objects.all()
|
||||
@@ -506,6 +538,7 @@ class PrefixListView(generic.ObjectListView):
|
||||
filterset_form = forms.PrefixFilterForm
|
||||
table = tables.PrefixTable
|
||||
template_name = 'ipam/prefix_list.html'
|
||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
||||
|
||||
|
||||
@register_model_view(Prefix)
|
||||
@@ -766,6 +799,11 @@ class IPRangeBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class IPRangeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = IPRange.objects.all()
|
||||
@@ -783,6 +821,7 @@ class IPAddressListView(generic.ObjectListView):
|
||||
filterset = filtersets.IPAddressFilterSet
|
||||
filterset_form = forms.IPAddressFilterForm
|
||||
table = tables.IPAddressTable
|
||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
||||
|
||||
|
||||
@register_model_view(IPAddress)
|
||||
@@ -1006,6 +1045,11 @@ class VLANGroupBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class VLANGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
|
||||
@@ -1095,6 +1139,11 @@ class VLANTranslationPolicyBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class VLANTranslationPolicyBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VLANTranslationPolicy.objects.all()
|
||||
@@ -1112,6 +1161,7 @@ class VLANTranslationRuleListView(generic.ObjectListView):
|
||||
filterset = filtersets.VLANTranslationRuleFilterSet
|
||||
filterset_form = forms.VLANTranslationRuleFilterForm
|
||||
table = tables.VLANTranslationRuleTable
|
||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
|
||||
|
||||
|
||||
@register_model_view(VLANTranslationRule)
|
||||
@@ -1244,6 +1294,11 @@ class FHRPGroupBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class FHRPGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = FHRPGroup.objects.all()
|
||||
@@ -1371,6 +1426,11 @@ class VLANBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class VLANBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VLAN.objects.all()
|
||||
@@ -1421,6 +1481,11 @@ class ServiceTemplateBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class ServiceTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ServiceTemplate.objects.all()
|
||||
@@ -1488,6 +1553,11 @@ class ServiceBulkEditView(generic.BulkEditView):
|
||||
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)
|
||||
class ServiceBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Service.objects.prefetch_related('parent')
|
||||
|
||||
@@ -28,7 +28,8 @@ ADVISORY_LOCK_KEYS = {
|
||||
'job-schedules': 110100,
|
||||
}
|
||||
|
||||
# Default view action permission mapping
|
||||
# TODO: Remove in NetBox v4.6
|
||||
# Legacy default view action permission mapping
|
||||
DEFAULT_ACTION_PERMISSIONS = {
|
||||
'add': {'add'},
|
||||
'export': {'view'},
|
||||
|
||||
@@ -8,7 +8,6 @@ from django_pglocks import advisory_lock
|
||||
from rq.timeouts import JobTimeoutException
|
||||
|
||||
from core.choices import JobStatusChoices
|
||||
from core.exceptions import JobFailed
|
||||
from core.models import Job, ObjectType
|
||||
from netbox.constants import ADVISORY_LOCK_KEYS
|
||||
from netbox.registry import registry
|
||||
@@ -35,6 +34,17 @@ def system_job(interval):
|
||||
return _wrapper
|
||||
|
||||
|
||||
class JobLogHandler(logging.Handler):
|
||||
|
||||
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):
|
||||
"""
|
||||
Background Job helper class.
|
||||
@@ -53,6 +63,11 @@ class JobRunner(ABC):
|
||||
"""
|
||||
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
|
||||
def name(cls):
|
||||
return getattr(cls.Meta, 'name', cls.__name__)
|
||||
@@ -74,21 +89,15 @@ class JobRunner(ABC):
|
||||
This method is called by the Job Scheduler to handle the execution of all job commands. It will maintain the
|
||||
job's metadata and handle errors. For periodic jobs, a new job is automatically scheduled using its `interval`.
|
||||
"""
|
||||
logger = logging.getLogger('netbox.jobs')
|
||||
|
||||
try:
|
||||
job.start()
|
||||
cls(job).run(*args, **kwargs)
|
||||
job.terminate()
|
||||
|
||||
except JobFailed:
|
||||
logger.warning(f"Job {job} failed")
|
||||
job.terminate(status=JobStatusChoices.STATUS_FAILED)
|
||||
|
||||
except Exception as e:
|
||||
job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=repr(e))
|
||||
if type(e) is JobTimeoutException:
|
||||
logger.error(e)
|
||||
logging.error(e)
|
||||
|
||||
# If the executed job is a periodic job, schedule its next execution at the specified interval.
|
||||
finally:
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Sequence, Optional
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
|
||||
__all__ = (
|
||||
'get_model_item',
|
||||
@@ -24,46 +22,20 @@ class MenuItemButton:
|
||||
link: str
|
||||
title: str
|
||||
icon_class: str
|
||||
_url: Optional[str] = None
|
||||
permissions: Optional[Sequence[str]] = ()
|
||||
color: Optional[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.link:
|
||||
self._url = reverse_lazy(self.link)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self._url
|
||||
|
||||
@url.setter
|
||||
def url(self, value):
|
||||
self._url = value
|
||||
|
||||
|
||||
@dataclass
|
||||
class MenuItem:
|
||||
|
||||
link: str
|
||||
link_text: str
|
||||
_url: Optional[str] = None
|
||||
permissions: Optional[Sequence[str]] = ()
|
||||
auth_required: Optional[bool] = False
|
||||
staff_only: Optional[bool] = False
|
||||
buttons: Optional[Sequence[MenuItemButton]] = ()
|
||||
|
||||
def __post_init__(self):
|
||||
if self.link:
|
||||
self._url = reverse_lazy(self.link)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self._url
|
||||
|
||||
@url.setter
|
||||
def url(self, value):
|
||||
self._url = value
|
||||
|
||||
|
||||
@dataclass
|
||||
class MenuGroup:
|
||||
|
||||
180
netbox/netbox/object_actions.py
Normal file
180
netbox/netbox/object_actions.py
Normal file
@@ -0,0 +1,180 @@
|
||||
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'
|
||||
@@ -1,4 +1,3 @@
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
@@ -33,23 +32,17 @@ class PluginMenuItem:
|
||||
This class represents a navigation menu item. This constitutes primary link and its text, but also allows for
|
||||
specifying additional link buttons that appear to the right of the item in the van menu.
|
||||
|
||||
Links are specified as Django reverse URL strings suitable for rendering via {% url item.link %}.
|
||||
Alternatively, a pre-generated url can be set on the object which will be rendered literally.
|
||||
Links are specified as Django reverse URL strings.
|
||||
Buttons are each specified as a list of PluginMenuButton instances.
|
||||
"""
|
||||
permissions = []
|
||||
buttons = []
|
||||
_url = None
|
||||
|
||||
def __init__(
|
||||
self, link, link_text, auth_required=False, staff_only=False, permissions=None, buttons=None
|
||||
):
|
||||
def __init__(self, link, link_text, auth_required=False, staff_only=False, permissions=None, buttons=None):
|
||||
self.link = link
|
||||
self.link_text = link_text
|
||||
self.auth_required = auth_required
|
||||
self.staff_only = staff_only
|
||||
if link:
|
||||
self._url = reverse_lazy(link)
|
||||
if permissions is not None:
|
||||
if type(permissions) not in (list, tuple):
|
||||
raise TypeError(_("Permissions must be passed as a tuple or list."))
|
||||
@@ -59,14 +52,6 @@ class PluginMenuItem:
|
||||
raise TypeError(_("Buttons must be passed as a tuple or list."))
|
||||
self.buttons = buttons
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self._url
|
||||
|
||||
@url.setter
|
||||
def url(self, value):
|
||||
self._url = value
|
||||
|
||||
|
||||
class PluginMenuButton:
|
||||
"""
|
||||
@@ -75,14 +60,11 @@ class PluginMenuButton:
|
||||
"""
|
||||
color = ButtonColorChoices.DEFAULT
|
||||
permissions = []
|
||||
_url = None
|
||||
|
||||
def __init__(self, link, title, icon_class, color=None, permissions=None):
|
||||
self.link = link
|
||||
self.title = title
|
||||
self.icon_class = icon_class
|
||||
if link:
|
||||
self._url = reverse_lazy(link)
|
||||
if permissions is not None:
|
||||
if type(permissions) not in (list, tuple):
|
||||
raise TypeError(_("Permissions must be passed as a tuple or list."))
|
||||
@@ -91,11 +73,3 @@ class PluginMenuButton:
|
||||
if color not in ButtonColorChoices.values():
|
||||
raise ValueError(_("Button color must be a choice within ButtonColorChoices."))
|
||||
self.color = color
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self._url
|
||||
|
||||
@url.setter
|
||||
def url(self, value):
|
||||
self._url = value
|
||||
|
||||
@@ -115,13 +115,11 @@ class CachedValueSearchBackend(SearchBackend):
|
||||
if lookup in (LookupTypes.STARTSWITH, LookupTypes.ENDSWITH):
|
||||
# "Starts/ends with" matches are valid only on string values
|
||||
query_filter &= Q(type=FieldTypes.STRING)
|
||||
elif lookup in (LookupTypes.PARTIAL, LookupTypes.EXACT):
|
||||
elif lookup == LookupTypes.PARTIAL:
|
||||
try:
|
||||
# If the value looks like an IP address, add extra filters for CIDR/INET values
|
||||
# If the value looks like an IP address, add an extra match for CIDR values
|
||||
address = str(netaddr.IPNetwork(value.strip()).cidr)
|
||||
query_filter |= Q(type=FieldTypes.INET) & Q(value__net_host=address)
|
||||
if lookup == LookupTypes.PARTIAL:
|
||||
query_filter |= Q(type=FieldTypes.CIDR) & Q(value__net_contains_or_equals=address)
|
||||
query_filter |= Q(type=FieldTypes.CIDR) & Q(value__net_contains_or_equals=address)
|
||||
except (AddrFormatError, ValueError):
|
||||
pass
|
||||
|
||||
|
||||
@@ -7,15 +7,11 @@ from django_rq import get_queue
|
||||
from ..jobs import *
|
||||
from core.models import DataSource, Job
|
||||
from core.choices import JobStatusChoices
|
||||
from core.exceptions import JobFailed
|
||||
from utilities.testing import disable_warnings
|
||||
|
||||
|
||||
class TestJobRunner(JobRunner):
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
if kwargs.get('make_fail', False):
|
||||
raise JobFailed()
|
||||
pass
|
||||
|
||||
|
||||
class JobRunnerTestCase(TestCase):
|
||||
@@ -53,12 +49,6 @@ class JobRunnerTest(JobRunnerTestCase):
|
||||
|
||||
self.assertEqual(job.status, JobStatusChoices.STATUS_COMPLETED)
|
||||
|
||||
def test_handle_failed(self):
|
||||
with disable_warnings('netbox.jobs'):
|
||||
job = TestJobRunner.enqueue(immediate=True, make_fail=True)
|
||||
|
||||
self.assertEqual(job.status, JobStatusChoices.STATUS_FAILED)
|
||||
|
||||
def test_handle_errored(self):
|
||||
class ErroredJobRunner(TestJobRunner):
|
||||
EXP = Exception('Test error')
|
||||
|
||||
@@ -22,6 +22,7 @@ from core.models import ObjectType
|
||||
from core.signals import clear_events
|
||||
from extras.choices import CustomFieldUIEditableChoices
|
||||
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.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
|
||||
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
|
||||
@@ -54,12 +55,12 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
||||
Attributes:
|
||||
filterset: A django-filter FilterSet that is applied to the queryset
|
||||
filterset_form: The form class used to render filter options
|
||||
actions: A mapping of supported actions to their required permissions. When adding custom actions, bulk
|
||||
action names must be prefixed with `bulk_`. (See ActionsMixin.)
|
||||
actions: An iterable of ObjectAction subclasses (see ActionsMixin)
|
||||
"""
|
||||
template_name = 'generic/object_list.html'
|
||||
filterset = None
|
||||
filterset_form = None
|
||||
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkRename, BulkDelete)
|
||||
|
||||
def get_required_permission(self):
|
||||
return get_permission_for_model(self.queryset.model, 'view')
|
||||
@@ -150,13 +151,13 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
||||
|
||||
# Determine the available actions
|
||||
actions = self.get_permitted_actions(request.user)
|
||||
has_bulk_actions = any([a.startswith('bulk_') for a in actions])
|
||||
has_table_actions = any(action.multi for action in actions)
|
||||
|
||||
if 'export' in request.GET:
|
||||
|
||||
# Export the current table view
|
||||
if request.GET['export'] == 'table':
|
||||
table = self.get_table(self.queryset, request, has_bulk_actions)
|
||||
table = self.get_table(self.queryset, request, has_table_actions)
|
||||
columns = [name for name, _ in table.selected_columns]
|
||||
return self.export_table(table, columns)
|
||||
|
||||
@@ -174,11 +175,11 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
||||
|
||||
# Fall back to default table/YAML export
|
||||
else:
|
||||
table = self.get_table(self.queryset, request, has_bulk_actions)
|
||||
table = self.get_table(self.queryset, request, has_table_actions)
|
||||
return self.export_table(table)
|
||||
|
||||
# Render the objects table
|
||||
table = self.get_table(self.queryset, request, has_bulk_actions)
|
||||
table = self.get_table(self.queryset, request, has_table_actions)
|
||||
|
||||
# If this is an HTMX request, return only the rendered table HTML
|
||||
if htmx_partial(request):
|
||||
@@ -729,7 +730,11 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
"""
|
||||
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'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -759,12 +764,12 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
replace = form.cleaned_data['replace']
|
||||
if form.cleaned_data['use_regex']:
|
||||
try:
|
||||
obj.new_name = re.sub(find, replace, obj.name or '')
|
||||
obj.new_name = re.sub(find, replace, getattr(obj, self.field_name, ''))
|
||||
# Catch regex group reference errors
|
||||
except re.error:
|
||||
obj.new_name = obj.name
|
||||
obj.new_name = getattr(obj, self.field_name)
|
||||
else:
|
||||
obj.new_name = (obj.name or '').replace(find, replace)
|
||||
obj.new_name = getattr(obj, self.field_name, '').replace(find, replace)
|
||||
renamed_pks.append(obj.pk)
|
||||
|
||||
return renamed_pks
|
||||
@@ -783,7 +788,7 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
|
||||
if '_apply' in request.POST:
|
||||
for obj in selected_objects:
|
||||
obj.name = obj.new_name
|
||||
setattr(obj, self.field_name, obj.new_name)
|
||||
obj.save()
|
||||
|
||||
# Enforce constrained permissions
|
||||
@@ -813,6 +818,7 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'field_name': self.field_name,
|
||||
'form': form,
|
||||
'obj_type_plural': self.queryset.model._meta.verbose_name_plural,
|
||||
'selected_objects': selected_objects,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from extras.models import TableConfig
|
||||
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
||||
from netbox import object_actions
|
||||
from utilities.permissions import get_permission_for_model
|
||||
|
||||
__all__ = (
|
||||
@@ -9,6 +9,18 @@ __all__ = (
|
||||
'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:
|
||||
"""
|
||||
@@ -19,7 +31,24 @@ class ActionsMixin:
|
||||
Standard actions include: add, import, export, bulk_edit, and bulk_delete. Some views extend this default map
|
||||
with custom actions, such as bulk_sync.
|
||||
"""
|
||||
actions = DEFAULT_ACTION_PERMISSIONS
|
||||
actions = tuple()
|
||||
|
||||
# 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):
|
||||
"""
|
||||
@@ -27,11 +56,15 @@ class ActionsMixin:
|
||||
"""
|
||||
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
|
||||
permitted_actions = []
|
||||
for action in self.actions:
|
||||
required_permissions = [
|
||||
get_permission_for_model(model, name) for name in self.actions.get(action, set())
|
||||
get_permission_for_model(model, perm) for perm in action.permissions_required
|
||||
]
|
||||
if not required_permissions or user.has_perms(required_permissions):
|
||||
permitted_actions.append(action)
|
||||
|
||||
@@ -14,6 +14,9 @@ from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
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.exceptions import AbortRequest, PermissionsViolation
|
||||
from utilities.forms import ConfirmationForm, restrict_form_fields
|
||||
@@ -36,7 +39,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ObjectView(BaseObjectView):
|
||||
class ObjectView(ActionsMixin, BaseObjectView):
|
||||
"""
|
||||
Retrieve a single object for display.
|
||||
|
||||
@@ -44,8 +47,10 @@ class ObjectView(BaseObjectView):
|
||||
|
||||
Attributes:
|
||||
tab: A ViewTab instance for the view
|
||||
actions: An iterable of ObjectAction subclasses (see ActionsMixin)
|
||||
"""
|
||||
tab = None
|
||||
actions = (CloneObject, EditObject, DeleteObject)
|
||||
|
||||
def get_required_permission(self):
|
||||
return get_permission_for_model(self.queryset.model, 'view')
|
||||
@@ -72,9 +77,11 @@ class ObjectView(BaseObjectView):
|
||||
request: The current request
|
||||
"""
|
||||
instance = self.get_object(**kwargs)
|
||||
actions = self.get_permitted_actions(request.user, model=instance)
|
||||
|
||||
return render(request, self.get_template_name(), {
|
||||
'object': instance,
|
||||
'actions': actions,
|
||||
'tab': self.tab,
|
||||
**self.get_extra_context(request, instance),
|
||||
})
|
||||
@@ -90,13 +97,13 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
|
||||
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_form: The form class used to render filter options
|
||||
actions: A mapping of supported actions to their required permissions. When adding custom actions, bulk
|
||||
action names must be prefixed with `bulk_`. (See ActionsMixin.)
|
||||
actions: An iterable of ObjectAction subclasses (see ActionsMixin)
|
||||
"""
|
||||
child_model = None
|
||||
table = None
|
||||
filterset = None
|
||||
filterset_form = None
|
||||
actions = (AddObject, BulkImport, BulkEdit, BulkExport, BulkDelete)
|
||||
template_name = 'generic/object_children.html'
|
||||
|
||||
def get_children(self, request, parent):
|
||||
@@ -138,10 +145,10 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
|
||||
|
||||
# Determine the available actions
|
||||
actions = self.get_permitted_actions(request.user, model=self.child_model)
|
||||
has_bulk_actions = any([a.startswith('bulk_') for a in actions])
|
||||
has_table_actions = any(action.multi for action in actions)
|
||||
|
||||
table_data = self.prep_table_data(request, child_objects, instance)
|
||||
table = self.get_table(table_data, request, has_bulk_actions)
|
||||
table = self.get_table(table_data, request, has_table_actions)
|
||||
|
||||
# If this is an HTMX request, return only the rendered table HTML
|
||||
if htmx_partial(request):
|
||||
|
||||
2
netbox/project-static/dist/netbox.css
vendored
2
netbox/project-static/dist/netbox.css
vendored
File diff suppressed because one or more lines are too long
10
netbox/project-static/dist/netbox.js
vendored
10
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
4
netbox/project-static/dist/netbox.js.map
vendored
4
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -23,13 +23,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "7.4.47",
|
||||
"@tabler/core": "1.4.0",
|
||||
"@tabler/core": "1.3.2",
|
||||
"bootstrap": "5.3.7",
|
||||
"clipboard": "2.0.11",
|
||||
"flatpickr": "4.6.13",
|
||||
"gridstack": "12.2.2",
|
||||
"htmx.org": "2.0.6",
|
||||
"query-string": "9.2.2",
|
||||
"gridstack": "12.2.1",
|
||||
"htmx.org": "2.0.5",
|
||||
"query-string": "9.2.1",
|
||||
"sass": "1.89.2",
|
||||
"tom-select": "2.4.3",
|
||||
"typeface-inter": "3.18.1",
|
||||
@@ -39,15 +39,15 @@
|
||||
"@types/bootstrap": "5.2.10",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/node": "^22.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.37.0",
|
||||
"@typescript-eslint/parser": "^8.37.0",
|
||||
"esbuild": "^0.25.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.1.0",
|
||||
"@typescript-eslint/parser": "^8.1.0",
|
||||
"esbuild": "^0.25.3",
|
||||
"esbuild-sass-plugin": "^3.3.1",
|
||||
"eslint": "<9.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.3",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "<5.5"
|
||||
},
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.rack-loading-container {
|
||||
min-height: 200px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
@@ -27,4 +27,3 @@
|
||||
@import 'custom/markdown';
|
||||
@import 'custom/misc';
|
||||
@import 'custom/notifications';
|
||||
@import 'custom/racks';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
||||
version: "4.3.4"
|
||||
version: "4.3.3"
|
||||
edition: "Community"
|
||||
published: "2025-07-15"
|
||||
published: "2025-06-26"
|
||||
|
||||
3
netbox/templates/core/buttons/bulk_sync.html
Normal file
3
netbox/templates/core/buttons/bulk_sync.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<button type="submit" name="_sync" {% formaction %}="{{ url }}" class="btn btn-primary">
|
||||
<i class="mdi mdi-sync" aria-hidden="true"></i> {{ label }}
|
||||
</button>
|
||||
@@ -11,12 +11,6 @@
|
||||
<li class="breadcrumb-item"><a href="{% url 'core:datafile_list' %}?source_id={{ object.source.pk }}">{{ object.source }}</a></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block control-buttons %}
|
||||
{% if request.user|can_delete:object %}
|
||||
{% delete_button object %}
|
||||
{% endif %}
|
||||
{% endblock control-buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
|
||||
@@ -22,12 +22,6 @@
|
||||
{% endif %}
|
||||
{% endblock breadcrumbs %}
|
||||
|
||||
{% block control-buttons %}
|
||||
{% if request.user|can_delete:object %}
|
||||
{% delete_button object %}
|
||||
{% endif %}
|
||||
{% endblock control-buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
|
||||
34
netbox/templates/core/job_log.html
Normal file
34
netbox/templates/core/job_log.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load perms %}
|
||||
{% load i18n %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% 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 content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
{% render_table table %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
71
netbox/templates/dcim/buttons/bulk_add_components.html
Normal file
71
netbox/templates/dcim/buttons/bulk_add_components.html
Normal file
@@ -0,0 +1,71 @@
|
||||
{% 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>
|
||||
3
netbox/templates/dcim/buttons/bulk_disconnect.html
Normal file
3
netbox/templates/dcim/buttons/bulk_disconnect.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<button type="submit" name="_disconnect" {% formaction %}="{{ url }}" class="btn btn-red">
|
||||
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i> {{ label }}
|
||||
</button>
|
||||
@@ -1,22 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,23 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,28 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,28 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,14 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,28 +0,0 @@
|
||||
{% 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,30 +1,5 @@
|
||||
{% extends 'dcim/device/components_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
{% extends 'generic/object_children.html' %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,14 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,28 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,28 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,28 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,89 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,25 +0,0 @@
|
||||
{% 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,17 +1,6 @@
|
||||
{% load i18n %}
|
||||
<div style="margin-left: -30px">
|
||||
<div
|
||||
hx-get="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{ face }}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}"
|
||||
hx-trigger="intersect"
|
||||
hx-swap="outerHTML"
|
||||
aria-label="{% trans "Rack elevation" %}"
|
||||
>
|
||||
<div class="d-flex justify-content-center align-items-center rack-loading-container">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">{% trans "Loading..." %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<object data="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{face}}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}" class="rack_elevation" aria-label="{% trans "Rack elevation" %}"></object>
|
||||
</div>
|
||||
<div class="text-center mt-3">
|
||||
<a class="btn btn-outline-primary" href="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{face}}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}" hx-boost="false">
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
{% 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,18 +1,8 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% 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 %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-4">
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,11 +0,0 @@
|
||||
{% extends 'generic/object_list.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block bulk_buttons %}
|
||||
{% if perms.extras.sync_configtemplate %}
|
||||
<button type="submit" name="_sync" {% formaction %}="{% url 'extras:configtemplate_bulk_sync' %}" class="btn btn-primary">
|
||||
<i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync Data" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
@@ -14,7 +14,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
<td>{{ object.description|markdown|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Base Choices</th>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{% extends 'generic/object_list.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block bulk_buttons %}
|
||||
{% if perms.extras.sync_configcontext %}
|
||||
<button type="submit" name="_sync" {% formaction %}="{% url 'extras:exporttemplate_bulk_sync' %}" class="btn btn-primary">
|
||||
<i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync Data" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
@@ -1,72 +0,0 @@
|
||||
{% extends 'generic/_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% comment %}
|
||||
Blocks:
|
||||
- title: Page title
|
||||
- tabs: Page tabs
|
||||
- content: Primary page content
|
||||
|
||||
Context:
|
||||
- form: The bulk edit form class
|
||||
- parent_obj: The parent object
|
||||
- table: A table of objects being removed
|
||||
- obj_type_plural: The plural form of the object type
|
||||
- return_url: The URL to which the user is redirected after submitting the form
|
||||
{% endcomment %}
|
||||
|
||||
{% block title %}
|
||||
{% trans "Remove" %} {{ table.rows|length }} {{ obj_type_plural|bettertitle }}?
|
||||
{% endblock %}
|
||||
|
||||
{% block tabs %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" type="button" role="tab" aria-controls="edit-form" aria-selected="true">
|
||||
{% trans "Bulk Remove" %}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock tabs %}
|
||||
|
||||
{% block content %}
|
||||
<div class="tab-pane show active" role="tabpanel">
|
||||
<div class="alert alert-danger bg-danger-subtle" role="alert">
|
||||
<div class="d-flex">
|
||||
<div>
|
||||
<i class="mdi mdi-alert-octagon p-2"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="alert-title">{% trans "Confirm Bulk Removal" %}</h4>
|
||||
{% blocktrans trimmed with count=table.rows|length %}
|
||||
The following operation will remove {{ count }} {{ obj_type_plural }} from {{ parent_obj }}. Please
|
||||
carefully review the {{ obj_type_plural }} to be removed and confirm below.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid px-0">
|
||||
<div class="card">
|
||||
<div class="table-responsive">
|
||||
{% render_table table 'inc/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="text-end">
|
||||
<a href="{{ return_url }}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
|
||||
<button type="submit" name="_confirm" class="btn btn-danger">
|
||||
{% blocktrans trimmed with count=table.rows|length %}
|
||||
Remove these {{ count }} {{ obj_type_plural }}
|
||||
{% endblocktrans %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -42,10 +42,12 @@ Context:
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for obj in selected_objects %}
|
||||
<tr{% if obj.new_name and obj.name != obj.new_name %} class="success"{% endif %}>
|
||||
<td>{{ obj.name }}</td>
|
||||
<td>{{ obj.new_name }}</td>
|
||||
</tr>
|
||||
{% with obj_name=obj|getattr:field_name %}
|
||||
<tr{% if obj.new_name and obj_name != obj.new_name %} class="success"{% endif %}>
|
||||
<td>{{ obj_name }}</td>
|
||||
<td>{{ obj.new_name }}</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -80,15 +80,7 @@ Context:
|
||||
{% if perms.extras.add_subscription and object.subscriptions %}
|
||||
{% subscribe_button object %}
|
||||
{% endif %}
|
||||
{% if request.user|can_add:object %}
|
||||
{% clone_button object %}
|
||||
{% endif %}
|
||||
{% if request.user|can_change:object %}
|
||||
{% edit_button object %}
|
||||
{% endif %}
|
||||
{% if request.user|can_delete:object %}
|
||||
{% delete_button object %}
|
||||
{% endif %}
|
||||
{% action_buttons actions object %}
|
||||
{% endblock control-buttons %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends base_template %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
@@ -7,8 +8,6 @@ Blocks:
|
||||
- content: Primary page content
|
||||
- table_controls: Control elements for the child objects table
|
||||
- bulk_controls: Bulk action buttons which appear beneath the child objects table
|
||||
- bulk_edit_controls: Bulk edit buttons
|
||||
- bulk_delete_controls: Bulk delete buttons
|
||||
- bulk_extra_controls: Other bulk action buttons
|
||||
- modals: Any pre-loaded modals
|
||||
|
||||
@@ -36,36 +35,8 @@ Context:
|
||||
</div>
|
||||
<div class="d-print-none mt-2">
|
||||
{% block bulk_controls %}
|
||||
<div class="btn-group" role="group">
|
||||
{# Bulk edit buttons #}
|
||||
{% 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 %}?return_url={{ return_url }}"
|
||||
class="btn btn-warning">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit Selected" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endblock bulk_edit_controls %}
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
{# Bulk delete buttons #}
|
||||
{% block bulk_delete_controls %}
|
||||
{% with bulk_delete_view=child_model|validated_viewname:"bulk_delete" %}
|
||||
{% if 'bulk_delete' in actions and bulk_delete_view %}
|
||||
<button type="submit"
|
||||
{% formaction %}="{% url bulk_delete_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-danger">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete Selected" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endblock bulk_delete_controls %}
|
||||
</div>
|
||||
{# Other bulk action buttons #}
|
||||
{% block bulk_extra_controls %}{% endblock %}
|
||||
{% action_buttons actions model multi=True %}
|
||||
{% block bulk_extra_controls %}{% endblock %}
|
||||
{% endblock bulk_controls %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -31,15 +31,7 @@ Context:
|
||||
<div class="btn-list">
|
||||
{% plugin_list_buttons model %}
|
||||
{% block extra_controls %}{% endblock %}
|
||||
{% if 'add' in actions %}
|
||||
{% add_button model %}
|
||||
{% endif %}
|
||||
{% if 'bulk_import' in actions %}
|
||||
{% import_button model %}
|
||||
{% endif %}
|
||||
{% if 'export' in actions %}
|
||||
{% export_button model %}
|
||||
{% endif %}
|
||||
{% action_buttons actions model %}
|
||||
</div>
|
||||
{% endblock controls %}
|
||||
|
||||
@@ -91,12 +83,7 @@ Context:
|
||||
</label>
|
||||
</div>
|
||||
<div class="bulk-action-buttons">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
{% bulk_edit_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
{% bulk_delete_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% action_buttons actions model multi=True %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,12 +111,7 @@ Context:
|
||||
<div class="btn-list d-print-none">
|
||||
{% block bulk_buttons %}
|
||||
<div class="bulk-action-buttons">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
{% bulk_edit_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
{% bulk_delete_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% action_buttons actions model multi=True %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
@@ -27,12 +27,7 @@
|
||||
{# Update the bulk action buttons with new query parameters #}
|
||||
{% if actions %}
|
||||
<div class="bulk-action-buttons" hx-swap-oob="outerHTML:.bulk-action-buttons">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
{% bulk_edit_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
{% bulk_delete_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% action_buttons actions model multi=True %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -29,7 +29,11 @@
|
||||
<div class="hr-text">
|
||||
<span>{% trans "Custom Fields" %}</span>
|
||||
</div>
|
||||
{% render_custom_fields filter_form %}
|
||||
{% for name in filter_form.custom_fields %}
|
||||
{% with field=filter_form|get_item:name %}
|
||||
{% render_field field %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{% 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> {% trans "Add Components" %}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% if perms.virtualization.add_vminterface %}
|
||||
<li>
|
||||
<button type="submit" {% formaction %}="{% url 'virtualization:virtualmachine_bulk_add_vminterface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Interfaces" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.virtualization.add_virtualdisk %}
|
||||
<li>
|
||||
<button type="submit" {% formaction %}="{% url 'virtualization:virtualmachine_bulk_add_virtualdisk' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Virtual Disks" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -1,13 +0,0 @@
|
||||
{% extends 'generic/object_children.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block bulk_delete_controls %}
|
||||
{{ block.super }}
|
||||
{% if 'bulk_remove_devices' in actions %}
|
||||
<button type="submit" name="_remove"
|
||||
{% formaction %}="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}?return_url={{ return_url }}"
|
||||
class="btn btn-danger">
|
||||
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Remove Selected" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endblock bulk_delete_controls %}
|
||||
@@ -1,14 +0,0 @@
|
||||
{% extends 'generic/object_children.html' %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block bulk_edit_controls %}
|
||||
{{ block.super }}
|
||||
{% if 'bulk_rename' in actions %}
|
||||
<button type="submit" name="_rename"
|
||||
{% formaction %}="{% url 'virtualization:vminterface_bulk_rename' %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-warning">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endblock bulk_edit_controls %}
|
||||
@@ -1,14 +0,0 @@
|
||||
{% extends 'generic/object_children.html' %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block bulk_edit_controls %}
|
||||
{{ block.super }}
|
||||
{% if 'bulk_rename' in actions %}
|
||||
<button type="submit" name="_rename"
|
||||
{% formaction %}="{% url 'virtualization:virtualdisk_bulk_rename' %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-warning">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endblock bulk_edit_controls %}
|
||||
@@ -1,29 +0,0 @@
|
||||
{% extends 'generic/object_list.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block bulk_buttons %}
|
||||
{% if perms.virtualization.change_virtualmachine %}
|
||||
<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.virtualization.add_vminterface %}
|
||||
<li>
|
||||
<button type="submit" {% formaction %}="{% url 'virtualization:virtualmachine_bulk_add_vminterface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Interfaces" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.virtualization.add_virtualdisk %}
|
||||
<li>
|
||||
<button type="submit" {% formaction %}="{% url 'virtualization:virtualmachine_bulk_add_virtualdisk' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Virtual Disks" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% 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