Compare commits

...

11 Commits

Author SHA1 Message Date
Jeremy Stretch
a3ee46a32a Update documentation
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-07-09 10:36:18 -04:00
Jeremy Stretch
eb86b9d6b0 Refactor HTML templates 2025-07-09 10:25:44 -04:00
Jeremy Stretch
37fb8ae8ae Test logging 2025-07-09 10:19:44 -04:00
Jeremy Stretch
6b2643c9cd Misc cleanup 2025-07-09 10:09:31 -04:00
Jeremy Stretch
9946ff910c Repurpose RQJobStatusColumn to display job entry level badges 2025-07-09 10:00:49 -04:00
Jeremy Stretch
e95c3825de Deserialize JobLogEntry timestamp 2025-07-09 09:30:38 -04:00
Jeremy Stretch
92a13a4226 Use TZ-aware timestamps 2025-07-09 08:51:03 -04:00
Jeremy Stretch
ae3de95dce Initial work on #19816 2025-07-08 16:56:52 -04:00
Jeremy Stretch
a1cd81ff35 Closes #17413: Permit identical names for platforms belonging to different manufacturers (#19814) 2025-07-07 10:38:01 -07:00
Jeremy Stretch
ce12de8b6d Closes #19231: Add bulk renaming support for all models (#19795)
* Closes #19231: Add bulk renaming support for all models

* Introduce a template filter for getattr()

* Extend BulkRenameView to support arbitrary field names

* Address bulk renaming support for remaining models

* Bulk rename URL resolution should fail silently

* Update documentation

* Fix bulk button rendering for HTMX requests
2025-07-02 13:35:34 -05:00
Jeremy Stretch
601a77ac73 Closes #19735: Implement reuable bulk operations classes (#19774)
* Initial work on #19735

* Work in progress

* Remove ClusterRemoveDevicesView (anti-pattern)

* Misc cleanup

* Fix has_bulk_actions

* Fix has_bulk_actions for ObjectChildrenView

* Restore clone button

* Misc cleanup

* Clean up custom bulk actions

* Rename individual object actions

* Collapse into a single template tag

* Fix support for legacy action dicts

* Rename bulk attr to multi

* clone_button tag should fail silently if view name is invalid

* Clean up action buttons

* Fix export button label

* Replace clone_button with an ObjectAction

* Create object actions for adding device/VM components

* Move core_sync.html to core app

* Remove extra_bulk_buttons from template doc
2025-06-30 13:03:07 -04:00
85 changed files with 1373 additions and 1050 deletions

View File

@@ -158,6 +158,7 @@ LOGGING = {
* `netbox.<app>.<model>` - Generic form for model-specific log messages * `netbox.<app>.<model>` - Generic form for model-specific log messages
* `netbox.auth.*` - Authentication events * `netbox.auth.*` - Authentication events
* `netbox.api.views.*` - Views which handle business logic for the REST API * `netbox.api.views.*` - Views which handle business logic for the REST API
* `netbox.jobs.*` - Background jobs
* `netbox.reports.*` - Report execution (`module.name`) * `netbox.reports.*` - Report execution (`module.name`)
* `netbox.scripts.*` - Custom script execution (`module.name`) * `netbox.scripts.*` - Custom script execution (`module.name`)
* `netbox.views.*` - Views which handle business logic for the web UI * `netbox.views.*` - Views which handle business logic for the web UI

View File

@@ -10,11 +10,11 @@ The assignment of platforms to devices is an optional feature, and may be disreg
### Name ### Name
A unique human-friendly name. A human-friendly name for the platform. Must be unique per manufacturer.
### Slug ### 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 ### Manufacturer

View File

@@ -38,6 +38,27 @@ You can schedule the background job from within your code (e.g. from a model's `
This is the human-friendly names of your background job. If omitted, the class name will be used. This is the human-friendly names of your background job. If omitted, the class name will be used.
### Logging
!!! info "This feature was introduced in NetBox v4.4."
A Python logger is instantiated by the runner for each job. It can be utilized within a job's `run()` method as needed:
```python
def run(self, *args, **kwargs):
obj = MyModel.objects.get(pk=kwargs.get('pk'))
self.logger.info("Retrieved object {obj}")
```
Four of the standard Python logging levels are supported:
* `debug()`
* `info()`
* `warning()`
* `error()`
Log entries recorded using the runner's logger will be saved in the job's log in the database in addition to being processed by other [system logging handlers](../../configuration/system.md#logging).
### Scheduled Jobs ### Scheduled Jobs
As described above, jobs can be scheduled for immediate execution or at any later time using the `enqueue()` method. However, for management purposes, the `enqueue_once()` method allows a job to be scheduled exactly once avoiding duplicates. If a job is already scheduled for a particular instance, a second one won't be scheduled, respecting thread safety. An example use case would be to schedule a periodic task that is bound to an instance in general, but not to any event of that instance (such as updates). The parameters of the `enqueue_once()` method are identical to those of `enqueue()`. As described above, jobs can be scheduled for immediate execution or at any later time using the `enqueue()` method. However, for management purposes, the `enqueue_once()` method allows a job to be scheduled exactly once avoiding duplicates. If a job is already scheduled for a particular instance, a second one won't be scheduled, respecting thread safety. An example use case would be to schedule a periodic task that is bound to an instance in general, but not to any event of that instance (such as updates). The parameters of the `enqueue_once()` method are identical to those of `enqueue()`.

View File

@@ -64,6 +64,7 @@ Generic view classes (documented below) facilitate common operations, such as cr
| `ObjectListView` | View a list of objects | | `ObjectListView` | View a list of objects |
| `BulkImportView` | Import a set of new objects | | `BulkImportView` | Import a set of new objects |
| `BulkEditView` | Edit multiple objects | | `BulkEditView` | Edit multiple objects |
| `BulkRenameView` | Rename multiple objects |
| `BulkDeleteView` | Delete multiple objects | | `BulkDeleteView` | Delete multiple objects |
!!! warning !!! warning
@@ -171,6 +172,10 @@ Below are the class definitions for NetBox's multi-object views. These views han
options: options:
members: false members: false
::: netbox.views.generic.BulkRenameView
options:
members: false
::: netbox.views.generic.BulkDeleteView ::: netbox.views.generic.BulkDeleteView
options: options:
members: members:

View File

@@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _
from dcim.views import PathTraceView from dcim.views import PathTraceView
from ipam.models import ASN from ipam.models import ASN
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
from netbox.views import generic from netbox.views import generic
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.query import count_related from utilities.query import count_related
@@ -79,6 +80,11 @@ class ProviderBulkEditView(generic.BulkEditView):
form = forms.ProviderBulkEditForm form = forms.ProviderBulkEditForm
@register_model_view(Provider, 'bulk_rename', path='rename', detail=False)
class ProviderBulkRenameView(generic.BulkRenameView):
queryset = Provider.objects.all()
@register_model_view(Provider, 'bulk_delete', path='delete', detail=False) @register_model_view(Provider, 'bulk_delete', path='delete', detail=False)
class ProviderBulkDeleteView(generic.BulkDeleteView): class ProviderBulkDeleteView(generic.BulkDeleteView):
queryset = Provider.objects.annotate( queryset = Provider.objects.annotate(
@@ -141,6 +147,11 @@ class ProviderAccountBulkEditView(generic.BulkEditView):
form = forms.ProviderAccountBulkEditForm form = forms.ProviderAccountBulkEditForm
@register_model_view(ProviderAccount, 'bulk_rename', path='rename', detail=False)
class ProviderAccountBulkRenameView(generic.BulkRenameView):
queryset = ProviderAccount.objects.all()
@register_model_view(ProviderAccount, 'bulk_delete', path='delete', detail=False) @register_model_view(ProviderAccount, 'bulk_delete', path='delete', detail=False)
class ProviderAccountBulkDeleteView(generic.BulkDeleteView): class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
queryset = ProviderAccount.objects.annotate( queryset = ProviderAccount.objects.annotate(
@@ -212,6 +223,11 @@ class ProviderNetworkBulkEditView(generic.BulkEditView):
form = forms.ProviderNetworkBulkEditForm form = forms.ProviderNetworkBulkEditForm
@register_model_view(ProviderNetwork, 'bulk_rename', path='rename', detail=False)
class ProviderNetworkBulkRenameView(generic.BulkRenameView):
queryset = ProviderNetwork.objects.all()
@register_model_view(ProviderNetwork, 'bulk_delete', path='delete', detail=False) @register_model_view(ProviderNetwork, 'bulk_delete', path='delete', detail=False)
class ProviderNetworkBulkDeleteView(generic.BulkDeleteView): class ProviderNetworkBulkDeleteView(generic.BulkDeleteView):
queryset = ProviderNetwork.objects.all() queryset = ProviderNetwork.objects.all()
@@ -271,6 +287,11 @@ class CircuitTypeBulkEditView(generic.BulkEditView):
form = forms.CircuitTypeBulkEditForm form = forms.CircuitTypeBulkEditForm
@register_model_view(CircuitType, 'bulk_rename', path='rename', detail=False)
class CircuitTypeBulkRenameView(generic.BulkRenameView):
queryset = CircuitType.objects.all()
@register_model_view(CircuitType, 'bulk_delete', path='delete', detail=False) @register_model_view(CircuitType, 'bulk_delete', path='delete', detail=False)
class CircuitTypeBulkDeleteView(generic.BulkDeleteView): class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
queryset = CircuitType.objects.annotate( queryset = CircuitType.objects.annotate(
@@ -337,6 +358,12 @@ class CircuitBulkEditView(generic.BulkEditView):
form = forms.CircuitBulkEditForm form = forms.CircuitBulkEditForm
@register_model_view(Circuit, 'bulk_rename', path='rename', detail=False)
class CircuitBulkRenameView(generic.BulkRenameView):
queryset = Circuit.objects.all()
field_name = 'cid'
@register_model_view(Circuit, 'bulk_delete', path='delete', detail=False) @register_model_view(Circuit, 'bulk_delete', path='delete', detail=False)
class CircuitBulkDeleteView(generic.BulkDeleteView): class CircuitBulkDeleteView(generic.BulkDeleteView):
queryset = Circuit.objects.prefetch_related( queryset = Circuit.objects.prefetch_related(
@@ -432,6 +459,7 @@ class CircuitTerminationListView(generic.ObjectListView):
filterset = filtersets.CircuitTerminationFilterSet filterset = filtersets.CircuitTerminationFilterSet
filterset_form = forms.CircuitTerminationFilterForm filterset_form = forms.CircuitTerminationFilterForm
table = tables.CircuitTerminationTable table = tables.CircuitTerminationTable
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
@register_model_view(CircuitTermination) @register_model_view(CircuitTermination)
@@ -526,6 +554,11 @@ class CircuitGroupBulkEditView(generic.BulkEditView):
form = forms.CircuitGroupBulkEditForm form = forms.CircuitGroupBulkEditForm
@register_model_view(CircuitGroup, 'bulk_rename', path='rename', detail=False)
class CircuitGroupBulkRenameView(generic.BulkRenameView):
queryset = CircuitGroup.objects.all()
@register_model_view(CircuitGroup, 'bulk_delete', path='delete', detail=False) @register_model_view(CircuitGroup, 'bulk_delete', path='delete', detail=False)
class CircuitGroupBulkDeleteView(generic.BulkDeleteView): class CircuitGroupBulkDeleteView(generic.BulkDeleteView):
queryset = CircuitGroup.objects.all() queryset = CircuitGroup.objects.all()
@@ -543,6 +576,7 @@ class CircuitGroupAssignmentListView(generic.ObjectListView):
filterset = filtersets.CircuitGroupAssignmentFilterSet filterset = filtersets.CircuitGroupAssignmentFilterSet
filterset_form = forms.CircuitGroupAssignmentFilterForm filterset_form = forms.CircuitGroupAssignmentFilterForm
table = tables.CircuitGroupAssignmentTable table = tables.CircuitGroupAssignmentTable
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
@register_model_view(CircuitGroupAssignment) @register_model_view(CircuitGroupAssignment)
@@ -635,6 +669,11 @@ class VirtualCircuitTypeBulkEditView(generic.BulkEditView):
form = forms.VirtualCircuitTypeBulkEditForm form = forms.VirtualCircuitTypeBulkEditForm
@register_model_view(VirtualCircuitType, 'bulk_rename', path='rename', detail=False)
class VirtualCircuitTypeBulkRenameView(generic.BulkRenameView):
queryset = VirtualCircuitType.objects.all()
@register_model_view(VirtualCircuitType, 'bulk_delete', path='delete', detail=False) @register_model_view(VirtualCircuitType, 'bulk_delete', path='delete', detail=False)
class VirtualCircuitTypeBulkDeleteView(generic.BulkDeleteView): class VirtualCircuitTypeBulkDeleteView(generic.BulkDeleteView):
queryset = VirtualCircuitType.objects.annotate( queryset = VirtualCircuitType.objects.annotate(
@@ -697,6 +736,12 @@ class VirtualCircuitBulkEditView(generic.BulkEditView):
form = forms.VirtualCircuitBulkEditForm form = forms.VirtualCircuitBulkEditForm
@register_model_view(VirtualCircuit, 'bulk_rename', path='rename', detail=False)
class VirtualCircuitulkRenameView(generic.BulkRenameView):
queryset = VirtualCircuit.objects.all()
field_name = 'cid'
class VirtualCircuitBulkDeleteView(generic.BulkDeleteView): class VirtualCircuitBulkDeleteView(generic.BulkDeleteView):
queryset = VirtualCircuit.objects.annotate( queryset = VirtualCircuit.objects.annotate(
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit') termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
@@ -714,6 +759,7 @@ class VirtualCircuitTerminationListView(generic.ObjectListView):
filterset = filtersets.VirtualCircuitTerminationFilterSet filterset = filtersets.VirtualCircuitTerminationFilterSet
filterset_form = forms.VirtualCircuitTerminationFilterForm filterset_form = forms.VirtualCircuitTerminationFilterForm
table = tables.VirtualCircuitTerminationTable table = tables.VirtualCircuitTerminationTable
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
@register_model_view(VirtualCircuitTermination) @register_model_view(VirtualCircuitTermination)

View File

@@ -23,6 +23,6 @@ class JobSerializer(BaseModelSerializer):
model = Job model = Job
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled',
'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'log_entries',
] ]
brief_fields = ('url', 'created', 'completed', 'user', 'status') brief_fields = ('url', 'created', 'completed', 'user', 'status')

View File

@@ -4,23 +4,31 @@ from django.utils.translation import gettext_lazy as _
from rq.job import JobStatus from rq.job import JobStatus
__all__ = ( __all__ = (
'JOB_LOG_ENTRY_LEVELS',
'RQ_TASK_STATUSES', 'RQ_TASK_STATUSES',
) )
@dataclass @dataclass
class Status: class Badge:
label: str label: str
color: str color: str
RQ_TASK_STATUSES = { RQ_TASK_STATUSES = {
JobStatus.QUEUED: Status(_('Queued'), 'cyan'), JobStatus.QUEUED: Badge(_('Queued'), 'cyan'),
JobStatus.FINISHED: Status(_('Finished'), 'green'), JobStatus.FINISHED: Badge(_('Finished'), 'green'),
JobStatus.FAILED: Status(_('Failed'), 'red'), JobStatus.FAILED: Badge(_('Failed'), 'red'),
JobStatus.STARTED: Status(_('Started'), 'blue'), JobStatus.STARTED: Badge(_('Started'), 'blue'),
JobStatus.DEFERRED: Status(_('Deferred'), 'gray'), JobStatus.DEFERRED: Badge(_('Deferred'), 'gray'),
JobStatus.SCHEDULED: Status(_('Scheduled'), 'purple'), JobStatus.SCHEDULED: Badge(_('Scheduled'), 'purple'),
JobStatus.STOPPED: Status(_('Stopped'), 'orange'), JobStatus.STOPPED: Badge(_('Stopped'), 'orange'),
JobStatus.CANCELED: Status(_('Cancelled'), 'yellow'), JobStatus.CANCELED: Badge(_('Cancelled'), 'yellow'),
}
JOB_LOG_ENTRY_LEVELS = {
'debug': Badge(_('Debug'), 'gray'),
'info': Badge(_('Info'), 'blue'),
'warning': Badge(_('Warning'), 'orange'),
'error': Badge(_('Error'), 'red'),
} }

View File

@@ -0,0 +1,21 @@
import logging
from dataclasses import dataclass, field
from datetime import datetime
from django.utils import timezone
__all__ = (
'JobLogEntry',
)
@dataclass
class JobLogEntry:
level: str
message: str
timestamp: datetime = field(default_factory=timezone.now)
@classmethod
def from_logrecord(cls, record: logging.LogRecord):
return cls(record.levelname.lower(), record.msg)

View File

@@ -7,7 +7,6 @@ from netbox.jobs import JobRunner, system_job
from netbox.search.backends import search_backend from netbox.search.backends import search_backend
from utilities.proxy import resolve_proxies from utilities.proxy import resolve_proxies
from .choices import DataSourceStatusChoices, JobIntervalChoices from .choices import DataSourceStatusChoices, JobIntervalChoices
from .exceptions import SyncError
from .models import DataSource from .models import DataSource
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -23,19 +22,23 @@ class SyncDataSourceJob(JobRunner):
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
datasource = DataSource.objects.get(pk=self.job.object_id) datasource = DataSource.objects.get(pk=self.job.object_id)
self.logger.debug(f"Found DataSource ID {datasource.pk}")
try: try:
self.logger.info(f"Syncing data source {datasource}")
datasource.sync() datasource.sync()
# Update the search cache for DataFiles belonging to this source # Update the search cache for DataFiles belonging to this source
self.logger.debug("Updating search cache for data files")
search_backend.cache(datasource.datafiles.iterator()) search_backend.cache(datasource.datafiles.iterator())
except Exception as e: except Exception as e:
self.logger.error(f"Error syncing data source: {e}")
DataSource.objects.filter(pk=datasource.pk).update(status=DataSourceStatusChoices.FAILED) DataSource.objects.filter(pk=datasource.pk).update(status=DataSourceStatusChoices.FAILED)
if type(e) is SyncError:
logging.error(e)
raise e raise e
self.logger.info("Syncing completed successfully")
@system_job(interval=JobIntervalChoices.INTERVAL_DAILY) @system_job(interval=JobIntervalChoices.INTERVAL_DAILY)
class SystemHousekeepingJob(JobRunner): class SystemHousekeepingJob(JobRunner):

View File

@@ -0,0 +1,28 @@
import django.contrib.postgres.fields
import django.core.serializers.json
from django.db import migrations, models
import utilities.json
class Migration(migrations.Migration):
dependencies = [
('core', '0015_remove_redundant_indexes'),
]
operations = [
migrations.AddField(
model_name='job',
name='log_entries',
field=django.contrib.postgres.fields.ArrayField(
base_field=models.JSONField(
decoder=utilities.json.JobLogDecoder,
encoder=django.core.serializers.json.DjangoJSONEncoder
),
blank=True,
default=list,
size=None
),
),
]

View File

@@ -1,9 +1,12 @@
import logging
import uuid import uuid
from dataclasses import asdict
from functools import partial from functools import partial
import django_rq import django_rq
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
@@ -14,8 +17,10 @@ from django.utils.translation import gettext as _
from rq.exceptions import InvalidJobOperation from rq.exceptions import InvalidJobOperation
from core.choices import JobStatusChoices from core.choices import JobStatusChoices
from core.dataclasses import JobLogEntry
from core.models import ObjectType from core.models import ObjectType
from core.signals import job_end, job_start from core.signals import job_end, job_start
from utilities.json import JobLogDecoder
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.rqworker import get_queue_for_model from utilities.rqworker import get_queue_for_model
@@ -104,6 +109,15 @@ class Job(models.Model):
verbose_name=_('job ID'), verbose_name=_('job ID'),
unique=True unique=True
) )
log_entries = ArrayField(
verbose_name=_('log entries'),
base_field=models.JSONField(
encoder=DjangoJSONEncoder,
decoder=JobLogDecoder,
),
blank=True,
default=list,
)
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
@@ -205,6 +219,13 @@ class Job(models.Model):
# Send signal # Send signal
job_end.send(self) job_end.send(self)
def log(self, record: logging.LogRecord):
"""
Record a LogRecord from Python's native logging in the job's log.
"""
entry = JobLogEntry.from_logrecord(record)
self.log_entries.append(asdict(entry))
@classmethod @classmethod
def enqueue( def enqueue(
cls, cls,

View 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'

View File

@@ -1,12 +1,11 @@
import django_tables2 as tables import django_tables2 as tables
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from core.constants import RQ_TASK_STATUSES
from netbox.registry import registry from netbox.registry import registry
__all__ = ( __all__ = (
'BackendTypeColumn', 'BackendTypeColumn',
'RQJobStatusColumn', 'BadgeColumn',
) )
@@ -23,14 +22,21 @@ class BackendTypeColumn(tables.Column):
return value return value
class RQJobStatusColumn(tables.Column): class BadgeColumn(tables.Column):
""" """
Render a colored label for the status of an RQ job. Render a colored badge for a value.
Args:
badges: A dictionary mapping of values to core.constants.Badge instances.
""" """
def __init__(self, badges, *args, **kwargs):
super().__init__(*args, **kwargs)
self.badges = badges
def render(self, value): def render(self, value):
status = RQ_TASK_STATUSES.get(value) badge = self.badges.get(value)
return mark_safe(f'<span class="badge text-bg-{status.color}">{status.label}</span>') return mark_safe(f'<span class="badge text-bg-{badge.color}">{badge.label}</span>')
def value(self, value): def value(self, value):
status = RQ_TASK_STATUSES.get(value) badge = self.badges.get(value)
return status.label return badge.label

View File

@@ -1,8 +1,10 @@
import django_tables2 as tables import django_tables2 as tables
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from netbox.tables import NetBoxTable, columns from netbox.tables import BaseTable, NetBoxTable, columns
from ..models import Job from core.constants import JOB_LOG_ENTRY_LEVELS
from core.models import Job
from core.tables.columns import BadgeColumn
class JobTable(NetBoxTable): class JobTable(NetBoxTable):
@@ -40,6 +42,9 @@ class JobTable(NetBoxTable):
completed = columns.DateTimeColumn( completed = columns.DateTimeColumn(
verbose_name=_('Completed'), verbose_name=_('Completed'),
) )
log_entries = tables.Column(
verbose_name=_('Log Entries'),
)
actions = columns.ActionsColumn( actions = columns.ActionsColumn(
actions=('delete',) actions=('delete',)
) )
@@ -53,3 +58,24 @@ class JobTable(NetBoxTable):
default_columns = ( default_columns = (
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user', 'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user',
) )
def render_log_entries(self, value):
return len(value)
class JobLogEntryTable(BaseTable):
timestamp = columns.DateTimeColumn(
timespec='milliseconds',
verbose_name=_('Time'),
)
level = BadgeColumn(
badges=JOB_LOG_ENTRY_LEVELS,
verbose_name=_('Level'),
)
message = tables.Column(
verbose_name=_('Message'),
)
class Meta(BaseTable.Meta):
empty_text = _('No log entries')
fields = ('timestamp', 'level', 'message')

View File

@@ -2,7 +2,8 @@ import django_tables2 as tables
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_tables2.utils import A from django_tables2.utils import A
from core.tables.columns import RQJobStatusColumn from core.constants import RQ_TASK_STATUSES
from core.tables.columns import BadgeColumn
from netbox.tables import BaseTable, columns from netbox.tables import BaseTable, columns
@@ -84,7 +85,8 @@ class BackgroundTaskTable(BaseTable):
ended_at = columns.DateTimeColumn( ended_at = columns.DateTimeColumn(
verbose_name=_("Ended") verbose_name=_("Ended")
) )
status = RQJobStatusColumn( status = BadgeColumn(
badges=RQ_TASK_STATUSES,
verbose_name=_("Status"), verbose_name=_("Status"),
accessor='get_status' accessor='get_status'
) )

View File

@@ -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 core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs_from_status, requeue_rq_job, stop_rq_job
from netbox.config import get_config, PARAMS from netbox.config import get_config, PARAMS
from netbox.object_actions import AddObject, BulkDelete, BulkExport, DeleteObject
from netbox.registry import registry from netbox.registry import registry
from netbox.views import generic from netbox.views import generic
from netbox.views.generic.base import BaseObjectView from netbox.views.generic.base import BaseObjectView
@@ -31,13 +32,13 @@ from utilities.forms import ConfirmationForm
from utilities.htmx import htmx_partial from utilities.htmx import htmx_partial
from utilities.json import ConfigJSONEncoder from utilities.json import ConfigJSONEncoder
from utilities.query import count_related from utilities.query import count_related
from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, register_model_view from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, ViewTab, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .choices import DataSourceStatusChoices from .choices import DataSourceStatusChoices
from .jobs import SyncDataSourceJob from .jobs import SyncDataSourceJob
from .models import * from .models import *
from .plugins import get_catalog_plugins, get_local_plugins from .plugins import get_catalog_plugins, get_local_plugins
from .tables import CatalogPluginTable, PluginVersionTable from .tables import CatalogPluginTable, JobLogEntryTable, PluginVersionTable
# #
@@ -119,6 +120,11 @@ class DataSourceBulkEditView(generic.BulkEditView):
form = forms.DataSourceBulkEditForm form = forms.DataSourceBulkEditForm
@register_model_view(DataSource, 'bulk_rename', path='rename', detail=False)
class DataSourceBulkRenameView(generic.BulkRenameView):
queryset = DataSource.objects.all()
@register_model_view(DataSource, 'bulk_delete', path='delete', detail=False) @register_model_view(DataSource, 'bulk_delete', path='delete', detail=False)
class DataSourceBulkDeleteView(generic.BulkDeleteView): class DataSourceBulkDeleteView(generic.BulkDeleteView):
queryset = DataSource.objects.annotate( queryset = DataSource.objects.annotate(
@@ -138,14 +144,13 @@ class DataFileListView(generic.ObjectListView):
filterset = filtersets.DataFileFilterSet filterset = filtersets.DataFileFilterSet
filterset_form = forms.DataFileFilterForm filterset_form = forms.DataFileFilterForm
table = tables.DataFileTable table = tables.DataFileTable
actions = { actions = (BulkDelete,)
'bulk_delete': {'delete'},
}
@register_model_view(DataFile) @register_model_view(DataFile)
class DataFileView(generic.ObjectView): class DataFileView(generic.ObjectView):
queryset = DataFile.objects.all() queryset = DataFile.objects.all()
actions = (DeleteObject,)
@register_model_view(DataFile, 'delete') @register_model_view(DataFile, 'delete')
@@ -170,15 +175,32 @@ class JobListView(generic.ObjectListView):
filterset = filtersets.JobFilterSet filterset = filtersets.JobFilterSet
filterset_form = forms.JobFilterForm filterset_form = forms.JobFilterForm
table = tables.JobTable table = tables.JobTable
actions = { actions = (BulkExport, BulkDelete)
'export': {'view'},
'bulk_delete': {'delete'},
}
@register_model_view(Job) @register_model_view(Job)
class JobView(generic.ObjectView): class JobView(generic.ObjectView):
queryset = Job.objects.all() queryset = Job.objects.all()
actions = (DeleteObject,)
@register_model_view(Job, 'log')
class JobLogView(generic.ObjectView):
queryset = Job.objects.all()
actions = (DeleteObject,)
template_name = 'core/job/log.html'
tab = ViewTab(
label=_('Log'),
badge=lambda obj: len(obj.log_entries),
weight=500,
)
def get_extra_context(self, request, instance):
table = JobLogEntryTable(instance.log_entries)
table.configure(request)
return {
'table': table,
}
@register_model_view(Job, 'delete') @register_model_view(Job, 'delete')
@@ -204,9 +226,7 @@ class ObjectChangeListView(generic.ObjectListView):
filterset_form = forms.ObjectChangeFilterForm filterset_form = forms.ObjectChangeFilterForm
table = tables.ObjectChangeTable table = tables.ObjectChangeTable
template_name = 'core/objectchange_list.html' template_name = 'core/objectchange_list.html'
actions = { actions = (BulkExport,)
'export': {'view'},
}
@register_model_view(ObjectChange) @register_model_view(ObjectChange)
@@ -274,6 +294,7 @@ class ConfigRevisionListView(generic.ObjectListView):
filterset = filtersets.ConfigRevisionFilterSet filterset = filtersets.ConfigRevisionFilterSet
filterset_form = forms.ConfigRevisionFilterForm filterset_form = forms.ConfigRevisionFilterForm
table = tables.ConfigRevisionTable table = tables.ConfigRevisionTable
actions = (AddObject, BulkExport)
@register_model_view(ConfigRevision) @register_model_view(ConfigRevision)

View File

@@ -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.'
),
),
]

View File

@@ -415,6 +415,15 @@ class Platform(OrganizationalModel):
null=True, null=True,
help_text=_('Optionally limit this platform to devices of a certain manufacturer') help_text=_('Optionally limit this platform to devices of a certain manufacturer')
) )
# Override name & slug from OrganizationalModel to not enforce uniqueness
name = models.CharField(
verbose_name=_('name'),
max_length=100
)
slug = models.SlugField(
verbose_name=_('slug'),
max_length=100
)
config_template = models.ForeignKey( config_template = models.ForeignKey(
to='extras.ConfigTemplate', to='extras.ConfigTemplate',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@@ -427,6 +436,28 @@ class Platform(OrganizationalModel):
ordering = ('name',) ordering = ('name',)
verbose_name = _('platform') verbose_name = _('platform')
verbose_name_plural = _('platforms') verbose_name_plural = _('platforms')
constraints = (
models.UniqueConstraint(
fields=('manufacturer', 'name'),
name='%(app_label)s_%(class)s_manufacturer_name',
),
models.UniqueConstraint(
fields=('name',),
name='%(app_label)s_%(class)s_name',
condition=Q(manufacturer__isnull=True),
violation_error_message=_("Platform name must be unique.")
),
models.UniqueConstraint(
fields=('manufacturer', 'slug'),
name='%(app_label)s_%(class)s_manufacturer_slug',
),
models.UniqueConstraint(
fields=('slug',),
name='%(app_label)s_%(class)s_slug',
condition=Q(manufacturer__isnull=True),
violation_error_message=_("Platform slug must be unique.")
),
)
class Device( class Device(

View 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'

View File

@@ -15,7 +15,7 @@ from circuits.models import Circuit, CircuitTermination
from extras.views import ObjectConfigContextView, ObjectRenderConfigView from extras.views import ObjectConfigContextView, ObjectRenderConfigView
from ipam.models import ASN, IPAddress, Prefix, VLANGroup, VLAN from ipam.models import ASN, IPAddress, Prefix, VLANGroup, VLAN
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.object_actions import *
from netbox.views import generic from netbox.views import generic
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.paginator import EnhancedPaginator, get_paginate_count
@@ -34,6 +34,7 @@ from wireless.models import WirelessLAN
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .choices import DeviceFaceChoices, InterfaceModeChoices from .choices import DeviceFaceChoices, InterfaceModeChoices
from .models import * from .models import *
from .object_actions import BulkAddComponents, BulkDisconnect
CABLE_TERMINATION_TYPES = { CABLE_TERMINATION_TYPES = {
'dcim.consoleport': ConsolePort, 'dcim.consoleport': ConsolePort,
@@ -49,11 +50,6 @@ CABLE_TERMINATION_TYPES = {
class DeviceComponentsView(generic.ObjectChildrenView): class DeviceComponentsView(generic.ObjectChildrenView):
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
'bulk_disconnect': {'change'},
}
queryset = Device.objects.all() queryset = Device.objects.all()
def get_children(self, request, parent): def get_children(self, request, parent):
@@ -61,12 +57,8 @@ class DeviceComponentsView(generic.ObjectChildrenView):
class DeviceTypeComponentsView(generic.ObjectChildrenView): class DeviceTypeComponentsView(generic.ObjectChildrenView):
actions = { actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
queryset = DeviceType.objects.all() queryset = DeviceType.objects.all()
template_name = 'dcim/devicetype/component_templates.html'
viewname = None # Used for return_url resolution viewname = None # Used for return_url resolution
def get_children(self, request, parent): def get_children(self, request, parent):
@@ -78,9 +70,9 @@ class DeviceTypeComponentsView(generic.ObjectChildrenView):
} }
class ModuleTypeComponentsView(DeviceComponentsView): class ModuleTypeComponentsView(generic.ObjectChildrenView):
queryset = ModuleType.objects.all() queryset = ModuleType.objects.all()
template_name = 'dcim/moduletype/component_templates.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
viewname = None # Used for return_url resolution viewname = None # Used for return_url resolution
def get_children(self, request, parent): def get_children(self, request, parent):
@@ -300,6 +292,11 @@ class RegionBulkEditView(generic.BulkEditView):
form = forms.RegionBulkEditForm form = forms.RegionBulkEditForm
@register_model_view(Region, 'bulk_rename', path='rename', detail=False)
class RegionBulkRenameView(generic.BulkRenameView):
queryset = Region.objects.all()
@register_model_view(Region, 'bulk_delete', path='delete', detail=False) @register_model_view(Region, 'bulk_delete', path='delete', detail=False)
class RegionBulkDeleteView(generic.BulkDeleteView): class RegionBulkDeleteView(generic.BulkDeleteView):
queryset = Region.objects.add_related_count( queryset = Region.objects.add_related_count(
@@ -426,6 +423,11 @@ class SiteGroupBulkEditView(generic.BulkEditView):
form = forms.SiteGroupBulkEditForm form = forms.SiteGroupBulkEditForm
@register_model_view(SiteGroup, 'bulk_rename', path='rename', detail=False)
class SiteGroupBulkRenameView(generic.BulkRenameView):
queryset = SiteGroup.objects.all()
@register_model_view(SiteGroup, 'bulk_delete', path='delete', detail=False) @register_model_view(SiteGroup, 'bulk_delete', path='delete', detail=False)
class SiteGroupBulkDeleteView(generic.BulkDeleteView): class SiteGroupBulkDeleteView(generic.BulkDeleteView):
queryset = SiteGroup.objects.add_related_count( queryset = SiteGroup.objects.add_related_count(
@@ -511,6 +513,11 @@ class SiteBulkEditView(generic.BulkEditView):
form = forms.SiteBulkEditForm form = forms.SiteBulkEditForm
@register_model_view(Site, 'bulk_rename', path='rename', detail=False)
class SiteBulkRenameView(generic.BulkRenameView):
queryset = Site.objects.all()
@register_model_view(Site, 'bulk_delete', path='delete', detail=False) @register_model_view(Site, 'bulk_delete', path='delete', detail=False)
class SiteBulkDeleteView(generic.BulkDeleteView): class SiteBulkDeleteView(generic.BulkDeleteView):
queryset = Site.objects.all() queryset = Site.objects.all()
@@ -615,6 +622,11 @@ class LocationBulkEditView(generic.BulkEditView):
form = forms.LocationBulkEditForm form = forms.LocationBulkEditForm
@register_model_view(Location, 'bulk_rename', path='rename', detail=False)
class LocationBulkRenameView(generic.BulkRenameView):
queryset = Location.objects.all()
@register_model_view(Location, 'bulk_delete', path='delete', detail=False) @register_model_view(Location, 'bulk_delete', path='delete', detail=False)
class LocationBulkDeleteView(generic.BulkDeleteView): class LocationBulkDeleteView(generic.BulkDeleteView):
queryset = Location.objects.add_related_count( queryset = Location.objects.add_related_count(
@@ -680,6 +692,11 @@ class RackRoleBulkEditView(generic.BulkEditView):
form = forms.RackRoleBulkEditForm form = forms.RackRoleBulkEditForm
@register_model_view(RackRole, 'bulk_rename', path='rename', detail=False)
class RackRoleBulkRenameView(generic.BulkRenameView):
queryset = RackRole.objects.all()
@register_model_view(RackRole, 'bulk_delete', path='delete', detail=False) @register_model_view(RackRole, 'bulk_delete', path='delete', detail=False)
class RackRoleBulkDeleteView(generic.BulkDeleteView): class RackRoleBulkDeleteView(generic.BulkDeleteView):
queryset = RackRole.objects.annotate( queryset = RackRole.objects.annotate(
@@ -739,6 +756,12 @@ class RackTypeBulkEditView(generic.BulkEditView):
form = forms.RackTypeBulkEditForm form = forms.RackTypeBulkEditForm
@register_model_view(RackType, 'bulk_rename', path='rename', detail=False)
class RackTypeBulkRenameView(generic.BulkRenameView):
queryset = RackType.objects.all()
field_name = 'model'
@register_model_view(RackType, 'bulk_delete', path='delete', detail=False) @register_model_view(RackType, 'bulk_delete', path='delete', detail=False)
class RackTypeBulkDeleteView(generic.BulkDeleteView): class RackTypeBulkDeleteView(generic.BulkDeleteView):
queryset = RackType.objects.all() queryset = RackType.objects.all()
@@ -918,6 +941,11 @@ class RackBulkEditView(generic.BulkEditView):
form = forms.RackBulkEditForm form = forms.RackBulkEditForm
@register_model_view(Rack, 'bulk_rename', path='rename', detail=False)
class RackBulkRenameView(generic.BulkRenameView):
queryset = Rack.objects.all()
@register_model_view(Rack, 'bulk_delete', path='delete', detail=False) @register_model_view(Rack, 'bulk_delete', path='delete', detail=False)
class RackBulkDeleteView(generic.BulkDeleteView): class RackBulkDeleteView(generic.BulkDeleteView):
queryset = Rack.objects.all() queryset = Rack.objects.all()
@@ -935,6 +963,7 @@ class RackReservationListView(generic.ObjectListView):
filterset = filtersets.RackReservationFilterSet filterset = filtersets.RackReservationFilterSet
filterset_form = forms.RackReservationFilterForm filterset_form = forms.RackReservationFilterForm
table = tables.RackReservationTable table = tables.RackReservationTable
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
@register_model_view(RackReservation) @register_model_view(RackReservation)
@@ -1051,6 +1080,11 @@ class ManufacturerBulkEditView(generic.BulkEditView):
form = forms.ManufacturerBulkEditForm form = forms.ManufacturerBulkEditForm
@register_model_view(Manufacturer, 'bulk_rename', path='rename', detail=False)
class ManufacturerBulkRenameView(generic.BulkRenameView):
queryset = Manufacturer.objects.all()
@register_model_view(Manufacturer, 'bulk_delete', path='delete', detail=False) @register_model_view(Manufacturer, 'bulk_delete', path='delete', detail=False)
class ManufacturerBulkDeleteView(generic.BulkDeleteView): class ManufacturerBulkDeleteView(generic.BulkDeleteView):
queryset = Manufacturer.objects.annotate( queryset = Manufacturer.objects.annotate(
@@ -1298,6 +1332,12 @@ class DeviceTypeBulkEditView(generic.BulkEditView):
form = forms.DeviceTypeBulkEditForm form = forms.DeviceTypeBulkEditForm
@register_model_view(DeviceType, 'bulk_rename', path='rename', detail=False)
class DeviceTypeBulkRenameView(generic.BulkRenameView):
queryset = DeviceType.objects.all()
field_name = 'model'
@register_model_view(DeviceType, 'bulk_delete', path='delete', detail=False) @register_model_view(DeviceType, 'bulk_delete', path='delete', detail=False)
class DeviceTypeBulkDeleteView(generic.BulkDeleteView): class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
queryset = DeviceType.objects.annotate( queryset = DeviceType.objects.annotate(
@@ -1354,6 +1394,11 @@ class ModuleTypeProfileBulkEditView(generic.BulkEditView):
form = forms.ModuleTypeProfileBulkEditForm form = forms.ModuleTypeProfileBulkEditForm
@register_model_view(ModuleTypeProfile, 'bulk_rename', path='rename', detail=False)
class ModuleTypeProfileBulkRenameView(generic.BulkRenameView):
queryset = ModuleTypeProfile.objects.all()
@register_model_view(ModuleTypeProfile, 'bulk_delete', path='delete', detail=False) @register_model_view(ModuleTypeProfile, 'bulk_delete', path='delete', detail=False)
class ModuleTypeProfileBulkDeleteView(generic.BulkDeleteView): class ModuleTypeProfileBulkDeleteView(generic.BulkDeleteView):
queryset = ModuleTypeProfile.objects.annotate( queryset = ModuleTypeProfile.objects.annotate(
@@ -1564,6 +1609,11 @@ class ModuleTypeBulkEditView(generic.BulkEditView):
form = forms.ModuleTypeBulkEditForm form = forms.ModuleTypeBulkEditForm
@register_model_view(ModuleType, 'bulk_rename', path='rename', detail=False)
class ModuleTypeBulkRenameView(generic.BulkRenameView):
queryset = ModuleType.objects.all()
@register_model_view(ModuleType, 'bulk_delete', path='delete', detail=False) @register_model_view(ModuleType, 'bulk_delete', path='delete', detail=False)
class ModuleTypeBulkDeleteView(generic.BulkDeleteView): class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
queryset = ModuleType.objects.annotate( queryset = ModuleType.objects.annotate(
@@ -2038,6 +2088,11 @@ class DeviceRoleBulkEditView(generic.BulkEditView):
form = forms.DeviceRoleBulkEditForm form = forms.DeviceRoleBulkEditForm
@register_model_view(DeviceRole, 'bulk_rename', path='rename', detail=False)
class DeviceRoleBulkRenameView(generic.BulkRenameView):
queryset = DeviceRole.objects.all()
@register_model_view(DeviceRole, 'bulk_delete', path='delete', detail=False) @register_model_view(DeviceRole, 'bulk_delete', path='delete', detail=False)
class DeviceRoleBulkDeleteView(generic.BulkDeleteView): class DeviceRoleBulkDeleteView(generic.BulkDeleteView):
queryset = DeviceRole.objects.annotate( queryset = DeviceRole.objects.annotate(
@@ -2099,6 +2154,11 @@ class PlatformBulkEditView(generic.BulkEditView):
form = forms.PlatformBulkEditForm form = forms.PlatformBulkEditForm
@register_model_view(Platform, 'bulk_rename', path='rename', detail=False)
class PlatformBulkRenameView(generic.BulkRenameView):
queryset = Platform.objects.all()
@register_model_view(Platform, 'bulk_delete', path='delete', detail=False) @register_model_view(Platform, 'bulk_delete', path='delete', detail=False)
class PlatformBulkDeleteView(generic.BulkDeleteView): class PlatformBulkDeleteView(generic.BulkDeleteView):
queryset = Platform.objects.all() queryset = Platform.objects.all()
@@ -2116,7 +2176,7 @@ class DeviceListView(generic.ObjectListView):
filterset = filtersets.DeviceFilterSet filterset = filtersets.DeviceFilterSet
filterset_form = forms.DeviceFilterForm filterset_form = forms.DeviceFilterForm
table = tables.DeviceTable table = tables.DeviceTable
template_name = 'dcim/device_list.html' actions = (AddObject, BulkImport, BulkExport, BulkAddComponents, BulkEdit, BulkRename, BulkDelete)
@register_model_view(Device) @register_model_view(Device)
@@ -2157,7 +2217,7 @@ class DeviceConsolePortsView(DeviceComponentsView):
table = tables.DeviceConsolePortTable table = tables.DeviceConsolePortTable
filterset = filtersets.ConsolePortFilterSet filterset = filtersets.ConsolePortFilterSet
filterset_form = forms.ConsolePortFilterForm filterset_form = forms.ConsolePortFilterForm
template_name = 'dcim/device/consoleports.html', actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
tab = ViewTab( tab = ViewTab(
label=_('Console Ports'), label=_('Console Ports'),
badge=lambda obj: obj.console_port_count, badge=lambda obj: obj.console_port_count,
@@ -2173,7 +2233,7 @@ class DeviceConsoleServerPortsView(DeviceComponentsView):
table = tables.DeviceConsoleServerPortTable table = tables.DeviceConsoleServerPortTable
filterset = filtersets.ConsoleServerPortFilterSet filterset = filtersets.ConsoleServerPortFilterSet
filterset_form = forms.ConsoleServerPortFilterForm filterset_form = forms.ConsoleServerPortFilterForm
template_name = 'dcim/device/consoleserverports.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
tab = ViewTab( tab = ViewTab(
label=_('Console Server Ports'), label=_('Console Server Ports'),
badge=lambda obj: obj.console_server_port_count, badge=lambda obj: obj.console_server_port_count,
@@ -2189,7 +2249,7 @@ class DevicePowerPortsView(DeviceComponentsView):
table = tables.DevicePowerPortTable table = tables.DevicePowerPortTable
filterset = filtersets.PowerPortFilterSet filterset = filtersets.PowerPortFilterSet
filterset_form = forms.PowerPortFilterForm filterset_form = forms.PowerPortFilterForm
template_name = 'dcim/device/powerports.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
tab = ViewTab( tab = ViewTab(
label=_('Power Ports'), label=_('Power Ports'),
badge=lambda obj: obj.power_port_count, badge=lambda obj: obj.power_port_count,
@@ -2205,7 +2265,7 @@ class DevicePowerOutletsView(DeviceComponentsView):
table = tables.DevicePowerOutletTable table = tables.DevicePowerOutletTable
filterset = filtersets.PowerOutletFilterSet filterset = filtersets.PowerOutletFilterSet
filterset_form = forms.PowerOutletFilterForm filterset_form = forms.PowerOutletFilterForm
template_name = 'dcim/device/poweroutlets.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
tab = ViewTab( tab = ViewTab(
label=_('Power Outlets'), label=_('Power Outlets'),
badge=lambda obj: obj.power_outlet_count, badge=lambda obj: obj.power_outlet_count,
@@ -2221,6 +2281,7 @@ class DeviceInterfacesView(DeviceComponentsView):
table = tables.DeviceInterfaceTable table = tables.DeviceInterfaceTable
filterset = filtersets.InterfaceFilterSet filterset = filtersets.InterfaceFilterSet
filterset_form = forms.InterfaceFilterForm filterset_form = forms.InterfaceFilterForm
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
template_name = 'dcim/device/interfaces.html' template_name = 'dcim/device/interfaces.html'
tab = ViewTab( tab = ViewTab(
label=_('Interfaces'), label=_('Interfaces'),
@@ -2243,7 +2304,7 @@ class DeviceFrontPortsView(DeviceComponentsView):
table = tables.DeviceFrontPortTable table = tables.DeviceFrontPortTable
filterset = filtersets.FrontPortFilterSet filterset = filtersets.FrontPortFilterSet
filterset_form = forms.FrontPortFilterForm filterset_form = forms.FrontPortFilterForm
template_name = 'dcim/device/frontports.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
tab = ViewTab( tab = ViewTab(
label=_('Front Ports'), label=_('Front Ports'),
badge=lambda obj: obj.front_port_count, badge=lambda obj: obj.front_port_count,
@@ -2259,7 +2320,7 @@ class DeviceRearPortsView(DeviceComponentsView):
table = tables.DeviceRearPortTable table = tables.DeviceRearPortTable
filterset = filtersets.RearPortFilterSet filterset = filtersets.RearPortFilterSet
filterset_form = forms.RearPortFilterForm filterset_form = forms.RearPortFilterForm
template_name = 'dcim/device/rearports.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
tab = ViewTab( tab = ViewTab(
label=_('Rear Ports'), label=_('Rear Ports'),
badge=lambda obj: obj.rear_port_count, badge=lambda obj: obj.rear_port_count,
@@ -2275,11 +2336,7 @@ class DeviceModuleBaysView(DeviceComponentsView):
table = tables.DeviceModuleBayTable table = tables.DeviceModuleBayTable
filterset = filtersets.ModuleBayFilterSet filterset = filtersets.ModuleBayFilterSet
filterset_form = forms.ModuleBayFilterForm filterset_form = forms.ModuleBayFilterForm
template_name = 'dcim/device/modulebays.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
tab = ViewTab( tab = ViewTab(
label=_('Module Bays'), label=_('Module Bays'),
badge=lambda obj: obj.module_bay_count, badge=lambda obj: obj.module_bay_count,
@@ -2295,11 +2352,7 @@ class DeviceDeviceBaysView(DeviceComponentsView):
table = tables.DeviceDeviceBayTable table = tables.DeviceDeviceBayTable
filterset = filtersets.DeviceBayFilterSet filterset = filtersets.DeviceBayFilterSet
filterset_form = forms.DeviceBayFilterForm filterset_form = forms.DeviceBayFilterForm
template_name = 'dcim/device/devicebays.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
tab = ViewTab( tab = ViewTab(
label=_('Device Bays'), label=_('Device Bays'),
badge=lambda obj: obj.device_bay_count, badge=lambda obj: obj.device_bay_count,
@@ -2315,11 +2368,7 @@ class DeviceInventoryView(DeviceComponentsView):
table = tables.DeviceInventoryItemTable table = tables.DeviceInventoryItemTable
filterset = filtersets.InventoryItemFilterSet filterset = filtersets.InventoryItemFilterSet
filterset_form = forms.InventoryItemFilterForm filterset_form = forms.InventoryItemFilterForm
template_name = 'dcim/device/inventory.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
tab = ViewTab( tab = ViewTab(
label=_('Inventory Items'), label=_('Inventory Items'),
badge=lambda obj: obj.inventory_item_count, badge=lambda obj: obj.inventory_item_count,
@@ -2393,16 +2442,16 @@ class DeviceBulkEditView(generic.BulkEditView):
form = forms.DeviceBulkEditForm form = forms.DeviceBulkEditForm
@register_model_view(Device, 'bulk_delete', path='delete', detail=False) @register_model_view(Device, 'bulk_rename', path='rename', detail=False)
class DeviceBulkDeleteView(generic.BulkDeleteView): class DeviceBulkRenameView(generic.BulkRenameView):
queryset = Device.objects.prefetch_related('device_type__manufacturer') queryset = Device.objects.all()
filterset = filtersets.DeviceFilterSet filterset = filtersets.DeviceFilterSet
table = tables.DeviceTable table = tables.DeviceTable
@register_model_view(Device, 'bulk_rename', path='rename', detail=False) @register_model_view(Device, 'bulk_delete', path='delete', detail=False)
class DeviceBulkRenameView(generic.BulkRenameView): class DeviceBulkDeleteView(generic.BulkDeleteView):
queryset = Device.objects.all() queryset = Device.objects.prefetch_related('device_type__manufacturer')
filterset = filtersets.DeviceFilterSet filterset = filtersets.DeviceFilterSet
table = tables.DeviceTable table = tables.DeviceTable
@@ -2417,6 +2466,7 @@ class ModuleListView(generic.ObjectListView):
filterset = filtersets.ModuleFilterSet filterset = filtersets.ModuleFilterSet
filterset_form = forms.ModuleFilterForm filterset_form = forms.ModuleFilterForm
table = tables.ModuleTable table = tables.ModuleTable
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
@register_model_view(Module) @register_model_view(Module)
@@ -2472,11 +2522,6 @@ class ConsolePortListView(generic.ObjectListView):
filterset = filtersets.ConsolePortFilterSet filterset = filtersets.ConsolePortFilterSet
filterset_form = forms.ConsolePortFilterForm filterset_form = forms.ConsolePortFilterForm
table = tables.ConsolePortTable table = tables.ConsolePortTable
template_name = 'dcim/component_list.html'
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(ConsolePort) @register_model_view(ConsolePort)
@@ -2547,11 +2592,6 @@ class ConsoleServerPortListView(generic.ObjectListView):
filterset = filtersets.ConsoleServerPortFilterSet filterset = filtersets.ConsoleServerPortFilterSet
filterset_form = forms.ConsoleServerPortFilterForm filterset_form = forms.ConsoleServerPortFilterForm
table = tables.ConsoleServerPortTable table = tables.ConsoleServerPortTable
template_name = 'dcim/component_list.html'
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(ConsoleServerPort) @register_model_view(ConsoleServerPort)
@@ -2622,11 +2662,6 @@ class PowerPortListView(generic.ObjectListView):
filterset = filtersets.PowerPortFilterSet filterset = filtersets.PowerPortFilterSet
filterset_form = forms.PowerPortFilterForm filterset_form = forms.PowerPortFilterForm
table = tables.PowerPortTable table = tables.PowerPortTable
template_name = 'dcim/component_list.html'
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(PowerPort) @register_model_view(PowerPort)
@@ -2697,11 +2732,6 @@ class PowerOutletListView(generic.ObjectListView):
filterset = filtersets.PowerOutletFilterSet filterset = filtersets.PowerOutletFilterSet
filterset_form = forms.PowerOutletFilterForm filterset_form = forms.PowerOutletFilterForm
table = tables.PowerOutletTable table = tables.PowerOutletTable
template_name = 'dcim/component_list.html'
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(PowerOutlet) @register_model_view(PowerOutlet)
@@ -2772,11 +2802,6 @@ class InterfaceListView(generic.ObjectListView):
filterset = filtersets.InterfaceFilterSet filterset = filtersets.InterfaceFilterSet
filterset_form = forms.InterfaceFilterForm filterset_form = forms.InterfaceFilterForm
table = tables.InterfaceTable table = tables.InterfaceTable
template_name = 'dcim/component_list.html'
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(Interface) @register_model_view(Interface)
@@ -2920,11 +2945,6 @@ class FrontPortListView(generic.ObjectListView):
filterset = filtersets.FrontPortFilterSet filterset = filtersets.FrontPortFilterSet
filterset_form = forms.FrontPortFilterForm filterset_form = forms.FrontPortFilterForm
table = tables.FrontPortTable table = tables.FrontPortTable
template_name = 'dcim/component_list.html'
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(FrontPort) @register_model_view(FrontPort)
@@ -2995,11 +3015,6 @@ class RearPortListView(generic.ObjectListView):
filterset = filtersets.RearPortFilterSet filterset = filtersets.RearPortFilterSet
filterset_form = forms.RearPortFilterForm filterset_form = forms.RearPortFilterForm
table = tables.RearPortTable table = tables.RearPortTable
template_name = 'dcim/component_list.html'
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(RearPort) @register_model_view(RearPort)
@@ -3070,11 +3085,6 @@ class ModuleBayListView(generic.ObjectListView):
filterset = filtersets.ModuleBayFilterSet filterset = filtersets.ModuleBayFilterSet
filterset_form = forms.ModuleBayFilterForm filterset_form = forms.ModuleBayFilterForm
table = tables.ModuleBayTable table = tables.ModuleBayTable
template_name = 'dcim/component_list.html'
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(ModuleBay) @register_model_view(ModuleBay)
@@ -3136,11 +3146,6 @@ class DeviceBayListView(generic.ObjectListView):
filterset = filtersets.DeviceBayFilterSet filterset = filtersets.DeviceBayFilterSet
filterset_form = forms.DeviceBayFilterForm filterset_form = forms.DeviceBayFilterForm
table = tables.DeviceBayTable table = tables.DeviceBayTable
template_name = 'dcim/component_list.html'
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(DeviceBay) @register_model_view(DeviceBay)
@@ -3283,11 +3288,6 @@ class InventoryItemListView(generic.ObjectListView):
filterset = filtersets.InventoryItemFilterSet filterset = filtersets.InventoryItemFilterSet
filterset_form = forms.InventoryItemFilterForm filterset_form = forms.InventoryItemFilterForm
table = tables.InventoryItemTable table = tables.InventoryItemTable
template_name = 'dcim/component_list.html'
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(InventoryItem) @register_model_view(InventoryItem)
@@ -3410,6 +3410,11 @@ class InventoryItemRoleBulkEditView(generic.BulkEditView):
form = forms.InventoryItemRoleBulkEditForm form = forms.InventoryItemRoleBulkEditForm
@register_model_view(InventoryItemRole, 'bulk_rename', path='rename', detail=False)
class InventoryItemRoleBulkRenameView(generic.BulkRenameView):
queryset = InventoryItemRole.objects.all()
@register_model_view(InventoryItemRole, 'bulk_delete', path='delete', detail=False) @register_model_view(InventoryItemRole, 'bulk_delete', path='delete', detail=False)
class InventoryItemRoleBulkDeleteView(generic.BulkDeleteView): class InventoryItemRoleBulkDeleteView(generic.BulkDeleteView):
queryset = InventoryItemRole.objects.annotate( queryset = InventoryItemRole.objects.annotate(
@@ -3607,6 +3612,12 @@ class CableBulkEditView(generic.BulkEditView):
form = forms.CableBulkEditForm form = forms.CableBulkEditForm
@register_model_view(Cable, 'bulk_rename', path='rename', detail=False)
class CableBulkRenameView(generic.BulkRenameView):
queryset = Cable.objects.all()
field_name = 'label'
@register_model_view(Cable, 'bulk_delete', path='delete', detail=False) @register_model_view(Cable, 'bulk_delete', path='delete', detail=False)
class CableBulkDeleteView(generic.BulkDeleteView): class CableBulkDeleteView(generic.BulkDeleteView):
queryset = Cable.objects.prefetch_related( queryset = Cable.objects.prefetch_related(
@@ -3627,9 +3638,7 @@ class ConsoleConnectionsListView(generic.ObjectListView):
filterset_form = forms.ConsoleConnectionFilterForm filterset_form = forms.ConsoleConnectionFilterForm
table = tables.ConsoleConnectionTable table = tables.ConsoleConnectionTable
template_name = 'dcim/connections_list.html' template_name = 'dcim/connections_list.html'
actions = { actions = (BulkExport,)
'export': {'view'},
}
def get_extra_context(self, request): def get_extra_context(self, request):
return { return {
@@ -3643,9 +3652,7 @@ class PowerConnectionsListView(generic.ObjectListView):
filterset_form = forms.PowerConnectionFilterForm filterset_form = forms.PowerConnectionFilterForm
table = tables.PowerConnectionTable table = tables.PowerConnectionTable
template_name = 'dcim/connections_list.html' template_name = 'dcim/connections_list.html'
actions = { actions = (BulkExport,)
'export': {'view'},
}
def get_extra_context(self, request): def get_extra_context(self, request):
return { return {
@@ -3659,9 +3666,7 @@ class InterfaceConnectionsListView(generic.ObjectListView):
filterset_form = forms.InterfaceConnectionFilterForm filterset_form = forms.InterfaceConnectionFilterForm
table = tables.InterfaceConnectionTable table = tables.InterfaceConnectionTable
template_name = 'dcim/connections_list.html' template_name = 'dcim/connections_list.html'
actions = { actions = (BulkExport,)
'export': {'view'},
}
def get_extra_context(self, request): def get_extra_context(self, request):
return { return {
@@ -3905,6 +3910,11 @@ class VirtualChassisBulkEditView(generic.BulkEditView):
form = forms.VirtualChassisBulkEditForm form = forms.VirtualChassisBulkEditForm
@register_model_view(VirtualChassis, 'bulk_rename', path='rename', detail=False)
class VirtualChassisBulkRenameView(generic.BulkRenameView):
queryset = VirtualChassis.objects.all()
@register_model_view(VirtualChassis, 'bulk_delete', path='delete', detail=False) @register_model_view(VirtualChassis, 'bulk_delete', path='delete', detail=False)
class VirtualChassisBulkDeleteView(generic.BulkDeleteView): class VirtualChassisBulkDeleteView(generic.BulkDeleteView):
queryset = VirtualChassis.objects.all() queryset = VirtualChassis.objects.all()
@@ -3962,6 +3972,11 @@ class PowerPanelBulkEditView(generic.BulkEditView):
form = forms.PowerPanelBulkEditForm form = forms.PowerPanelBulkEditForm
@register_model_view(PowerPanel, 'bulk_rename', path='rename', detail=False)
class PowerPanelBulkRenameView(generic.BulkRenameView):
queryset = PowerPanel.objects.all()
@register_model_view(PowerPanel, 'bulk_delete', path='delete', detail=False) @register_model_view(PowerPanel, 'bulk_delete', path='delete', detail=False)
class PowerPanelBulkDeleteView(generic.BulkDeleteView): class PowerPanelBulkDeleteView(generic.BulkDeleteView):
queryset = PowerPanel.objects.annotate( queryset = PowerPanel.objects.annotate(
@@ -4014,6 +4029,11 @@ class PowerFeedBulkEditView(generic.BulkEditView):
form = forms.PowerFeedBulkEditForm form = forms.PowerFeedBulkEditForm
@register_model_view(PowerFeed, 'bulk_rename', path='rename', detail=False)
class PowerFeedBulkRenameView(generic.BulkRenameView):
queryset = PowerFeed.objects.all()
@register_model_view(PowerFeed, 'bulk_disconnect', path='disconnect', detail=False) @register_model_view(PowerFeed, 'bulk_disconnect', path='disconnect', detail=False)
class PowerFeedBulkDisconnectView(BulkDisconnectView): class PowerFeedBulkDisconnectView(BulkDisconnectView):
queryset = PowerFeed.objects.all() queryset = PowerFeed.objects.all()
@@ -4042,6 +4062,7 @@ class VirtualDeviceContextListView(generic.ObjectListView):
filterset = filtersets.VirtualDeviceContextFilterSet filterset = filtersets.VirtualDeviceContextFilterSet
filterset_form = forms.VirtualDeviceContextFilterForm filterset_form = forms.VirtualDeviceContextFilterForm
table = tables.VirtualDeviceContextTable table = tables.VirtualDeviceContextTable
actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
@register_model_view(VirtualDeviceContext) @register_model_view(VirtualDeviceContext)
@@ -4086,6 +4107,11 @@ class VirtualDeviceContextBulkEditView(generic.BulkEditView):
form = forms.VirtualDeviceContextBulkEditForm form = forms.VirtualDeviceContextBulkEditForm
@register_model_view(VirtualDeviceContext, 'bulk_rename', path='rename', detail=False)
class VirtualDeviceContextBulkRenameView(generic.BulkRenameView):
queryset = VirtualDeviceContext.objects.all()
@register_model_view(VirtualDeviceContext, 'bulk_delete', path='delete', detail=False) @register_model_view(VirtualDeviceContext, 'bulk_delete', path='delete', detail=False)
class VirtualDeviceContextBulkDeleteView(generic.BulkDeleteView): class VirtualDeviceContextBulkDeleteView(generic.BulkDeleteView):
queryset = VirtualDeviceContext.objects.all() queryset = VirtualDeviceContext.objects.all()
@@ -4103,6 +4129,7 @@ class MACAddressListView(generic.ObjectListView):
filterset = filtersets.MACAddressFilterSet filterset = filtersets.MACAddressFilterSet
filterset_form = forms.MACAddressFilterForm filterset_form = forms.MACAddressFilterForm
table = tables.MACAddressTable table = tables.MACAddressTable
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
@register_model_view(MACAddress) @register_model_view(MACAddress)

View File

@@ -90,7 +90,10 @@ class ScriptJob(JobRunner):
request: The WSGI request associated with this execution (if any) request: The WSGI request associated with this execution (if any)
commit: Passed through to Script.run() commit: Passed through to Script.run()
""" """
script = 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 # Add files to form data
if request: if request:
@@ -100,6 +103,7 @@ class ScriptJob(JobRunner):
# Add the current request as a property of the script # Add the current request as a property of the script
script.request = request script.request = request
self.logger.debug(f"Request ID: {request.id}")
# Execute the script. If commit is True, wrap it with the event_tracking context manager to ensure we process # Execute the script. If commit is True, wrap it with the event_tracking context manager to ensure we process
# change logging, event rules, etc. # change logging, event rules, etc.

View File

@@ -14,12 +14,13 @@ from jinja2.exceptions import TemplateError
from core.choices import ManagedFileRootPathChoices from core.choices import ManagedFileRootPathChoices
from core.models import Job from core.models import Job
from core.object_actions import BulkSync
from dcim.models import Device, DeviceRole, Platform from dcim.models import Device, DeviceRole, Platform
from extras.choices import LogLevelChoices from extras.choices import LogLevelChoices
from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
from extras.dashboard.utils import get_widget_class from extras.dashboard.utils import get_widget_class
from extras.utils import SharedObjectViewMixin from extras.utils import SharedObjectViewMixin
from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.object_actions import *
from netbox.views import generic from netbox.views import generic
from netbox.views.generic.mixins import TableMixin from netbox.views.generic.mixins import TableMixin
from utilities.forms import ConfirmationForm, get_field_value from utilities.forms import ConfirmationForm, get_field_value
@@ -96,6 +97,11 @@ class CustomFieldBulkEditView(generic.BulkEditView):
form = forms.CustomFieldBulkEditForm form = forms.CustomFieldBulkEditForm
@register_model_view(CustomField, 'bulk_rename', path='rename', detail=False)
class CustomFieldBulkRenameView(generic.BulkRenameView):
queryset = CustomField.objects.all()
@register_model_view(CustomField, 'bulk_delete', path='delete', detail=False) @register_model_view(CustomField, 'bulk_delete', path='delete', detail=False)
class CustomFieldBulkDeleteView(generic.BulkDeleteView): class CustomFieldBulkDeleteView(generic.BulkDeleteView):
queryset = CustomField.objects.select_related('choice_set') queryset = CustomField.objects.select_related('choice_set')
@@ -165,6 +171,11 @@ class CustomFieldChoiceSetBulkEditView(generic.BulkEditView):
form = forms.CustomFieldChoiceSetBulkEditForm form = forms.CustomFieldChoiceSetBulkEditForm
@register_model_view(CustomFieldChoiceSet, 'bulk_rename', path='rename', detail=False)
class CustomFieldChoiceSetBulkRenameView(generic.BulkRenameView):
queryset = CustomFieldChoiceSet.objects.all()
@register_model_view(CustomFieldChoiceSet, 'bulk_delete', path='delete', detail=False) @register_model_view(CustomFieldChoiceSet, 'bulk_delete', path='delete', detail=False)
class CustomFieldChoiceSetBulkDeleteView(generic.BulkDeleteView): class CustomFieldChoiceSetBulkDeleteView(generic.BulkDeleteView):
queryset = CustomFieldChoiceSet.objects.all() queryset = CustomFieldChoiceSet.objects.all()
@@ -215,6 +226,11 @@ class CustomLinkBulkEditView(generic.BulkEditView):
form = forms.CustomLinkBulkEditForm form = forms.CustomLinkBulkEditForm
@register_model_view(CustomLink, 'bulk_rename', path='rename', detail=False)
class CustomLinkBulkRenameView(generic.BulkRenameView):
queryset = CustomLink.objects.all()
@register_model_view(CustomLink, 'bulk_delete', path='delete', detail=False) @register_model_view(CustomLink, 'bulk_delete', path='delete', detail=False)
class CustomLinkBulkDeleteView(generic.BulkDeleteView): class CustomLinkBulkDeleteView(generic.BulkDeleteView):
queryset = CustomLink.objects.all() queryset = CustomLink.objects.all()
@@ -232,11 +248,7 @@ class ExportTemplateListView(generic.ObjectListView):
filterset = filtersets.ExportTemplateFilterSet filterset = filtersets.ExportTemplateFilterSet
filterset_form = forms.ExportTemplateFilterForm filterset_form = forms.ExportTemplateFilterForm
table = tables.ExportTemplateTable table = tables.ExportTemplateTable
template_name = 'extras/exporttemplate_list.html' actions = (AddObject, BulkImport, BulkSync, BulkExport, BulkEdit, BulkRename, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_sync': {'sync'},
}
@register_model_view(ExportTemplate) @register_model_view(ExportTemplate)
@@ -270,6 +282,11 @@ class ExportTemplateBulkEditView(generic.BulkEditView):
form = forms.ExportTemplateBulkEditForm form = forms.ExportTemplateBulkEditForm
@register_model_view(ExportTemplate, 'bulk_rename', path='rename', detail=False)
class ExportTemplateBulkRenameView(generic.BulkRenameView):
queryset = ExportTemplate.objects.all()
@register_model_view(ExportTemplate, 'bulk_delete', path='delete', detail=False) @register_model_view(ExportTemplate, 'bulk_delete', path='delete', detail=False)
class ExportTemplateBulkDeleteView(generic.BulkDeleteView): class ExportTemplateBulkDeleteView(generic.BulkDeleteView):
queryset = ExportTemplate.objects.all() queryset = ExportTemplate.objects.all()
@@ -330,6 +347,11 @@ class SavedFilterBulkEditView(SharedObjectViewMixin, generic.BulkEditView):
form = forms.SavedFilterBulkEditForm form = forms.SavedFilterBulkEditForm
@register_model_view(SavedFilter, 'bulk_rename', path='rename', detail=False)
class SavedFilterBulkRenameView(generic.BulkRenameView):
queryset = SavedFilter.objects.all()
@register_model_view(SavedFilter, 'bulk_delete', path='delete', detail=False) @register_model_view(SavedFilter, 'bulk_delete', path='delete', detail=False)
class SavedFilterBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView): class SavedFilterBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView):
queryset = SavedFilter.objects.all() queryset = SavedFilter.objects.all()
@@ -347,9 +369,7 @@ class TableConfigListView(SharedObjectViewMixin, generic.ObjectListView):
filterset = filtersets.TableConfigFilterSet filterset = filtersets.TableConfigFilterSet
filterset_form = forms.TableConfigFilterForm filterset_form = forms.TableConfigFilterForm
table = tables.TableConfigTable table = tables.TableConfigTable
actions = { actions = (BulkExport, BulkEdit, BulkRename, BulkDelete)
'export': {'view'},
}
@register_model_view(TableConfig) @register_model_view(TableConfig)
@@ -389,6 +409,11 @@ class TableConfigBulkEditView(SharedObjectViewMixin, generic.BulkEditView):
form = forms.TableConfigBulkEditForm form = forms.TableConfigBulkEditForm
@register_model_view(TableConfig, 'bulk_rename', path='rename', detail=False)
class TableConfigBulkRenameView(generic.BulkRenameView):
queryset = TableConfig.objects.all()
@register_model_view(TableConfig, 'bulk_delete', path='delete', detail=False) @register_model_view(TableConfig, 'bulk_delete', path='delete', detail=False)
class TableConfigBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView): class TableConfigBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView):
queryset = TableConfig.objects.all() queryset = TableConfig.objects.all()
@@ -470,6 +495,11 @@ class NotificationGroupBulkEditView(generic.BulkEditView):
form = forms.NotificationGroupBulkEditForm form = forms.NotificationGroupBulkEditForm
@register_model_view(NotificationGroup, 'bulk_rename', path='rename', detail=False)
class NotificationGroupBulkRenameView(generic.BulkRenameView):
queryset = NotificationGroup.objects.all()
@register_model_view(NotificationGroup, 'bulk_delete', path='delete', detail=False) @register_model_view(NotificationGroup, 'bulk_delete', path='delete', detail=False)
class NotificationGroupBulkDeleteView(generic.BulkDeleteView): class NotificationGroupBulkDeleteView(generic.BulkDeleteView):
queryset = NotificationGroup.objects.all() queryset = NotificationGroup.objects.all()
@@ -616,6 +646,11 @@ class WebhookBulkEditView(generic.BulkEditView):
form = forms.WebhookBulkEditForm form = forms.WebhookBulkEditForm
@register_model_view(Webhook, 'bulk_rename', path='rename', detail=False)
class WebhookBulkRenameView(generic.BulkRenameView):
queryset = Webhook.objects.all()
@register_model_view(Webhook, 'bulk_delete', path='delete', detail=False) @register_model_view(Webhook, 'bulk_delete', path='delete', detail=False)
class WebhookBulkDeleteView(generic.BulkDeleteView): class WebhookBulkDeleteView(generic.BulkDeleteView):
queryset = Webhook.objects.all() queryset = Webhook.objects.all()
@@ -666,6 +701,11 @@ class EventRuleBulkEditView(generic.BulkEditView):
form = forms.EventRuleBulkEditForm form = forms.EventRuleBulkEditForm
@register_model_view(EventRule, 'bulk_rename', path='rename', detail=False)
class EventRuleBulkRenameView(generic.BulkRenameView):
queryset = EventRule.objects.all()
@register_model_view(EventRule, 'bulk_delete', path='delete', detail=False) @register_model_view(EventRule, 'bulk_delete', path='delete', detail=False)
class EventRuleBulkDeleteView(generic.BulkDeleteView): class EventRuleBulkDeleteView(generic.BulkDeleteView):
queryset = EventRule.objects.all() queryset = EventRule.objects.all()
@@ -740,6 +780,11 @@ class TagBulkEditView(generic.BulkEditView):
form = forms.TagBulkEditForm form = forms.TagBulkEditForm
@register_model_view(Tag, 'bulk_rename', path='rename', detail=False)
class TagBulkRenameView(generic.BulkRenameView):
queryset = Tag.objects.all()
@register_model_view(Tag, 'bulk_delete', path='delete', detail=False) @register_model_view(Tag, 'bulk_delete', path='delete', detail=False)
class TagBulkDeleteView(generic.BulkDeleteView): class TagBulkDeleteView(generic.BulkDeleteView):
queryset = Tag.objects.annotate( queryset = Tag.objects.annotate(
@@ -758,13 +803,7 @@ class ConfigContextListView(generic.ObjectListView):
filterset = filtersets.ConfigContextFilterSet filterset = filtersets.ConfigContextFilterSet
filterset_form = forms.ConfigContextFilterForm filterset_form = forms.ConfigContextFilterForm
table = tables.ConfigContextTable table = tables.ConfigContextTable
template_name = 'extras/configcontext_list.html' actions = (AddObject, BulkSync, BulkEdit, BulkRename, BulkDelete)
actions = {
'add': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
'bulk_sync': {'sync'},
}
@register_model_view(ConfigContext) @register_model_view(ConfigContext)
@@ -825,6 +864,11 @@ class ConfigContextBulkEditView(generic.BulkEditView):
form = forms.ConfigContextBulkEditForm form = forms.ConfigContextBulkEditForm
@register_model_view(ConfigContext, 'bulk_rename', path='rename', detail=False)
class ConfigContextBulkRenameView(generic.BulkRenameView):
queryset = ConfigContext.objects.all()
@register_model_view(ConfigContext, 'bulk_delete', path='delete', detail=False) @register_model_view(ConfigContext, 'bulk_delete', path='delete', detail=False)
class ConfigContextBulkDeleteView(generic.BulkDeleteView): class ConfigContextBulkDeleteView(generic.BulkDeleteView):
queryset = ConfigContext.objects.all() queryset = ConfigContext.objects.all()
@@ -877,11 +921,7 @@ class ConfigTemplateListView(generic.ObjectListView):
filterset = filtersets.ConfigTemplateFilterSet filterset = filtersets.ConfigTemplateFilterSet
filterset_form = forms.ConfigTemplateFilterForm filterset_form = forms.ConfigTemplateFilterForm
table = tables.ConfigTemplateTable table = tables.ConfigTemplateTable
template_name = 'extras/configtemplate_list.html' actions = (AddObject, BulkImport, BulkExport, BulkSync, BulkEdit, BulkRename, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_sync': {'sync'},
}
@register_model_view(ConfigTemplate) @register_model_view(ConfigTemplate)
@@ -915,6 +955,11 @@ class ConfigTemplateBulkEditView(generic.BulkEditView):
form = forms.ConfigTemplateBulkEditForm form = forms.ConfigTemplateBulkEditForm
@register_model_view(ConfigTemplate, 'bulk_rename', path='rename', detail=False)
class ConfigTemplateBulkRenameView(generic.BulkRenameView):
queryset = ConfigTemplate.objects.all()
@register_model_view(ConfigTemplate, 'bulk_delete', path='delete', detail=False) @register_model_view(ConfigTemplate, 'bulk_delete', path='delete', detail=False)
class ConfigTemplateBulkDeleteView(generic.BulkDeleteView): class ConfigTemplateBulkDeleteView(generic.BulkDeleteView):
queryset = ConfigTemplate.objects.all() queryset = ConfigTemplate.objects.all()
@@ -992,9 +1037,7 @@ class ImageAttachmentListView(generic.ObjectListView):
filterset = filtersets.ImageAttachmentFilterSet filterset = filtersets.ImageAttachmentFilterSet
filterset_form = forms.ImageAttachmentFilterForm filterset_form = forms.ImageAttachmentFilterForm
table = tables.ImageAttachmentTable table = tables.ImageAttachmentTable
actions = { actions = (BulkExport,)
'export': {'view'},
}
@register_model_view(ImageAttachment, 'add', detail=False) @register_model_view(ImageAttachment, 'add', detail=False)
@@ -1038,12 +1081,7 @@ class JournalEntryListView(generic.ObjectListView):
filterset = filtersets.JournalEntryFilterSet filterset = filtersets.JournalEntryFilterSet
filterset_form = forms.JournalEntryFilterForm filterset_form = forms.JournalEntryFilterForm
table = tables.JournalEntryTable table = tables.JournalEntryTable
actions = { actions = (BulkImport, BulkEdit, BulkDelete)
'export': {'view'},
'bulk_import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
}
@register_model_view(JournalEntry) @register_model_view(JournalEntry)

View File

@@ -10,6 +10,7 @@ from dcim.filtersets import InterfaceFilterSet
from dcim.forms import InterfaceFilterForm from dcim.forms import InterfaceFilterForm
from dcim.models import Device, Interface, Site from dcim.models import Device, Interface, Site
from ipam.tables import VLANTranslationRuleTable from ipam.tables import VLANTranslationRuleTable
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
from netbox.views import generic from netbox.views import generic
from utilities.query import count_related from utilities.query import count_related
from utilities.tables import get_table_ordering from utilities.tables import get_table_ordering
@@ -86,6 +87,11 @@ class VRFBulkEditView(generic.BulkEditView):
form = forms.VRFBulkEditForm form = forms.VRFBulkEditForm
@register_model_view(VRF, 'bulk_rename', path='rename', detail=False)
class VRFBulkRenameView(generic.BulkRenameView):
queryset = VRF.objects.all()
@register_model_view(VRF, 'bulk_delete', path='delete', detail=False) @register_model_view(VRF, 'bulk_delete', path='delete', detail=False)
class VRFBulkDeleteView(generic.BulkDeleteView): class VRFBulkDeleteView(generic.BulkDeleteView):
queryset = VRF.objects.all() queryset = VRF.objects.all()
@@ -136,6 +142,11 @@ class RouteTargetBulkEditView(generic.BulkEditView):
form = forms.RouteTargetBulkEditForm form = forms.RouteTargetBulkEditForm
@register_model_view(RouteTarget, 'bulk_rename', path='rename', detail=False)
class RouteTargetBulkRenameView(generic.BulkRenameView):
queryset = RouteTarget.objects.all()
@register_model_view(RouteTarget, 'bulk_delete', path='delete', detail=False) @register_model_view(RouteTarget, 'bulk_delete', path='delete', detail=False)
class RouteTargetBulkDeleteView(generic.BulkDeleteView): class RouteTargetBulkDeleteView(generic.BulkDeleteView):
queryset = RouteTarget.objects.all() queryset = RouteTarget.objects.all()
@@ -195,6 +206,11 @@ class RIRBulkEditView(generic.BulkEditView):
form = forms.RIRBulkEditForm form = forms.RIRBulkEditForm
@register_model_view(RIR, 'bulk_rename', path='rename', detail=False)
class RIRBulkRenameView(generic.BulkRenameView):
queryset = RIR.objects.all()
@register_model_view(RIR, 'bulk_delete', path='delete', detail=False) @register_model_view(RIR, 'bulk_delete', path='delete', detail=False)
class RIRBulkDeleteView(generic.BulkDeleteView): class RIRBulkDeleteView(generic.BulkDeleteView):
queryset = RIR.objects.annotate( queryset = RIR.objects.annotate(
@@ -268,6 +284,11 @@ class ASNRangeBulkEditView(generic.BulkEditView):
form = forms.ASNRangeBulkEditForm form = forms.ASNRangeBulkEditForm
@register_model_view(ASNRange, 'bulk_rename', path='rename', detail=False)
class ASNRangeBulkRenameView(generic.BulkRenameView):
queryset = ASNRange.objects.all()
@register_model_view(ASNRange, 'bulk_delete', path='delete', detail=False) @register_model_view(ASNRange, 'bulk_delete', path='delete', detail=False)
class ASNRangeBulkDeleteView(generic.BulkDeleteView): class ASNRangeBulkDeleteView(generic.BulkDeleteView):
queryset = ASNRange.objects.annotate_asn_counts() queryset = ASNRange.objects.annotate_asn_counts()
@@ -335,6 +356,11 @@ class ASNBulkEditView(generic.BulkEditView):
form = forms.ASNBulkEditForm form = forms.ASNBulkEditForm
@register_model_view(ASN, 'bulk_rename', path='rename', detail=False)
class ASNBulkRenameView(generic.BulkRenameView):
queryset = ASN.objects.all()
@register_model_view(ASN, 'bulk_delete', path='delete', detail=False) @register_model_view(ASN, 'bulk_delete', path='delete', detail=False)
class ASNBulkDeleteView(generic.BulkDeleteView): class ASNBulkDeleteView(generic.BulkDeleteView):
queryset = ASN.objects.annotate( queryset = ASN.objects.annotate(
@@ -356,6 +382,7 @@ class AggregateListView(generic.ObjectListView):
filterset = filtersets.AggregateFilterSet filterset = filtersets.AggregateFilterSet
filterset_form = forms.AggregateFilterForm filterset_form = forms.AggregateFilterForm
table = tables.AggregateTable table = tables.AggregateTable
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
@register_model_view(Aggregate) @register_model_view(Aggregate)
@@ -488,6 +515,11 @@ class RoleBulkEditView(generic.BulkEditView):
form = forms.RoleBulkEditForm form = forms.RoleBulkEditForm
@register_model_view(Role, 'bulk_rename', path='rename', detail=False)
class RoleBulkRenameView(generic.BulkRenameView):
queryset = Role.objects.all()
@register_model_view(Role, 'bulk_delete', path='delete', detail=False) @register_model_view(Role, 'bulk_delete', path='delete', detail=False)
class RoleBulkDeleteView(generic.BulkDeleteView): class RoleBulkDeleteView(generic.BulkDeleteView):
queryset = Role.objects.all() queryset = Role.objects.all()
@@ -506,6 +538,7 @@ class PrefixListView(generic.ObjectListView):
filterset_form = forms.PrefixFilterForm filterset_form = forms.PrefixFilterForm
table = tables.PrefixTable table = tables.PrefixTable
template_name = 'ipam/prefix_list.html' template_name = 'ipam/prefix_list.html'
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
@register_model_view(Prefix) @register_model_view(Prefix)
@@ -766,6 +799,11 @@ class IPRangeBulkEditView(generic.BulkEditView):
form = forms.IPRangeBulkEditForm form = forms.IPRangeBulkEditForm
@register_model_view(IPRange, 'bulk_rename', path='rename', detail=False)
class IPRangeBulkRenameView(generic.BulkRenameView):
queryset = IPRange.objects.all()
@register_model_view(IPRange, 'bulk_delete', path='delete', detail=False) @register_model_view(IPRange, 'bulk_delete', path='delete', detail=False)
class IPRangeBulkDeleteView(generic.BulkDeleteView): class IPRangeBulkDeleteView(generic.BulkDeleteView):
queryset = IPRange.objects.all() queryset = IPRange.objects.all()
@@ -783,6 +821,7 @@ class IPAddressListView(generic.ObjectListView):
filterset = filtersets.IPAddressFilterSet filterset = filtersets.IPAddressFilterSet
filterset_form = forms.IPAddressFilterForm filterset_form = forms.IPAddressFilterForm
table = tables.IPAddressTable table = tables.IPAddressTable
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
@register_model_view(IPAddress) @register_model_view(IPAddress)
@@ -1006,6 +1045,11 @@ class VLANGroupBulkEditView(generic.BulkEditView):
form = forms.VLANGroupBulkEditForm form = forms.VLANGroupBulkEditForm
@register_model_view(VLANGroup, 'bulk_rename', path='rename', detail=False)
class VLANGroupBulkRenameView(generic.BulkRenameView):
queryset = VLANGroup.objects.all()
@register_model_view(VLANGroup, 'bulk_delete', path='delete', detail=False) @register_model_view(VLANGroup, 'bulk_delete', path='delete', detail=False)
class VLANGroupBulkDeleteView(generic.BulkDeleteView): class VLANGroupBulkDeleteView(generic.BulkDeleteView):
queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags') queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
@@ -1095,6 +1139,11 @@ class VLANTranslationPolicyBulkEditView(generic.BulkEditView):
form = forms.VLANTranslationPolicyBulkEditForm form = forms.VLANTranslationPolicyBulkEditForm
@register_model_view(VLANTranslationPolicy, 'bulk_rename', path='rename', detail=False)
class VLANTranslationPolicyBulkRenameView(generic.BulkRenameView):
queryset = VLANTranslationPolicy.objects.all()
@register_model_view(VLANTranslationPolicy, 'bulk_delete', path='delete', detail=False) @register_model_view(VLANTranslationPolicy, 'bulk_delete', path='delete', detail=False)
class VLANTranslationPolicyBulkDeleteView(generic.BulkDeleteView): class VLANTranslationPolicyBulkDeleteView(generic.BulkDeleteView):
queryset = VLANTranslationPolicy.objects.all() queryset = VLANTranslationPolicy.objects.all()
@@ -1112,6 +1161,7 @@ class VLANTranslationRuleListView(generic.ObjectListView):
filterset = filtersets.VLANTranslationRuleFilterSet filterset = filtersets.VLANTranslationRuleFilterSet
filterset_form = forms.VLANTranslationRuleFilterForm filterset_form = forms.VLANTranslationRuleFilterForm
table = tables.VLANTranslationRuleTable table = tables.VLANTranslationRuleTable
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
@register_model_view(VLANTranslationRule) @register_model_view(VLANTranslationRule)
@@ -1244,6 +1294,11 @@ class FHRPGroupBulkEditView(generic.BulkEditView):
form = forms.FHRPGroupBulkEditForm form = forms.FHRPGroupBulkEditForm
@register_model_view(FHRPGroup, 'bulk_rename', path='rename', detail=False)
class FHRPGroupBulkRenameView(generic.BulkRenameView):
queryset = FHRPGroup.objects.all()
@register_model_view(FHRPGroup, 'bulk_delete', path='delete', detail=False) @register_model_view(FHRPGroup, 'bulk_delete', path='delete', detail=False)
class FHRPGroupBulkDeleteView(generic.BulkDeleteView): class FHRPGroupBulkDeleteView(generic.BulkDeleteView):
queryset = FHRPGroup.objects.all() queryset = FHRPGroup.objects.all()
@@ -1371,6 +1426,11 @@ class VLANBulkEditView(generic.BulkEditView):
form = forms.VLANBulkEditForm form = forms.VLANBulkEditForm
@register_model_view(VLAN, 'bulk_rename', path='rename', detail=False)
class VLANBulkRenameView(generic.BulkRenameView):
queryset = VLAN.objects.all()
@register_model_view(VLAN, 'bulk_delete', path='delete', detail=False) @register_model_view(VLAN, 'bulk_delete', path='delete', detail=False)
class VLANBulkDeleteView(generic.BulkDeleteView): class VLANBulkDeleteView(generic.BulkDeleteView):
queryset = VLAN.objects.all() queryset = VLAN.objects.all()
@@ -1421,6 +1481,11 @@ class ServiceTemplateBulkEditView(generic.BulkEditView):
form = forms.ServiceTemplateBulkEditForm form = forms.ServiceTemplateBulkEditForm
@register_model_view(ServiceTemplate, 'bulk_rename', path='rename', detail=False)
class ServiceTemplateBulkRenameView(generic.BulkRenameView):
queryset = ServiceTemplate.objects.all()
@register_model_view(ServiceTemplate, 'bulk_delete', path='delete', detail=False) @register_model_view(ServiceTemplate, 'bulk_delete', path='delete', detail=False)
class ServiceTemplateBulkDeleteView(generic.BulkDeleteView): class ServiceTemplateBulkDeleteView(generic.BulkDeleteView):
queryset = ServiceTemplate.objects.all() queryset = ServiceTemplate.objects.all()
@@ -1488,6 +1553,11 @@ class ServiceBulkEditView(generic.BulkEditView):
form = forms.ServiceBulkEditForm form = forms.ServiceBulkEditForm
@register_model_view(Service, 'bulk_rename', path='rename', detail=False)
class ServiceBulkRenameView(generic.BulkRenameView):
queryset = Service.objects.all()
@register_model_view(Service, 'bulk_delete', path='delete', detail=False) @register_model_view(Service, 'bulk_delete', path='delete', detail=False)
class ServiceBulkDeleteView(generic.BulkDeleteView): class ServiceBulkDeleteView(generic.BulkDeleteView):
queryset = Service.objects.prefetch_related('parent') queryset = Service.objects.prefetch_related('parent')

View File

@@ -28,7 +28,8 @@ ADVISORY_LOCK_KEYS = {
'job-schedules': 110100, 'job-schedules': 110100,
} }
# Default view action permission mapping # TODO: Remove in NetBox v4.6
# Legacy default view action permission mapping
DEFAULT_ACTION_PERMISSIONS = { DEFAULT_ACTION_PERMISSIONS = {
'add': {'add'}, 'add': {'add'},
'export': {'view'}, 'export': {'view'},

View File

@@ -34,6 +34,19 @@ def system_job(interval):
return _wrapper return _wrapper
class JobLogHandler(logging.Handler):
"""
A logging handler which records entries on a Job.
"""
def __init__(self, job, *args, **kwargs):
super().__init__(*args, **kwargs)
self.job = job
def emit(self, record):
# Enter the record in the log of the associated Job
self.job.log(record)
class JobRunner(ABC): class JobRunner(ABC):
""" """
Background Job helper class. Background Job helper class.
@@ -52,6 +65,11 @@ class JobRunner(ABC):
""" """
self.job = job self.job = job
# Initiate the system logger
self.logger = logging.getLogger(f"netbox.jobs.{self.__class__.__name__}")
self.logger.setLevel(logging.DEBUG)
self.logger.addHandler(JobLogHandler(job))
@classproperty @classproperty
def name(cls): def name(cls):
return getattr(cls.Meta, 'name', cls.__name__) return getattr(cls.Meta, 'name', cls.__name__)

View 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'

View File

@@ -11,7 +11,10 @@ from core.choices import JobStatusChoices
class TestJobRunner(JobRunner): class TestJobRunner(JobRunner):
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
pass self.logger.debug("Debug message")
self.logger.info("Info message")
self.logger.warning("Warning message")
self.logger.error("Error message")
class JobRunnerTestCase(TestCase): class JobRunnerTestCase(TestCase):
@@ -47,8 +50,16 @@ class JobRunnerTest(JobRunnerTestCase):
def test_handle(self): def test_handle(self):
job = TestJobRunner.enqueue(immediate=True) job = TestJobRunner.enqueue(immediate=True)
# Check job status
self.assertEqual(job.status, JobStatusChoices.STATUS_COMPLETED) self.assertEqual(job.status, JobStatusChoices.STATUS_COMPLETED)
# Check logging
self.assertEqual(len(job.log_entries), 4)
self.assertEqual(job.log_entries[0]['message'], "Debug message")
self.assertEqual(job.log_entries[1]['message'], "Info message")
self.assertEqual(job.log_entries[2]['message'], "Warning message")
self.assertEqual(job.log_entries[3]['message'], "Error message")
def test_handle_errored(self): def test_handle_errored(self):
class ErroredJobRunner(TestJobRunner): class ErroredJobRunner(TestJobRunner):
EXP = Exception('Test error') EXP = Exception('Test error')

View File

@@ -22,6 +22,7 @@ from core.models import ObjectType
from core.signals import clear_events from core.signals import clear_events
from extras.choices import CustomFieldUIEditableChoices from extras.choices import CustomFieldUIEditableChoices
from extras.models import CustomField, ExportTemplate from extras.models import CustomField, ExportTemplate
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename
from utilities.error_handlers import handle_protectederror from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
@@ -54,12 +55,12 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
Attributes: Attributes:
filterset: A django-filter FilterSet that is applied to the queryset filterset: A django-filter FilterSet that is applied to the queryset
filterset_form: The form class used to render filter options filterset_form: The form class used to render filter options
actions: A mapping of supported actions to their required permissions. When adding custom actions, bulk actions: An iterable of ObjectAction subclasses (see ActionsMixin)
action names must be prefixed with `bulk_`. (See ActionsMixin.)
""" """
template_name = 'generic/object_list.html' template_name = 'generic/object_list.html'
filterset = None filterset = None
filterset_form = None filterset_form = None
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkRename, BulkDelete)
def get_required_permission(self): def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'view') return get_permission_for_model(self.queryset.model, 'view')
@@ -150,13 +151,13 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
# Determine the available actions # Determine the available actions
actions = self.get_permitted_actions(request.user) actions = self.get_permitted_actions(request.user)
has_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: if 'export' in request.GET:
# Export the current table view # Export the current table view
if request.GET['export'] == 'table': if request.GET['export'] == 'table':
table = self.get_table(self.queryset, request, has_bulk_actions) table = self.get_table(self.queryset, request, has_table_actions)
columns = [name for name, _ in table.selected_columns] columns = [name for name, _ in table.selected_columns]
return self.export_table(table, columns) return self.export_table(table, columns)
@@ -174,11 +175,11 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
# Fall back to default table/YAML export # Fall back to default table/YAML export
else: else:
table = self.get_table(self.queryset, request, has_bulk_actions) table = self.get_table(self.queryset, request, has_table_actions)
return self.export_table(table) return self.export_table(table)
# Render the objects 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 this is an HTMX request, return only the rendered table HTML
if htmx_partial(request): if htmx_partial(request):
@@ -729,7 +730,11 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView): class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
""" """
An extendable view for renaming objects in bulk. An extendable view for renaming objects in bulk.
Attributes:
field_name: The name of the object attribute for which the value is being updated (defaults to "name")
""" """
field_name = 'name'
template_name = 'generic/bulk_rename.html' template_name = 'generic/bulk_rename.html'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -759,12 +764,12 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
replace = form.cleaned_data['replace'] replace = form.cleaned_data['replace']
if form.cleaned_data['use_regex']: if form.cleaned_data['use_regex']:
try: try:
obj.new_name = re.sub(find, replace, obj.name or '') obj.new_name = re.sub(find, replace, getattr(obj, self.field_name, ''))
# Catch regex group reference errors # Catch regex group reference errors
except re.error: except re.error:
obj.new_name = obj.name obj.new_name = getattr(obj, self.field_name)
else: 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) renamed_pks.append(obj.pk)
return renamed_pks return renamed_pks
@@ -783,7 +788,7 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
if '_apply' in request.POST: if '_apply' in request.POST:
for obj in selected_objects: for obj in selected_objects:
obj.name = obj.new_name setattr(obj, self.field_name, obj.new_name)
obj.save() obj.save()
# Enforce constrained permissions # Enforce constrained permissions
@@ -813,6 +818,7 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
selected_objects = self.queryset.filter(pk__in=form.initial['pk']) selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
return render(request, self.template_name, { return render(request, self.template_name, {
'field_name': self.field_name,
'form': form, 'form': form,
'obj_type_plural': self.queryset.model._meta.verbose_name_plural, 'obj_type_plural': self.queryset.model._meta.verbose_name_plural,
'selected_objects': selected_objects, 'selected_objects': selected_objects,

View File

@@ -1,7 +1,7 @@
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from extras.models import TableConfig from extras.models import TableConfig
from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox import object_actions
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
__all__ = ( __all__ = (
@@ -9,6 +9,18 @@ __all__ = (
'TableMixin', 'TableMixin',
) )
# TODO: Remove in NetBox v4.5
LEGACY_ACTIONS = {
'add': object_actions.AddObject,
'edit': object_actions.EditObject,
'delete': object_actions.DeleteObject,
'export': object_actions.BulkExport,
'bulk_import': object_actions.BulkImport,
'bulk_edit': object_actions.BulkEdit,
'bulk_rename': object_actions.BulkRename,
'bulk_delete': object_actions.BulkDelete,
}
class ActionsMixin: class ActionsMixin:
""" """
@@ -19,7 +31,24 @@ class ActionsMixin:
Standard actions include: add, import, export, bulk_edit, and bulk_delete. Some views extend this default map Standard actions include: add, import, export, bulk_edit, and bulk_delete. Some views extend this default map
with custom actions, such as bulk_sync. with custom actions, such as bulk_sync.
""" """
actions = 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): def get_permitted_actions(self, user, model=None):
""" """
@@ -27,11 +56,15 @@ class ActionsMixin:
""" """
model = model or self.queryset.model model = model or self.queryset.model
# TODO: Remove in NetBox v4.5
# Handle legacy action sets
self._convert_legacy_actions()
# Resolve required permissions for each action # Resolve required permissions for each action
permitted_actions = [] permitted_actions = []
for action in self.actions: for action in self.actions:
required_permissions = [ required_permissions = [
get_permission_for_model(model, 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): if not required_permissions or user.has_perms(required_permissions):
permitted_actions.append(action) permitted_actions.append(action)

View File

@@ -14,6 +14,9 @@ from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from core.signals import clear_events from core.signals import clear_events
from netbox.object_actions import (
AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, CloneObject, DeleteObject, EditObject,
)
from utilities.error_handlers import handle_protectederror from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, PermissionsViolation from utilities.exceptions import AbortRequest, PermissionsViolation
from utilities.forms import ConfirmationForm, restrict_form_fields from utilities.forms import ConfirmationForm, restrict_form_fields
@@ -36,7 +39,7 @@ __all__ = (
) )
class ObjectView(BaseObjectView): class ObjectView(ActionsMixin, BaseObjectView):
""" """
Retrieve a single object for display. Retrieve a single object for display.
@@ -44,8 +47,10 @@ class ObjectView(BaseObjectView):
Attributes: Attributes:
tab: A ViewTab instance for the view tab: A ViewTab instance for the view
actions: An iterable of ObjectAction subclasses (see ActionsMixin)
""" """
tab = None tab = None
actions = (CloneObject, EditObject, DeleteObject)
def get_required_permission(self): def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'view') return get_permission_for_model(self.queryset.model, 'view')
@@ -72,9 +77,11 @@ class ObjectView(BaseObjectView):
request: The current request request: The current request
""" """
instance = self.get_object(**kwargs) instance = self.get_object(**kwargs)
actions = self.get_permitted_actions(request.user, model=instance)
return render(request, self.get_template_name(), { return render(request, self.get_template_name(), {
'object': instance, 'object': instance,
'actions': actions,
'tab': self.tab, 'tab': self.tab,
**self.get_extra_context(request, instance), **self.get_extra_context(request, instance),
}) })
@@ -90,13 +97,13 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
table: The django-tables2 Table class used to render the child objects list table: The django-tables2 Table class used to render the child objects list
filterset: A django-filter FilterSet that is applied to the queryset filterset: A django-filter FilterSet that is applied to the queryset
filterset_form: The form class used to render filter options filterset_form: The form class used to render filter options
actions: A mapping of supported actions to their required permissions. When adding custom actions, bulk actions: An iterable of ObjectAction subclasses (see ActionsMixin)
action names must be prefixed with `bulk_`. (See ActionsMixin.)
""" """
child_model = None child_model = None
table = None table = None
filterset = None filterset = None
filterset_form = None filterset_form = None
actions = (AddObject, BulkImport, BulkEdit, BulkExport, BulkDelete)
template_name = 'generic/object_children.html' template_name = 'generic/object_children.html'
def get_children(self, request, parent): def get_children(self, request, parent):
@@ -138,10 +145,10 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
# Determine the available actions # Determine the available actions
actions = self.get_permitted_actions(request.user, model=self.child_model) actions = self.get_permitted_actions(request.user, model=self.child_model)
has_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_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 this is an HTMX request, return only the rendered table HTML
if htmx_partial(request): if htmx_partial(request):

View 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>

View File

@@ -11,12 +11,6 @@
<li class="breadcrumb-item"><a href="{% url 'core:datafile_list' %}?source_id={{ object.source.pk }}">{{ object.source }}</a></li> <li class="breadcrumb-item"><a href="{% url 'core:datafile_list' %}?source_id={{ object.source.pk }}">{{ object.source }}</a></li>
{% endblock %} {% endblock %}
{% block control-buttons %}
{% if request.user|can_delete:object %}
{% delete_button object %}
{% endif %}
{% endblock control-buttons %}
{% block content %} {% block content %}
<div class="row mb-3"> <div class="row mb-3">
<div class="col"> <div class="col">

View File

@@ -1,33 +1,6 @@
{% extends 'generic/object.html' %} {% extends 'core/job/base.html' %}
{% load buttons %}
{% load helpers %}
{% load perms %}
{% load i18n %} {% load i18n %}
{% block breadcrumbs %}
{{ block.super }}
{% if object.object %}
<li class="breadcrumb-item">
<a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ object.object|meta:"verbose_name_plural"|bettertitle }}</a>
</li>
{% with parent_jobs_viewname=object.object|viewname:"jobs" %}
<li class="breadcrumb-item">
<a href="{% url parent_jobs_viewname pk=object.object.pk %}">{{ object.object }}</a>
</li>
{% endwith %}
{% else %}
<li class="breadcrumb-item">
<a href="{% url 'core:job_list' %}?name={{ object.name|urlencode }}">{{ object.name }}</a>
</li>
{% endif %}
{% endblock breadcrumbs %}
{% block control-buttons %}
{% if request.user|can_delete:object %}
{% delete_button object %}
{% endif %}
{% endblock control-buttons %}
{% block content %} {% block content %}
<div class="row mb-3"> <div class="row mb-3">
<div class="col col-12 col-md-6"> <div class="col col-12 col-md-6">

View File

@@ -0,0 +1,23 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load helpers %}
{% load perms %}
{% load i18n %}
{% block breadcrumbs %}
{{ block.super }}
{% if object.object %}
<li class="breadcrumb-item">
<a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ object.object|meta:"verbose_name_plural"|bettertitle }}</a>
</li>
{% with parent_jobs_viewname=object.object|viewname:"jobs" %}
<li class="breadcrumb-item">
<a href="{% url parent_jobs_viewname pk=object.object.pk %}">{{ object.object }}</a>
</li>
{% endwith %}
{% else %}
<li class="breadcrumb-item">
<a href="{% url 'core:job_list' %}?name={{ object.name|urlencode }}">{{ object.name }}</a>
</li>
{% endif %}
{% endblock breadcrumbs %}

View File

@@ -0,0 +1,12 @@
{% extends 'core/job/base.html' %}
{% load render_table from django_tables2 %}
{% block content %}
<div class="row mb-3">
<div class="col">
<div class="card">
{% render_table table %}
</div>
</div>
</div>
{% endblock %}

View 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>

View 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>

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -1,30 +1,5 @@
{% extends 'dcim/device/components_base.html' %} {% extends 'generic/object_children.html' %}
{% load helpers %}
{% load i18n %}
{% block table_controls %} {% block table_controls %}
{% include 'dcim/device/inc/interface_table_controls.html' with table_modal="DeviceInterfaceTable_config" %} {% include 'dcim/device/inc/interface_table_controls.html' with table_modal="DeviceInterfaceTable_config" %}
{% endblock table_controls %} {% endblock table_controls %}
{% block bulk_delete_controls %}
{{ block.super }}
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
<button type="submit" name="_disconnect"
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
class="btn btn-outline-danger">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button>
{% endif %}
{% endwith %}
{% endblock bulk_delete_controls %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_interface %}
<a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
class="btn btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Interfaces" %}
</a>
{% endif %}
{% endblock bulk_extra_controls %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -1,18 +1,8 @@
{% extends 'generic/object.html' %} {% extends 'generic/object.html' %}
{% load buttons %}
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load i18n %} {% load i18n %}
{% block buttons %}
{% if perms.dcim.change_virtualchassis %}
{% edit_button object %}
{% endif %}
{% if perms.dcim.delete_virtualchassis %}
{% delete_button object %}
{% endif %}
{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col col-12 col-md-4"> <div class="col col-12 col-md-4">

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -42,10 +42,12 @@ Context:
</thead> </thead>
<tbody> <tbody>
{% for obj in selected_objects %} {% for obj in selected_objects %}
<tr{% if obj.new_name and obj.name != obj.new_name %} class="success"{% endif %}> {% with obj_name=obj|getattr:field_name %}
<td>{{ obj.name }}</td> <tr{% if obj.new_name and obj_name != obj.new_name %} class="success"{% endif %}>
<td>{{ obj.new_name }}</td> <td>{{ obj_name }}</td>
</tr> <td>{{ obj.new_name }}</td>
</tr>
{% endwith %}
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@@ -80,15 +80,7 @@ Context:
{% if perms.extras.add_subscription and object.subscriptions %} {% if perms.extras.add_subscription and object.subscriptions %}
{% subscribe_button object %} {% subscribe_button object %}
{% endif %} {% endif %}
{% if request.user|can_add:object %} {% action_buttons actions 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 %}
{% endblock control-buttons %} {% endblock control-buttons %}
</div> </div>

View File

@@ -1,4 +1,5 @@
{% extends base_template %} {% extends base_template %}
{% load buttons %}
{% load helpers %} {% load helpers %}
{% load i18n %} {% load i18n %}
@@ -7,8 +8,6 @@ Blocks:
- content: Primary page content - content: Primary page content
- table_controls: Control elements for the child objects table - table_controls: Control elements for the child objects table
- bulk_controls: Bulk action buttons which appear beneath 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 - bulk_extra_controls: Other bulk action buttons
- modals: Any pre-loaded modals - modals: Any pre-loaded modals
@@ -36,36 +35,8 @@ Context:
</div> </div>
<div class="d-print-none mt-2"> <div class="d-print-none mt-2">
{% block bulk_controls %} {% block bulk_controls %}
<div class="btn-group" role="group"> {% action_buttons actions model multi=True %}
{# Bulk edit buttons #} {% block bulk_extra_controls %}{% 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 %}?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 %}
{% endblock bulk_controls %} {% endblock bulk_controls %}
</div> </div>
</form> </form>

View File

@@ -31,15 +31,7 @@ Context:
<div class="btn-list"> <div class="btn-list">
{% plugin_list_buttons model %} {% plugin_list_buttons model %}
{% block extra_controls %}{% endblock %} {% block extra_controls %}{% endblock %}
{% if 'add' in actions %} {% action_buttons actions model %}
{% add_button model %}
{% endif %}
{% if 'bulk_import' in actions %}
{% import_button model %}
{% endif %}
{% if 'export' in actions %}
{% export_button model %}
{% endif %}
</div> </div>
{% endblock controls %} {% endblock controls %}
@@ -91,12 +83,7 @@ Context:
</label> </label>
</div> </div>
<div class="bulk-action-buttons"> <div class="bulk-action-buttons">
{% if 'bulk_edit' in actions %} {% action_buttons actions model multi=True %}
{% bulk_edit_button model query_params=request.GET %}
{% endif %}
{% if 'bulk_delete' in actions %}
{% bulk_delete_button model query_params=request.GET %}
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -124,12 +111,7 @@ Context:
<div class="btn-list d-print-none"> <div class="btn-list d-print-none">
{% block bulk_buttons %} {% block bulk_buttons %}
<div class="bulk-action-buttons"> <div class="bulk-action-buttons">
{% if 'bulk_edit' in actions %} {% action_buttons actions model multi=True %}
{% bulk_edit_button model query_params=request.GET %}
{% endif %}
{% if 'bulk_delete' in actions %}
{% bulk_delete_button model query_params=request.GET %}
{% endif %}
</div> </div>
{% endblock %} {% endblock %}
</div> </div>

View File

@@ -27,12 +27,7 @@
{# Update the bulk action buttons with new query parameters #} {# Update the bulk action buttons with new query parameters #}
{% if actions %} {% if actions %}
<div class="bulk-action-buttons" hx-swap-oob="outerHTML:.bulk-action-buttons"> <div class="bulk-action-buttons" hx-swap-oob="outerHTML:.bulk-action-buttons">
{% if 'bulk_edit' in actions %} {% action_buttons actions model multi=True %}
{% bulk_edit_button model query_params=request.GET %}
{% endif %}
{% if 'bulk_delete' in actions %}
{% bulk_delete_button model query_params=request.GET %}
{% endif %}
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -1,6 +1,7 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from netbox.object_actions import BulkDelete, BulkEdit, BulkExport, BulkImport
from netbox.views import generic from netbox.views import generic
from utilities.query import count_related from utilities.query import count_related
from utilities.views import GetRelatedModelsMixin, register_model_view from utilities.views import GetRelatedModelsMixin, register_model_view
@@ -70,6 +71,11 @@ class TenantGroupBulkEditView(generic.BulkEditView):
form = forms.TenantGroupBulkEditForm form = forms.TenantGroupBulkEditForm
@register_model_view(TenantGroup, 'bulk_rename', path='rename', detail=False)
class TenantGroupBulkRenameView(generic.BulkRenameView):
queryset = TenantGroup.objects.all()
@register_model_view(TenantGroup, 'bulk_delete', path='delete', detail=False) @register_model_view(TenantGroup, 'bulk_delete', path='delete', detail=False)
class TenantGroupBulkDeleteView(generic.BulkDeleteView): class TenantGroupBulkDeleteView(generic.BulkDeleteView):
queryset = TenantGroup.objects.add_related_count( queryset = TenantGroup.objects.add_related_count(
@@ -131,6 +137,11 @@ class TenantBulkEditView(generic.BulkEditView):
form = forms.TenantBulkEditForm form = forms.TenantBulkEditForm
@register_model_view(Tenant, 'bulk_rename', path='rename', detail=False)
class TenantBulkRenameView(generic.BulkRenameView):
queryset = Tenant.objects.all()
@register_model_view(Tenant, 'bulk_delete', path='delete', detail=False) @register_model_view(Tenant, 'bulk_delete', path='delete', detail=False)
class TenantBulkDeleteView(generic.BulkDeleteView): class TenantBulkDeleteView(generic.BulkDeleteView):
queryset = Tenant.objects.all() queryset = Tenant.objects.all()
@@ -206,6 +217,11 @@ class ContactGroupBulkEditView(generic.BulkEditView):
form = forms.ContactGroupBulkEditForm form = forms.ContactGroupBulkEditForm
@register_model_view(ContactGroup, 'bulk_rename', path='rename', detail=False)
class ContactGroupBulkRenameView(generic.BulkRenameView):
queryset = ContactGroup.objects.all()
@register_model_view(ContactGroup, 'bulk_delete', path='delete', detail=False) @register_model_view(ContactGroup, 'bulk_delete', path='delete', detail=False)
class ContactGroupBulkDeleteView(generic.BulkDeleteView): class ContactGroupBulkDeleteView(generic.BulkDeleteView):
queryset = ContactGroup.objects.add_related_count( queryset = ContactGroup.objects.add_related_count(
@@ -267,6 +283,11 @@ class ContactRoleBulkEditView(generic.BulkEditView):
form = forms.ContactRoleBulkEditForm form = forms.ContactRoleBulkEditForm
@register_model_view(ContactRole, 'bulk_rename', path='rename', detail=False)
class ContactRoleBulkRenameView(generic.BulkRenameView):
queryset = ContactRole.objects.all()
@register_model_view(ContactRole, 'bulk_delete', path='delete', detail=False) @register_model_view(ContactRole, 'bulk_delete', path='delete', detail=False)
class ContactRoleBulkDeleteView(generic.BulkDeleteView): class ContactRoleBulkDeleteView(generic.BulkDeleteView):
queryset = ContactRole.objects.all() queryset = ContactRole.objects.all()
@@ -330,6 +351,11 @@ class ContactBulkEditView(generic.BulkEditView):
obj.groups.remove(*form.cleaned_data['remove_groups']) obj.groups.remove(*form.cleaned_data['remove_groups'])
@register_model_view(Contact, 'bulk_rename', path='rename', detail=False)
class ContactBulkRenameView(generic.BulkRenameView):
queryset = Contact.objects.all()
@register_model_view(Contact, 'bulk_delete', path='delete', detail=False) @register_model_view(Contact, 'bulk_delete', path='delete', detail=False)
class ContactBulkDeleteView(generic.BulkDeleteView): class ContactBulkDeleteView(generic.BulkDeleteView):
queryset = Contact.objects.annotate( queryset = Contact.objects.annotate(
@@ -349,12 +375,7 @@ class ContactAssignmentListView(generic.ObjectListView):
filterset = filtersets.ContactAssignmentFilterSet filterset = filtersets.ContactAssignmentFilterSet
filterset_form = forms.ContactAssignmentFilterForm filterset_form = forms.ContactAssignmentFilterForm
table = tables.ContactAssignmentTable table = tables.ContactAssignmentTable
actions = { actions = (BulkExport, BulkImport, BulkEdit, BulkDelete)
'export': {'view'},
'bulk_import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
}
@register_model_view(ContactAssignment, 'add', detail=False) @register_model_view(ContactAssignment, 'add', detail=False)

View File

@@ -2,6 +2,7 @@ from django.db.models import Count
from core.models import ObjectChange from core.models import ObjectChange
from core.tables import ObjectChangeTable from core.tables import ObjectChangeTable
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename
from netbox.views import generic from netbox.views import generic
from utilities.views import register_model_view from utilities.views import register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
@@ -18,6 +19,7 @@ class TokenListView(generic.ObjectListView):
filterset = filtersets.TokenFilterSet filterset = filtersets.TokenFilterSet
filterset_form = forms.TokenFilterForm filterset_form = forms.TokenFilterForm
table = tables.TokenTable table = tables.TokenTable
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
@register_model_view(Token) @register_model_view(Token)
@@ -111,6 +113,12 @@ class UserBulkEditView(generic.BulkEditView):
form = forms.UserBulkEditForm form = forms.UserBulkEditForm
@register_model_view(User, 'bulk_rename', path='rename', detail=False)
class UserBulkRenameView(generic.BulkRenameView):
queryset = User.objects.all()
field_name = 'username'
@register_model_view(User, 'bulk_delete', path='delete', detail=False) @register_model_view(User, 'bulk_delete', path='delete', detail=False)
class UserBulkDeleteView(generic.BulkDeleteView): class UserBulkDeleteView(generic.BulkDeleteView):
queryset = User.objects.all() queryset = User.objects.all()
@@ -162,6 +170,11 @@ class GroupBulkEditView(generic.BulkEditView):
form = forms.GroupBulkEditForm form = forms.GroupBulkEditForm
@register_model_view(Group, 'bulk_rename', path='rename', detail=False)
class GroupBulkRenameView(generic.BulkRenameView):
queryset = Group.objects.all()
@register_model_view(Group, 'bulk_delete', path='delete', detail=False) @register_model_view(Group, 'bulk_delete', path='delete', detail=False)
class GroupBulkDeleteView(generic.BulkDeleteView): class GroupBulkDeleteView(generic.BulkDeleteView):
queryset = Group.objects.annotate(users_count=Count('user')).order_by('name') queryset = Group.objects.annotate(users_count=Count('user')).order_by('name')
@@ -179,6 +192,7 @@ class ObjectPermissionListView(generic.ObjectListView):
filterset = filtersets.ObjectPermissionFilterSet filterset = filtersets.ObjectPermissionFilterSet
filterset_form = forms.ObjectPermissionFilterForm filterset_form = forms.ObjectPermissionFilterForm
table = tables.ObjectPermissionTable table = tables.ObjectPermissionTable
actions = (AddObject, BulkExport, BulkEdit, BulkRename, BulkDelete)
@register_model_view(ObjectPermission) @register_model_view(ObjectPermission)
@@ -207,6 +221,11 @@ class ObjectPermissionBulkEditView(generic.BulkEditView):
form = forms.ObjectPermissionBulkEditForm form = forms.ObjectPermissionBulkEditForm
@register_model_view(ObjectPermission, 'bulk_rename', path='rename', detail=False)
class ObjectPermissionBulkRenameView(generic.BulkRenameView):
queryset = ObjectPermission.objects.all()
@register_model_view(ObjectPermission, 'bulk_delete', path='delete', detail=False) @register_model_view(ObjectPermission, 'bulk_delete', path='delete', detail=False)
class ObjectPermissionBulkDeleteView(generic.BulkDeleteView): class ObjectPermissionBulkDeleteView(generic.BulkDeleteView):
queryset = ObjectPermission.objects.all() queryset = ObjectPermission.objects.all()

View File

@@ -1,10 +1,14 @@
import decimal import decimal
import json
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from utilities.datetime import datetime_from_timestamp
__all__ = ( __all__ = (
'ConfigJSONEncoder', 'ConfigJSONEncoder',
'CustomFieldJSONEncoder', 'CustomFieldJSONEncoder',
'JobLogDecoder',
) )
@@ -29,3 +33,21 @@ class ConfigJSONEncoder(DjangoJSONEncoder):
return type(o).__name__ return type(o).__name__
return super().default(o) return super().default(o)
class JobLogDecoder(json.JSONDecoder):
"""
Deserialize JobLogEntry timestamps.
"""
def __init__(self, *args, **kwargs):
kwargs['object_hook'] = self._deserialize_entry
super().__init__(*args, **kwargs)
def _deserialize_entry(self, obj: dict) -> dict:
if obj.get('timestamp'):
# Deserialize a timestamp string to a native datetime object
try:
obj['timestamp'] = datetime_from_timestamp(obj['timestamp'])
except ValueError:
pass
return obj

View File

@@ -1,6 +1,3 @@
{% if url %} <a href="{{ url }}" class="btn btn-primary" role="button">
{% load i18n %} <i class="mdi mdi-plus-thick" aria-hidden="true"></i> {{ label }}
<a href="{{ url }}" type="button" class="btn btn-primary"> </a>
<i class="mdi mdi-plus-thick"></i> {% trans "Add" %}
</a>
{% endif %}

View File

@@ -1,6 +1,3 @@
{% load i18n %} <button type="submit" name="_delete" {% formaction %}="{{ url }}" class="btn btn-red">
{% if url %} <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {{ label }}
<button type="submit" name="_delete" {% formaction %}="{{ url }}" class="btn btn-red"> </button>
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete Selected" %}
</button>
{% endif %}

View File

@@ -1,6 +1,3 @@
{% load i18n %} <button type="submit" name="_edit" {% formaction %}="{{ url }}" class="btn btn-yellow">
{% if url %} <i class="mdi mdi-pencil" aria-hidden="true"></i> {{ label }}
<button type="submit" name="_edit" {% formaction %}="{{ url }}" class="btn btn-yellow"> </button>
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit Selected" %}
</button>
{% endif %}

View File

@@ -0,0 +1,5 @@
{% if url %}
<button type="submit" name="_rename" {% formaction %}="{{ url }}" class="btn btn-yellow">
<i class="mdi mdi-pencil" aria-hidden="true"></i> {{ label }}
</button>
{% endif %}

View File

@@ -1,12 +1,12 @@
{% load i18n %}
<a href="#" <a href="#"
hx-get="{{ url }}" hx-get="{{ url }}"
hx-target="#htmx-modal-content" hx-target="#htmx-modal-content"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-select="form" hx-select="form"
class="btn btn-red" class="btn btn-red"
role="button"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#htmx-modal" data-bs-target="#htmx-modal"
> >
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %} <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {{ label }}
</a> </a>

View File

@@ -1,4 +1,3 @@
{% load i18n %}
<a href="{{ url }}" class="btn btn-yellow" role="button"> <a href="{{ url }}" class="btn btn-yellow" role="button">
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %} <i class="mdi mdi-pencil" aria-hidden="true"></i> {{ label }}
</a> </a>

View File

@@ -1,7 +1,7 @@
{% load i18n %} {% load i18n %}
<div class="dropdown"> <div class="dropdown">
<button type="button" class="btn btn-purple dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" class="btn btn-purple dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="mdi mdi-download"></i> {% trans "Export" %} <i class="mdi mdi-download" aria-hidden="true"></i> {{ label }}
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li><a id="export_current_view" class="dropdown-item" href="?{% if url_params %}{{ url_params }}&{% endif %}export=table">{% trans "Current View" %}</a></li> <li><a id="export_current_view" class="dropdown-item" href="?{% if url_params %}{{ url_params }}&{% endif %}export=table">{% trans "Current View" %}</a></li>

View File

@@ -1,6 +1,3 @@
{% load i18n %} <a href="{{ url }}" class="btn btn-cyan" role="button">
{% if url %} <i class="mdi mdi-upload" aria-hidden="true"></i> {{ label }}
<a href="{{ url }}" type="button" class="btn btn-cyan"> </a>
<i class="mdi mdi-upload"></i> {% trans "Import" %}
</a>
{% endif %}

View File

@@ -1,7 +1,6 @@
{% load i18n %}
<form action="{{ url }}" method="post"> <form action="{{ url }}" method="post">
{% csrf_token %} {% csrf_token %}
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync" %} <i class="mdi mdi-sync" aria-hidden="true"></i> {{ label }}
</button> </button>
</form> </form>

View File

@@ -22,6 +22,7 @@ __all__ = (
'content_type', 'content_type',
'content_type_id', 'content_type_id',
'fgcolor', 'fgcolor',
'getattr_',
'isodate', 'isodate',
'isodatetime', 'isodatetime',
'isotime', 'isotime',
@@ -88,6 +89,14 @@ def fgcolor(value, dark='000000', light='ffffff'):
return f'#{foreground_color(value, dark, light)}' return f'#{foreground_color(value, dark, light)}'
@register.filter('getattr')
def getattr_(instance, name):
"""
Call getattr() on the object for the specified attribute.
"""
return getattr(instance, name, None)
@register.filter() @register.filter()
def meta(model, attr): def meta(model, attr):
""" """

View File

@@ -1,6 +1,9 @@
from django import template from django import template
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.template import loader
from django.urls import NoReverseMatch, reverse from django.urls import NoReverseMatch, reverse
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from core.models import ObjectType from core.models import ObjectType
from extras.models import Bookmark, ExportTemplate, Subscription from extras.models import Bookmark, ExportTemplate, Subscription
@@ -9,6 +12,7 @@ from utilities.querydict import prepare_cloned_fields
from utilities.views import get_viewname from utilities.views import get_viewname
__all__ = ( __all__ = (
'action_buttons',
'add_button', 'add_button',
'bookmark_button', 'bookmark_button',
'bulk_delete_button', 'bulk_delete_button',
@@ -25,9 +29,14 @@ __all__ = (
register = template.Library() register = template.Library()
# @register.simple_tag(takes_context=True)
# Instance buttons def action_buttons(context, actions, obj, multi=False):
# buttons = [
loader.render_to_string(action.template_name, action.get_context(context, obj))
for action in actions if action.multi == multi
]
return mark_safe(''.join(buttons))
@register.inclusion_tag('buttons/bookmark.html', takes_context=True) @register.inclusion_tag('buttons/bookmark.html', takes_context=True)
def bookmark_button(context, instance): def bookmark_button(context, instance):
@@ -60,42 +69,6 @@ def bookmark_button(context, instance):
} }
@register.inclusion_tag('buttons/clone.html')
def clone_button(instance):
url = reverse(get_viewname(instance, 'add'))
# Populate cloned field values
param_string = prepare_cloned_fields(instance).urlencode()
if param_string:
url = f'{url}?{param_string}'
else:
url = None
return {
'url': url,
}
@register.inclusion_tag('buttons/edit.html')
def edit_button(instance):
viewname = get_viewname(instance, 'edit')
url = reverse(viewname, kwargs={'pk': instance.pk})
return {
'url': url,
}
@register.inclusion_tag('buttons/delete.html')
def delete_button(instance):
viewname = get_viewname(instance, 'delete')
url = reverse(viewname, kwargs={'pk': instance.pk})
return {
'url': url,
}
@register.inclusion_tag('buttons/subscribe.html', takes_context=True) @register.inclusion_tag('buttons/subscribe.html', takes_context=True)
def subscribe_button(context, instance): def subscribe_button(context, instance):
# Skip for objects which don't support notifications # Skip for objects which don't support notifications
@@ -131,20 +104,70 @@ def subscribe_button(context, instance):
} }
#
# Legacy object buttons
#
# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/clone.html')
def clone_button(instance):
# Resolve URL path
viewname = get_viewname(instance, 'add')
try:
url = reverse(viewname)
except NoReverseMatch:
return {
'url': None,
}
# Populate cloned field values and return full URL
param_string = prepare_cloned_fields(instance).urlencode()
return {
'url': f'{url}?{param_string}' if param_string else None,
}
# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/edit.html')
def edit_button(instance):
viewname = get_viewname(instance, 'edit')
url = reverse(viewname, kwargs={'pk': instance.pk})
return {
'url': url,
'label': _('Edit'),
}
# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/delete.html')
def delete_button(instance):
viewname = get_viewname(instance, 'delete')
url = reverse(viewname, kwargs={'pk': instance.pk})
return {
'url': url,
'label': _('Delete'),
}
# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/sync.html') @register.inclusion_tag('buttons/sync.html')
def sync_button(instance): def sync_button(instance):
viewname = get_viewname(instance, 'sync') viewname = get_viewname(instance, 'sync')
url = reverse(viewname, kwargs={'pk': instance.pk}) url = reverse(viewname, kwargs={'pk': instance.pk})
return { return {
'label': _('Sync'),
'url': url, 'url': url,
} }
# #
# List buttons # Legacy list buttons
# #
# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/add.html') @register.inclusion_tag('buttons/add.html')
def add_button(model, action='add'): def add_button(model, action='add'):
try: try:
@@ -154,9 +177,11 @@ def add_button(model, action='add'):
return { return {
'url': url, 'url': url,
'label': _('Add'),
} }
# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/import.html') @register.inclusion_tag('buttons/import.html')
def import_button(model, action='bulk_import'): def import_button(model, action='bulk_import'):
try: try:
@@ -166,9 +191,11 @@ def import_button(model, action='bulk_import'):
return { return {
'url': url, 'url': url,
'label': _('Import'),
} }
# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/export.html', takes_context=True) @register.inclusion_tag('buttons/export.html', takes_context=True)
def export_button(context, model): def export_button(context, model):
object_type = ObjectType.objects.get_for_model(model) object_type = ObjectType.objects.get_for_model(model)
@@ -181,6 +208,7 @@ def export_button(context, model):
export_templates = ExportTemplate.objects.restrict(user, 'view').filter(object_types=object_type) export_templates = ExportTemplate.objects.restrict(user, 'view').filter(object_types=object_type)
return { return {
'label': _('Export'),
'perms': context['perms'], 'perms': context['perms'],
'object_type': object_type, 'object_type': object_type,
'url_params': context['request'].GET.urlencode() if context['request'].GET else '', 'url_params': context['request'].GET.urlencode() if context['request'].GET else '',
@@ -189,6 +217,7 @@ def export_button(context, model):
} }
# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/bulk_edit.html', takes_context=True) @register.inclusion_tag('buttons/bulk_edit.html', takes_context=True)
def bulk_edit_button(context, model, action='bulk_edit', query_params=None): def bulk_edit_button(context, model, action='bulk_edit', query_params=None):
try: try:
@@ -199,11 +228,13 @@ def bulk_edit_button(context, model, action='bulk_edit', query_params=None):
url = None url = None
return { return {
'htmx_navigation': context.get('htmx_navigation'), 'label': _('Edit Selected'),
'url': url, 'url': url,
'htmx_navigation': context.get('htmx_navigation'),
} }
# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/bulk_delete.html', takes_context=True) @register.inclusion_tag('buttons/bulk_delete.html', takes_context=True)
def bulk_delete_button(context, model, action='bulk_delete', query_params=None): def bulk_delete_button(context, model, action='bulk_delete', query_params=None):
try: try:
@@ -214,6 +245,7 @@ def bulk_delete_button(context, model, action='bulk_delete', query_params=None):
url = None url = None
return { return {
'htmx_navigation': context.get('htmx_navigation'), 'label': _('Delete Selected'),
'url': url, 'url': url,
'htmx_navigation': context.get('htmx_navigation'),
} }

View File

@@ -0,0 +1,26 @@
from django.utils.translation import gettext as _
from netbox.object_actions import ObjectAction
__all__ = (
'BulkAddComponents',
)
class BulkAddComponents(ObjectAction):
"""
Add components to the selected virtual machines.
"""
label = _('Add Components')
multi = True
permissions_required = {'change'}
template_name = 'virtualization/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,
}

View File

@@ -13,13 +13,16 @@ from dcim.tables import DeviceTable
from extras.views import ObjectConfigContextView, ObjectRenderConfigView from extras.views import ObjectConfigContextView, ObjectRenderConfigView
from ipam.models import IPAddress, VLANGroup from ipam.models import IPAddress, VLANGroup
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.object_actions import (
AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename, DeleteObject, EditObject,
)
from netbox.views import generic from netbox.views import generic
from utilities.query import count_related from utilities.query import count_related
from utilities.query_functions import CollateAsChar from utilities.query_functions import CollateAsChar
from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *
from .object_actions import BulkAddComponents
# #
@@ -74,6 +77,11 @@ class ClusterTypeBulkEditView(generic.BulkEditView):
form = forms.ClusterTypeBulkEditForm form = forms.ClusterTypeBulkEditForm
@register_model_view(ClusterType, 'bulk_rename', path='rename', detail=False)
class ClusterTypeBulkRenameView(generic.BulkRenameView):
queryset = ClusterType.objects.all()
@register_model_view(ClusterType, 'bulk_delete', path='delete', detail=False) @register_model_view(ClusterType, 'bulk_delete', path='delete', detail=False)
class ClusterTypeBulkDeleteView(generic.BulkDeleteView): class ClusterTypeBulkDeleteView(generic.BulkDeleteView):
queryset = ClusterType.objects.annotate( queryset = ClusterType.objects.annotate(
@@ -147,6 +155,11 @@ class ClusterGroupBulkEditView(generic.BulkEditView):
form = forms.ClusterGroupBulkEditForm form = forms.ClusterGroupBulkEditForm
@register_model_view(ClusterGroup, 'bulk_rename', path='rename', detail=False)
class ClusterGroupBulkRenameView(generic.BulkRenameView):
queryset = ClusterGroup.objects.all()
@register_model_view(ClusterGroup, 'bulk_delete', path='delete', detail=False) @register_model_view(ClusterGroup, 'bulk_delete', path='delete', detail=False)
class ClusterGroupBulkDeleteView(generic.BulkDeleteView): class ClusterGroupBulkDeleteView(generic.BulkDeleteView):
queryset = ClusterGroup.objects.annotate( queryset = ClusterGroup.objects.annotate(
@@ -204,6 +217,7 @@ class ClusterVirtualMachinesView(generic.ObjectChildrenView):
table = tables.VirtualMachineTable table = tables.VirtualMachineTable
filterset = filtersets.VirtualMachineFilterSet filterset = filtersets.VirtualMachineFilterSet
filterset_form = forms.VirtualMachineFilterForm filterset_form = forms.VirtualMachineFilterForm
actions = (EditObject, DeleteObject, BulkEdit)
tab = ViewTab( tab = ViewTab(
label=_('Virtual Machines'), label=_('Virtual Machines'),
badge=lambda obj: obj.virtual_machines.count(), badge=lambda obj: obj.virtual_machines.count(),
@@ -222,14 +236,7 @@ class ClusterDevicesView(generic.ObjectChildrenView):
table = DeviceTable table = DeviceTable
filterset = DeviceFilterSet filterset = DeviceFilterSet
filterset_form = DeviceFilterForm filterset_form = DeviceFilterForm
template_name = 'virtualization/cluster/devices.html' actions = (EditObject, DeleteObject, BulkEdit)
actions = {
'add': {'add'},
'export': {'view'},
'bulk_import': {'add'},
'bulk_edit': {'change'},
'bulk_remove_devices': {'change'},
}
tab = ViewTab( tab = ViewTab(
label=_('Devices'), label=_('Devices'),
badge=lambda obj: obj.devices.count(), badge=lambda obj: obj.devices.count(),
@@ -267,6 +274,11 @@ class ClusterBulkEditView(generic.BulkEditView):
form = forms.ClusterBulkEditForm form = forms.ClusterBulkEditForm
@register_model_view(Cluster, 'bulk_rename', path='rename', detail=False)
class ClusterBulkRenameView(generic.BulkRenameView):
queryset = Cluster.objects.all()
@register_model_view(Cluster, 'bulk_delete', path='delete', detail=False) @register_model_view(Cluster, 'bulk_delete', path='delete', detail=False)
class ClusterBulkDeleteView(generic.BulkDeleteView): class ClusterBulkDeleteView(generic.BulkDeleteView):
queryset = Cluster.objects.all() queryset = Cluster.objects.all()
@@ -317,50 +329,6 @@ class ClusterAddDevicesView(generic.ObjectEditView):
}) })
@register_model_view(Cluster, 'remove_devices', path='devices/remove')
class ClusterRemoveDevicesView(generic.ObjectEditView):
queryset = Cluster.objects.all()
form = forms.ClusterRemoveDevicesForm
template_name = 'generic/bulk_remove.html'
def post(self, request, pk):
cluster = get_object_or_404(self.queryset, pk=pk)
if '_confirm' in request.POST:
form = self.form(request.POST)
if form.is_valid():
device_pks = form.cleaned_data['pk']
with transaction.atomic(using=router.db_for_write(Device)):
# Remove the selected Devices from the Cluster
for device in Device.objects.filter(pk__in=device_pks):
device.cluster = None
device.save()
messages.success(request, _("Removed {count} devices from cluster {cluster}").format(
count=len(device_pks),
cluster=cluster
))
return redirect(cluster.get_absolute_url())
else:
form = self.form(initial={'pk': request.POST.getlist('pk')})
selected_objects = Device.objects.filter(pk__in=form.initial['pk'])
device_table = DeviceTable(list(selected_objects), orderable=False)
device_table.configure(request)
return render(request, self.template_name, {
'form': form,
'parent_obj': cluster,
'table': device_table,
'obj_type_plural': 'devices',
'return_url': cluster.get_absolute_url(),
})
# #
# Virtual machines # Virtual machines
# #
@@ -371,7 +339,7 @@ class VirtualMachineListView(generic.ObjectListView):
filterset = filtersets.VirtualMachineFilterSet filterset = filtersets.VirtualMachineFilterSet
filterset_form = forms.VirtualMachineFilterForm filterset_form = forms.VirtualMachineFilterForm
table = tables.VirtualMachineTable table = tables.VirtualMachineTable
template_name = 'virtualization/virtualmachine_list.html' actions = (AddObject, BulkImport, BulkExport, BulkAddComponents, BulkEdit, BulkRename, BulkDelete)
@register_model_view(VirtualMachine) @register_model_view(VirtualMachine)
@@ -386,11 +354,7 @@ class VirtualMachineInterfacesView(generic.ObjectChildrenView):
table = tables.VirtualMachineVMInterfaceTable table = tables.VirtualMachineVMInterfaceTable
filterset = filtersets.VMInterfaceFilterSet filterset = filtersets.VMInterfaceFilterSet
filterset_form = forms.VMInterfaceFilterForm filterset_form = forms.VMInterfaceFilterForm
template_name = 'virtualization/virtualmachine/interfaces.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
tab = ViewTab( tab = ViewTab(
label=_('Interfaces'), label=_('Interfaces'),
badge=lambda obj: obj.interface_count, badge=lambda obj: obj.interface_count,
@@ -412,17 +376,13 @@ class VirtualMachineVirtualDisksView(generic.ObjectChildrenView):
table = tables.VirtualMachineVirtualDiskTable table = tables.VirtualMachineVirtualDiskTable
filterset = filtersets.VirtualDiskFilterSet filterset = filtersets.VirtualDiskFilterSet
filterset_form = forms.VirtualDiskFilterForm filterset_form = forms.VirtualDiskFilterForm
template_name = 'virtualization/virtualmachine/virtual_disks.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
tab = ViewTab( tab = ViewTab(
label=_('Virtual Disks'), label=_('Virtual Disks'),
badge=lambda obj: obj.virtual_disk_count, badge=lambda obj: obj.virtual_disk_count,
permission='virtualization.view_virtualdisk', permission='virtualization.view_virtualdisk',
weight=500 weight=500
) )
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
def get_children(self, request, parent): def get_children(self, request, parent):
return parent.virtualdisks.restrict(request.user, 'view').prefetch_related('tags') return parent.virtualdisks.restrict(request.user, 'view').prefetch_related('tags')
@@ -474,6 +434,11 @@ class VirtualMachineBulkEditView(generic.BulkEditView):
form = forms.VirtualMachineBulkEditForm form = forms.VirtualMachineBulkEditForm
@register_model_view(VirtualMachine, 'bulk_rename', path='rename', detail=False)
class VirtualMachineBulkRenameView(generic.BulkRenameView):
queryset = VirtualMachine.objects.all()
@register_model_view(VirtualMachine, 'bulk_delete', path='delete', detail=False) @register_model_view(VirtualMachine, 'bulk_delete', path='delete', detail=False)
class VirtualMachineBulkDeleteView(generic.BulkDeleteView): class VirtualMachineBulkDeleteView(generic.BulkDeleteView):
queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6') queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6')

View File

@@ -1,4 +1,5 @@
from ipam.tables import RouteTargetTable from ipam.tables import RouteTargetTable
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
from netbox.views import generic from netbox.views import generic
from utilities.query import count_related from utilities.query import count_related
from utilities.views import GetRelatedModelsMixin, register_model_view from utilities.views import GetRelatedModelsMixin, register_model_view
@@ -58,6 +59,11 @@ class TunnelGroupBulkEditView(generic.BulkEditView):
form = forms.TunnelGroupBulkEditForm form = forms.TunnelGroupBulkEditForm
@register_model_view(TunnelGroup, 'bulk_rename', path='rename', detail=False)
class TunnelGroupBulkRenameView(generic.BulkRenameView):
queryset = TunnelGroup.objects.all()
@register_model_view(TunnelGroup, 'bulk_delete', path='delete', detail=False) @register_model_view(TunnelGroup, 'bulk_delete', path='delete', detail=False)
class TunnelGroupBulkDeleteView(generic.BulkDeleteView): class TunnelGroupBulkDeleteView(generic.BulkDeleteView):
queryset = TunnelGroup.objects.annotate( queryset = TunnelGroup.objects.annotate(
@@ -122,6 +128,11 @@ class TunnelBulkEditView(generic.BulkEditView):
form = forms.TunnelBulkEditForm form = forms.TunnelBulkEditForm
@register_model_view(Tunnel, 'bulk_rename', path='rename', detail=False)
class TunnelBulkRenameView(generic.BulkRenameView):
queryset = Tunnel.objects.all()
@register_model_view(Tunnel, 'bulk_delete', path='delete', detail=False) @register_model_view(Tunnel, 'bulk_delete', path='delete', detail=False)
class TunnelBulkDeleteView(generic.BulkDeleteView): class TunnelBulkDeleteView(generic.BulkDeleteView):
queryset = Tunnel.objects.annotate( queryset = Tunnel.objects.annotate(
@@ -224,6 +235,11 @@ class IKEProposalBulkEditView(generic.BulkEditView):
form = forms.IKEProposalBulkEditForm form = forms.IKEProposalBulkEditForm
@register_model_view(IKEProposal, 'bulk_rename', path='rename', detail=False)
class IKEProposalBulkRenameView(generic.BulkRenameView):
queryset = IKEProposal.objects.all()
@register_model_view(IKEProposal, 'bulk_delete', path='delete', detail=False) @register_model_view(IKEProposal, 'bulk_delete', path='delete', detail=False)
class IKEProposalBulkDeleteView(generic.BulkDeleteView): class IKEProposalBulkDeleteView(generic.BulkDeleteView):
queryset = IKEProposal.objects.all() queryset = IKEProposal.objects.all()
@@ -274,6 +290,11 @@ class IKEPolicyBulkEditView(generic.BulkEditView):
form = forms.IKEPolicyBulkEditForm form = forms.IKEPolicyBulkEditForm
@register_model_view(IKEPolicy, 'bulk_rename', path='rename', detail=False)
class IKEPolicyBulkRenameView(generic.BulkRenameView):
queryset = IKEPolicy.objects.all()
@register_model_view(IKEPolicy, 'bulk_delete', path='delete', detail=False) @register_model_view(IKEPolicy, 'bulk_delete', path='delete', detail=False)
class IKEPolicyBulkDeleteView(generic.BulkDeleteView): class IKEPolicyBulkDeleteView(generic.BulkDeleteView):
queryset = IKEPolicy.objects.all() queryset = IKEPolicy.objects.all()
@@ -324,6 +345,11 @@ class IPSecProposalBulkEditView(generic.BulkEditView):
form = forms.IPSecProposalBulkEditForm form = forms.IPSecProposalBulkEditForm
@register_model_view(IPSecProposal, 'bulk_rename', path='rename', detail=False)
class IPSecProposalBulkRenameView(generic.BulkRenameView):
queryset = IPSecProposal.objects.all()
@register_model_view(IPSecProposal, 'bulk_delete', path='delete', detail=False) @register_model_view(IPSecProposal, 'bulk_delete', path='delete', detail=False)
class IPSecProposalBulkDeleteView(generic.BulkDeleteView): class IPSecProposalBulkDeleteView(generic.BulkDeleteView):
queryset = IPSecProposal.objects.all() queryset = IPSecProposal.objects.all()
@@ -374,6 +400,11 @@ class IPSecPolicyBulkEditView(generic.BulkEditView):
form = forms.IPSecPolicyBulkEditForm form = forms.IPSecPolicyBulkEditForm
@register_model_view(IPSecPolicy, 'bulk_rename', path='rename', detail=False)
class IPSecPolicyBulkRenameView(generic.BulkRenameView):
queryset = IPSecPolicy.objects.all()
@register_model_view(IPSecPolicy, 'bulk_delete', path='delete', detail=False) @register_model_view(IPSecPolicy, 'bulk_delete', path='delete', detail=False)
class IPSecPolicyBulkDeleteView(generic.BulkDeleteView): class IPSecPolicyBulkDeleteView(generic.BulkDeleteView):
queryset = IPSecPolicy.objects.all() queryset = IPSecPolicy.objects.all()
@@ -424,6 +455,11 @@ class IPSecProfileBulkEditView(generic.BulkEditView):
form = forms.IPSecProfileBulkEditForm form = forms.IPSecProfileBulkEditForm
@register_model_view(IPSecProfile, 'bulk_rename', path='rename', detail=False)
class IPSecProfileBulkRenameView(generic.BulkRenameView):
queryset = IPSecProfile.objects.all()
@register_model_view(IPSecProfile, 'bulk_delete', path='delete', detail=False) @register_model_view(IPSecProfile, 'bulk_delete', path='delete', detail=False)
class IPSecProfileBulkDeleteView(generic.BulkDeleteView): class IPSecProfileBulkDeleteView(generic.BulkDeleteView):
queryset = IPSecProfile.objects.all() queryset = IPSecProfile.objects.all()
@@ -491,6 +527,11 @@ class L2VPNBulkEditView(generic.BulkEditView):
form = forms.L2VPNBulkEditForm form = forms.L2VPNBulkEditForm
@register_model_view(L2VPN, 'bulk_rename', path='rename', detail=False)
class L2VPNBulkRenameView(generic.BulkRenameView):
queryset = L2VPN.objects.all()
@register_model_view(L2VPN, 'bulk_delete', path='delete', detail=False) @register_model_view(L2VPN, 'bulk_delete', path='delete', detail=False)
class L2VPNBulkDeleteView(generic.BulkDeleteView): class L2VPNBulkDeleteView(generic.BulkDeleteView):
queryset = L2VPN.objects.all() queryset = L2VPN.objects.all()
@@ -508,6 +549,7 @@ class L2VPNTerminationListView(generic.ObjectListView):
table = tables.L2VPNTerminationTable table = tables.L2VPNTerminationTable
filterset = filtersets.L2VPNTerminationFilterSet filterset = filtersets.L2VPNTerminationFilterSet
filterset_form = forms.L2VPNTerminationFilterForm filterset_form = forms.L2VPNTerminationFilterForm
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
@register_model_view(L2VPNTermination) @register_model_view(L2VPNTermination)

View File

@@ -68,6 +68,11 @@ class WirelessLANGroupBulkEditView(generic.BulkEditView):
form = forms.WirelessLANGroupBulkEditForm form = forms.WirelessLANGroupBulkEditForm
@register_model_view(WirelessLANGroup, 'bulk_rename', path='rename', detail=False)
class WirelessLANGroupBulkRenameView(generic.BulkRenameView):
queryset = WirelessLANGroup.objects.all()
@register_model_view(WirelessLANGroup, 'bulk_delete', path='delete', detail=False) @register_model_view(WirelessLANGroup, 'bulk_delete', path='delete', detail=False)
class WirelessLANGroupBulkDeleteView(generic.BulkDeleteView): class WirelessLANGroupBulkDeleteView(generic.BulkDeleteView):
queryset = WirelessLANGroup.objects.add_related_count( queryset = WirelessLANGroup.objects.add_related_count(
@@ -137,6 +142,12 @@ class WirelessLANBulkEditView(generic.BulkEditView):
form = forms.WirelessLANBulkEditForm form = forms.WirelessLANBulkEditForm
@register_model_view(WirelessLAN, 'bulk_rename', path='rename', detail=False)
class WirelessLANBulkRenameView(generic.BulkRenameView):
queryset = WirelessLAN.objects.all()
field_name = 'ssid'
@register_model_view(WirelessLAN, 'bulk_delete', path='delete', detail=False) @register_model_view(WirelessLAN, 'bulk_delete', path='delete', detail=False)
class WirelessLANBulkDeleteView(generic.BulkDeleteView): class WirelessLANBulkDeleteView(generic.BulkDeleteView):
queryset = WirelessLAN.objects.all() queryset = WirelessLAN.objects.all()
@@ -187,6 +198,12 @@ class WirelessLinkBulkEditView(generic.BulkEditView):
form = forms.WirelessLinkBulkEditForm form = forms.WirelessLinkBulkEditForm
@register_model_view(WirelessLink, 'bulk_rename', path='rename', detail=False)
class WirelessLinkBulkRenameView(generic.BulkRenameView):
queryset = WirelessLink.objects.all()
field_name = 'ssid'
@register_model_view(WirelessLink, 'bulk_delete', path='delete', detail=False) @register_model_view(WirelessLink, 'bulk_delete', path='delete', detail=False)
class WirelessLinkBulkDeleteView(generic.BulkDeleteView): class WirelessLinkBulkDeleteView(generic.BulkDeleteView):
queryset = WirelessLink.objects.all() queryset = WirelessLink.objects.all()