mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge pull request #8428 from netbox-community/8334-plugins-views
Closes #8334: Formally support use of generic views by plugins
This commit is contained in:
96
docs/plugins/development/generic-views.md
Normal file
96
docs/plugins/development/generic-views.md
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# Generic Views
|
||||||
|
|
||||||
|
NetBox provides several generic view classes to facilitate common operations, such as creating, viewing, modifying, and deleting objects. Plugins can subclass these views for their own use.
|
||||||
|
|
||||||
|
| View Class | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| `ObjectView` | View a single object |
|
||||||
|
| `ObjectEditView` | Create or edit a single object |
|
||||||
|
| `ObjectDeleteView` | Delete a single object |
|
||||||
|
| `ObjectListView` | View a list of objects |
|
||||||
|
| `BulkImportView` | Import a set of new objects |
|
||||||
|
| `BulkEditView` | Edit multiple objects |
|
||||||
|
| `BulkDeleteView` | Delete multiple objects |
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Please note that only the classes which appear in this documentation are currently supported. Although other classes may be present within the `views.generic` module, they are not yet supported for use by plugins.
|
||||||
|
|
||||||
|
### Example Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
# views.py
|
||||||
|
from netbox.views.generic import ObjectEditView
|
||||||
|
from .models import Thing
|
||||||
|
|
||||||
|
class ThingEditView(ObjectEditView):
|
||||||
|
queryset = Thing.objects.all()
|
||||||
|
template_name = 'myplugin/thing.html'
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Object Views
|
||||||
|
|
||||||
|
Below is the class definition for NetBox's BaseObjectView. The attributes and methods defined here are available on all generic views which handle a single object.
|
||||||
|
|
||||||
|
::: netbox.views.generic.base.BaseObjectView
|
||||||
|
rendering:
|
||||||
|
show_source: false
|
||||||
|
|
||||||
|
::: netbox.views.generic.ObjectView
|
||||||
|
selection:
|
||||||
|
members:
|
||||||
|
- get_object
|
||||||
|
- get_template_name
|
||||||
|
rendering:
|
||||||
|
show_source: false
|
||||||
|
|
||||||
|
::: netbox.views.generic.ObjectEditView
|
||||||
|
selection:
|
||||||
|
members:
|
||||||
|
- get_object
|
||||||
|
- alter_object
|
||||||
|
rendering:
|
||||||
|
show_source: false
|
||||||
|
|
||||||
|
::: netbox.views.generic.ObjectDeleteView
|
||||||
|
selection:
|
||||||
|
members:
|
||||||
|
- get_object
|
||||||
|
rendering:
|
||||||
|
show_source: false
|
||||||
|
|
||||||
|
## Multi-Object Views
|
||||||
|
|
||||||
|
Below is the class definition for NetBox's BaseMultiObjectView. The attributes and methods defined here are available on all generic views which deal with multiple objects.
|
||||||
|
|
||||||
|
::: netbox.views.generic.base.BaseMultiObjectView
|
||||||
|
rendering:
|
||||||
|
show_source: false
|
||||||
|
|
||||||
|
::: netbox.views.generic.ObjectListView
|
||||||
|
selection:
|
||||||
|
members:
|
||||||
|
- get_table
|
||||||
|
- export_table
|
||||||
|
- export_template
|
||||||
|
rendering:
|
||||||
|
show_source: false
|
||||||
|
|
||||||
|
::: netbox.views.generic.BulkImportView
|
||||||
|
selection:
|
||||||
|
members: false
|
||||||
|
rendering:
|
||||||
|
show_source: false
|
||||||
|
|
||||||
|
::: netbox.views.generic.BulkEditView
|
||||||
|
selection:
|
||||||
|
members: false
|
||||||
|
rendering:
|
||||||
|
show_source: false
|
||||||
|
|
||||||
|
::: netbox.views.generic.BulkDeleteView
|
||||||
|
selection:
|
||||||
|
members:
|
||||||
|
- get_form
|
||||||
|
rendering:
|
||||||
|
show_source: false
|
@ -28,6 +28,7 @@ plugins:
|
|||||||
- django.setup()
|
- django.setup()
|
||||||
rendering:
|
rendering:
|
||||||
heading_level: 3
|
heading_level: 3
|
||||||
|
members_order: source
|
||||||
show_root_heading: true
|
show_root_heading: true
|
||||||
show_root_full_path: false
|
show_root_full_path: false
|
||||||
show_root_toc_entry: false
|
show_root_toc_entry: false
|
||||||
@ -102,6 +103,7 @@ nav:
|
|||||||
- Developing Plugins:
|
- Developing Plugins:
|
||||||
- Introduction: 'plugins/development/index.md'
|
- Introduction: 'plugins/development/index.md'
|
||||||
- Model Features: 'plugins/development/model-features.md'
|
- Model Features: 'plugins/development/model-features.md'
|
||||||
|
- Generic Views: 'plugins/development/generic-views.md'
|
||||||
- Developing Plugins (Old): 'plugins/development.md'
|
- Developing Plugins (Old): 'plugins/development.md'
|
||||||
- Administration:
|
- Administration:
|
||||||
- Authentication: 'administration/authentication.md'
|
- Authentication: 'administration/authentication.md'
|
||||||
|
58
netbox/netbox/views/generic/base.py
Normal file
58
netbox/netbox/views/generic/base.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.views.generic import View
|
||||||
|
|
||||||
|
from utilities.views import ObjectPermissionRequiredMixin
|
||||||
|
|
||||||
|
|
||||||
|
class BaseObjectView(ObjectPermissionRequiredMixin, View):
|
||||||
|
"""
|
||||||
|
Base view class for reusable generic views.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
queryset: Django QuerySet from which the object(s) will be fetched
|
||||||
|
template_name: The name of the HTML template file to render
|
||||||
|
"""
|
||||||
|
queryset = None
|
||||||
|
template_name = None
|
||||||
|
|
||||||
|
def get_object(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Return the object being viewed or modified. The object is identified by an arbitrary set of keyword arguments
|
||||||
|
gleaned from the URL, which are passed to `get_object_or_404()`. (Typically, only a primary key is needed.)
|
||||||
|
|
||||||
|
If no matching object is found, return a 404 response.
|
||||||
|
"""
|
||||||
|
return get_object_or_404(self.queryset, **kwargs)
|
||||||
|
|
||||||
|
def get_extra_context(self, request, instance):
|
||||||
|
"""
|
||||||
|
Return any additional context data to include when rendering the template.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The current request
|
||||||
|
instance: The object being viewed
|
||||||
|
"""
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMultiObjectView(ObjectPermissionRequiredMixin, View):
|
||||||
|
"""
|
||||||
|
Base view class for reusable generic views.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
queryset: Django QuerySet from which the object(s) will be fetched
|
||||||
|
table: The django-tables2 Table class used to render the objects list
|
||||||
|
template_name: The name of the HTML template file to render
|
||||||
|
"""
|
||||||
|
queryset = None
|
||||||
|
table = None
|
||||||
|
template_name = None
|
||||||
|
|
||||||
|
def get_extra_context(self, request):
|
||||||
|
"""
|
||||||
|
Return any additional context data to include when rendering the template.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The current request
|
||||||
|
"""
|
||||||
|
return {}
|
@ -3,21 +3,27 @@ import re
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import FieldDoesNotExist, ValidationError
|
from django.core.exceptions import FieldDoesNotExist, ValidationError
|
||||||
from django.db import transaction, IntegrityError
|
from django.db import transaction, IntegrityError
|
||||||
from django.db.models import ManyToManyField, ProtectedError
|
from django.db.models import ManyToManyField, ProtectedError
|
||||||
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
|
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
|
||||||
from django.shortcuts import redirect, render
|
from django.http import HttpResponse
|
||||||
from django.views.generic import View
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
from django_tables2.export import TableExport
|
||||||
|
|
||||||
|
from extras.models import ExportTemplate
|
||||||
from extras.signals import clear_webhooks
|
from extras.signals import clear_webhooks
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
from utilities.exceptions import PermissionsViolation
|
from utilities.exceptions import PermissionsViolation
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
BootstrapMixin, BulkRenameForm, ConfirmationForm, CSVDataField, CSVFileField, restrict_form_fields,
|
BootstrapMixin, BulkRenameForm, ConfirmationForm, CSVDataField, CSVFileField, restrict_form_fields,
|
||||||
)
|
)
|
||||||
|
from utilities.htmx import is_htmx
|
||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin
|
from utilities.tables import configure_table
|
||||||
|
from utilities.views import GetReturnURLMixin
|
||||||
|
from .base import BaseMultiObjectView
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'BulkComponentCreateView',
|
'BulkComponentCreateView',
|
||||||
@ -26,24 +32,174 @@ __all__ = (
|
|||||||
'BulkEditView',
|
'BulkEditView',
|
||||||
'BulkImportView',
|
'BulkImportView',
|
||||||
'BulkRenameView',
|
'BulkRenameView',
|
||||||
|
'ObjectListView',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
class ObjectListView(BaseMultiObjectView):
|
||||||
|
"""
|
||||||
|
Display multiple objects, all of the same type, as a table.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
filterset: A django-filter FilterSet that is applied to the queryset
|
||||||
|
filterset_form: The form class used to render filter options
|
||||||
|
action_buttons: A list of buttons to include at the top of the page
|
||||||
|
"""
|
||||||
|
template_name = 'generic/object_list.html'
|
||||||
|
filterset = None
|
||||||
|
filterset_form = None
|
||||||
|
action_buttons = ('add', 'import', 'export')
|
||||||
|
|
||||||
|
def get_required_permission(self):
|
||||||
|
return get_permission_for_model(self.queryset.model, 'view')
|
||||||
|
|
||||||
|
def get_table(self, request, permissions):
|
||||||
|
"""
|
||||||
|
Return the django-tables2 Table instance to be used for rendering the objects list.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The current request
|
||||||
|
permissions: A dictionary mapping of the view, add, change, and delete permissions to booleans indicating
|
||||||
|
whether the user has each
|
||||||
|
"""
|
||||||
|
table = self.table(self.queryset, user=request.user)
|
||||||
|
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
|
||||||
|
table.columns.show('pk')
|
||||||
|
|
||||||
|
return table
|
||||||
|
|
||||||
|
#
|
||||||
|
# Export methods
|
||||||
|
#
|
||||||
|
|
||||||
|
def export_yaml(self):
|
||||||
|
"""
|
||||||
|
Export the queryset of objects as concatenated YAML documents.
|
||||||
|
"""
|
||||||
|
yaml_data = [obj.to_yaml() for obj in self.queryset]
|
||||||
|
|
||||||
|
return '---\n'.join(yaml_data)
|
||||||
|
|
||||||
|
def export_table(self, table, columns=None, filename=None):
|
||||||
|
"""
|
||||||
|
Export all table data in CSV format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table: The Table instance to export
|
||||||
|
columns: A list of specific columns to include. If None, all columns will be exported.
|
||||||
|
filename: The name of the file attachment sent to the client. If None, will be determined automatically
|
||||||
|
from the queryset model name.
|
||||||
|
"""
|
||||||
|
exclude_columns = {'pk', 'actions'}
|
||||||
|
if columns:
|
||||||
|
all_columns = [col_name for col_name, _ in table.selected_columns + table.available_columns]
|
||||||
|
exclude_columns.update({
|
||||||
|
col for col in all_columns if col not in columns
|
||||||
|
})
|
||||||
|
exporter = TableExport(
|
||||||
|
export_format=TableExport.CSV,
|
||||||
|
table=table,
|
||||||
|
exclude_columns=exclude_columns
|
||||||
|
)
|
||||||
|
return exporter.response(
|
||||||
|
filename=filename or f'netbox_{self.queryset.model._meta.verbose_name_plural}.csv'
|
||||||
|
)
|
||||||
|
|
||||||
|
def export_template(self, template, request):
|
||||||
|
"""
|
||||||
|
Render an ExportTemplate using the current queryset.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
template: ExportTemplate instance
|
||||||
|
request: The current request
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return template.render_to_response(self.queryset)
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(request, f"There was an error rendering the selected export template ({template.name}): {e}")
|
||||||
|
return redirect(request.path)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Request handlers
|
||||||
|
#
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""
|
||||||
|
GET request handler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The current request
|
||||||
|
"""
|
||||||
|
model = self.queryset.model
|
||||||
|
content_type = ContentType.objects.get_for_model(model)
|
||||||
|
|
||||||
|
if self.filterset:
|
||||||
|
self.queryset = self.filterset(request.GET, self.queryset).qs
|
||||||
|
|
||||||
|
# Compile a dictionary indicating which permissions are available to the current user for this model
|
||||||
|
permissions = {}
|
||||||
|
for action in ('add', 'change', 'delete', 'view'):
|
||||||
|
perm_name = get_permission_for_model(model, action)
|
||||||
|
permissions[action] = request.user.has_perm(perm_name)
|
||||||
|
|
||||||
|
if 'export' in request.GET:
|
||||||
|
|
||||||
|
# Export the current table view
|
||||||
|
if request.GET['export'] == 'table':
|
||||||
|
table = self.get_table(request, permissions)
|
||||||
|
columns = [name for name, _ in table.selected_columns]
|
||||||
|
return self.export_table(table, columns)
|
||||||
|
|
||||||
|
# Render an ExportTemplate
|
||||||
|
elif request.GET['export']:
|
||||||
|
template = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export'])
|
||||||
|
return self.export_template(template, request)
|
||||||
|
|
||||||
|
# Check for YAML export support on the model
|
||||||
|
elif hasattr(model, 'to_yaml'):
|
||||||
|
response = HttpResponse(self.export_yaml(), content_type='text/yaml')
|
||||||
|
filename = 'netbox_{}.yaml'.format(self.queryset.model._meta.verbose_name_plural)
|
||||||
|
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||||
|
return response
|
||||||
|
|
||||||
|
# Fall back to default table/YAML export
|
||||||
|
else:
|
||||||
|
table = self.get_table(request, permissions)
|
||||||
|
return self.export_table(table)
|
||||||
|
|
||||||
|
# Render the objects table
|
||||||
|
table = self.get_table(request, permissions)
|
||||||
|
configure_table(table, request)
|
||||||
|
|
||||||
|
# If this is an HTMX request, return only the rendered table HTML
|
||||||
|
if is_htmx(request):
|
||||||
|
return render(request, 'htmx/table.html', {
|
||||||
|
'table': table,
|
||||||
|
})
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'content_type': content_type,
|
||||||
|
'table': table,
|
||||||
|
'permissions': permissions,
|
||||||
|
'action_buttons': self.action_buttons,
|
||||||
|
'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
|
||||||
|
**self.get_extra_context(request),
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, self.template_name, context)
|
||||||
|
|
||||||
|
|
||||||
|
class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView):
|
||||||
"""
|
"""
|
||||||
Create new objects in bulk.
|
Create new objects in bulk.
|
||||||
|
|
||||||
queryset: Base queryset for the objects being created
|
|
||||||
form: Form class which provides the `pattern` field
|
form: Form class which provides the `pattern` field
|
||||||
model_form: The ModelForm used to create individual objects
|
model_form: The ModelForm used to create individual objects
|
||||||
pattern_target: Name of the field to be evaluated as a pattern (if any)
|
pattern_target: Name of the field to be evaluated as a pattern (if any)
|
||||||
template_name: The name of the template
|
|
||||||
"""
|
"""
|
||||||
queryset = None
|
|
||||||
form = None
|
form = None
|
||||||
model_form = None
|
model_form = None
|
||||||
pattern_target = ''
|
pattern_target = ''
|
||||||
template_name = None
|
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return get_permission_for_model(self.queryset.model, 'add')
|
return get_permission_for_model(self.queryset.model, 'add')
|
||||||
@ -73,6 +229,10 @@ class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
|
|
||||||
return new_objects
|
return new_objects
|
||||||
|
|
||||||
|
#
|
||||||
|
# Request handlers
|
||||||
|
#
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
# Set initial values for visible form fields from query args
|
# Set initial values for visible form fields from query args
|
||||||
initial = {}
|
initial = {}
|
||||||
@ -88,6 +248,7 @@ class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
'form': form,
|
'form': form,
|
||||||
'model_form': model_form,
|
'model_form': model_form,
|
||||||
'return_url': self.get_return_url(request),
|
'return_url': self.get_return_url(request),
|
||||||
|
**self.get_extra_context(request),
|
||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
@ -132,31 +293,25 @@ class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
'model_form': model_form,
|
'model_form': model_form,
|
||||||
'obj_type': model._meta.verbose_name,
|
'obj_type': model._meta.verbose_name,
|
||||||
'return_url': self.get_return_url(request),
|
'return_url': self.get_return_url(request),
|
||||||
|
**self.get_extra_context(request),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
||||||
"""
|
"""
|
||||||
Import objects in bulk (CSV format).
|
Import objects in bulk (CSV format).
|
||||||
|
|
||||||
queryset: Base queryset for the model
|
Attributes:
|
||||||
model_form: The form used to create each imported object
|
model_form: The form used to create each imported object
|
||||||
table: The django-tables2 Table used to render the list of imported objects
|
|
||||||
template_name: The name of the template
|
|
||||||
widget_attrs: A dict of attributes to apply to the import widget (e.g. to require a session key)
|
|
||||||
"""
|
"""
|
||||||
queryset = None
|
|
||||||
model_form = None
|
|
||||||
table = None
|
|
||||||
template_name = 'generic/object_bulk_import.html'
|
template_name = 'generic/object_bulk_import.html'
|
||||||
widget_attrs = {}
|
model_form = None
|
||||||
|
|
||||||
def _import_form(self, *args, **kwargs):
|
def _import_form(self, *args, **kwargs):
|
||||||
|
|
||||||
class ImportForm(BootstrapMixin, Form):
|
class ImportForm(BootstrapMixin, Form):
|
||||||
csv = CSVDataField(
|
csv = CSVDataField(
|
||||||
from_form=self.model_form,
|
from_form=self.model_form
|
||||||
widget=Textarea(attrs=self.widget_attrs)
|
|
||||||
)
|
)
|
||||||
csv_file = CSVFileField(
|
csv_file = CSVFileField(
|
||||||
label="CSV file",
|
label="CSV file",
|
||||||
@ -207,6 +362,10 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return get_permission_for_model(self.queryset.model, 'add')
|
return get_permission_for_model(self.queryset.model, 'add')
|
||||||
|
|
||||||
|
#
|
||||||
|
# Request handlers
|
||||||
|
#
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
@ -214,6 +373,7 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
'fields': self.model_form().fields,
|
'fields': self.model_form().fields,
|
||||||
'obj_type': self.model_form._meta.model._meta.verbose_name,
|
'obj_type': self.model_form._meta.model._meta.verbose_name,
|
||||||
'return_url': self.get_return_url(request),
|
'return_url': self.get_return_url(request),
|
||||||
|
**self.get_extra_context(request),
|
||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
@ -262,24 +422,21 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
'fields': self.model_form().fields,
|
'fields': self.model_form().fields,
|
||||||
'obj_type': self.model_form._meta.model._meta.verbose_name,
|
'obj_type': self.model_form._meta.model._meta.verbose_name,
|
||||||
'return_url': self.get_return_url(request),
|
'return_url': self.get_return_url(request),
|
||||||
|
**self.get_extra_context(request),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
|
||||||
"""
|
"""
|
||||||
Edit objects in bulk.
|
Edit objects in bulk.
|
||||||
|
|
||||||
queryset: Custom queryset to use when retrieving objects (e.g. to select related objects)
|
Attributes:
|
||||||
filterset: FilterSet to apply when deleting by QuerySet
|
filterset: FilterSet to apply when deleting by QuerySet
|
||||||
table: The table used to display devices being edited
|
|
||||||
form: The form class used to edit objects in bulk
|
form: The form class used to edit objects in bulk
|
||||||
template_name: The name of the template
|
|
||||||
"""
|
"""
|
||||||
queryset = None
|
|
||||||
filterset = None
|
|
||||||
table = None
|
|
||||||
form = None
|
|
||||||
template_name = 'generic/object_bulk_edit.html'
|
template_name = 'generic/object_bulk_edit.html'
|
||||||
|
filterset = None
|
||||||
|
form = None
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return get_permission_for_model(self.queryset.model, 'change')
|
return get_permission_for_model(self.queryset.model, 'change')
|
||||||
@ -341,6 +498,10 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
|
|
||||||
return updated_objects
|
return updated_objects
|
||||||
|
|
||||||
|
#
|
||||||
|
# Request handlers
|
||||||
|
#
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return redirect(self.get_return_url(request))
|
return redirect(self.get_return_url(request))
|
||||||
|
|
||||||
@ -419,17 +580,14 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
'table': table,
|
'table': table,
|
||||||
'obj_type_plural': model._meta.verbose_name_plural,
|
'obj_type_plural': model._meta.verbose_name_plural,
|
||||||
'return_url': self.get_return_url(request),
|
'return_url': self.get_return_url(request),
|
||||||
|
**self.get_extra_context(request),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
|
||||||
"""
|
"""
|
||||||
An extendable view for renaming objects in bulk.
|
An extendable view for renaming objects in bulk.
|
||||||
|
|
||||||
queryset: QuerySet of objects being renamed
|
|
||||||
template_name: The name of the template
|
|
||||||
"""
|
"""
|
||||||
queryset = None
|
|
||||||
template_name = 'generic/object_bulk_rename.html'
|
template_name = 'generic/object_bulk_rename.html'
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -513,25 +671,38 @@ class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
|
||||||
"""
|
"""
|
||||||
Delete objects in bulk.
|
Delete objects in bulk.
|
||||||
|
|
||||||
queryset: Custom queryset to use when retrieving objects (e.g. to select related objects)
|
|
||||||
filterset: FilterSet to apply when deleting by QuerySet
|
filterset: FilterSet to apply when deleting by QuerySet
|
||||||
table: The table used to display devices being deleted
|
table: The table used to display devices being deleted
|
||||||
form: The form class used to delete objects in bulk
|
form: The form class used to delete objects in bulk
|
||||||
template_name: The name of the template
|
|
||||||
"""
|
"""
|
||||||
queryset = None
|
template_name = 'generic/object_bulk_delete.html'
|
||||||
filterset = None
|
filterset = None
|
||||||
table = None
|
table = None
|
||||||
form = None
|
form = None
|
||||||
template_name = 'generic/object_bulk_delete.html'
|
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return get_permission_for_model(self.queryset.model, 'delete')
|
return get_permission_for_model(self.queryset.model, 'delete')
|
||||||
|
|
||||||
|
def get_form(self):
|
||||||
|
"""
|
||||||
|
Provide a standard bulk delete form if none has been specified for the view
|
||||||
|
"""
|
||||||
|
class BulkDeleteForm(ConfirmationForm):
|
||||||
|
pk = ModelMultipleChoiceField(queryset=self.queryset, widget=MultipleHiddenInput)
|
||||||
|
|
||||||
|
if self.form:
|
||||||
|
return self.form
|
||||||
|
|
||||||
|
return BulkDeleteForm
|
||||||
|
|
||||||
|
#
|
||||||
|
# Request handlers
|
||||||
|
#
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return redirect(self.get_return_url(request))
|
return redirect(self.get_return_url(request))
|
||||||
|
|
||||||
@ -594,37 +765,25 @@ class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
'obj_type_plural': model._meta.verbose_name_plural,
|
'obj_type_plural': model._meta.verbose_name_plural,
|
||||||
'table': table,
|
'table': table,
|
||||||
'return_url': self.get_return_url(request),
|
'return_url': self.get_return_url(request),
|
||||||
|
**self.get_extra_context(request),
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_form(self):
|
|
||||||
"""
|
|
||||||
Provide a standard bulk delete form if none has been specified for the view
|
|
||||||
"""
|
|
||||||
class BulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = ModelMultipleChoiceField(queryset=self.queryset, widget=MultipleHiddenInput)
|
|
||||||
|
|
||||||
if self.form:
|
|
||||||
return self.form
|
|
||||||
|
|
||||||
return BulkDeleteForm
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device/VirtualMachine components
|
# Device/VirtualMachine components
|
||||||
#
|
#
|
||||||
|
|
||||||
class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
|
||||||
"""
|
"""
|
||||||
Add one or more components (e.g. interfaces, console ports, etc.) to a set of Devices or VirtualMachines.
|
Add one or more components (e.g. interfaces, console ports, etc.) to a set of Devices or VirtualMachines.
|
||||||
"""
|
"""
|
||||||
|
template_name = 'generic/object_bulk_add_component.html'
|
||||||
parent_model = None
|
parent_model = None
|
||||||
parent_field = None
|
parent_field = None
|
||||||
form = None
|
form = None
|
||||||
queryset = None
|
|
||||||
model_form = None
|
model_form = None
|
||||||
filterset = None
|
filterset = None
|
||||||
table = None
|
table = None
|
||||||
template_name = 'generic/object_bulk_add_component.html'
|
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return f'dcim.add_{self.queryset.model._meta.model_name}'
|
return f'dcim.add_{self.queryset.model._meta.model_name}'
|
||||||
|
@ -2,21 +2,16 @@ import logging
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import ProtectedError
|
from django.db.models import ProtectedError
|
||||||
from django.forms.widgets import HiddenInput
|
from django.forms.widgets import HiddenInput
|
||||||
from django.http import HttpResponse
|
from django.shortcuts import redirect, render
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.http import is_safe_url
|
from django.utils.http import is_safe_url
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.views.generic import View
|
|
||||||
from django_tables2.export import TableExport
|
|
||||||
|
|
||||||
from extras.models import ExportTemplate
|
|
||||||
from extras.signals import clear_webhooks
|
from extras.signals import clear_webhooks
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
from utilities.exceptions import AbortTransaction, PermissionsViolation
|
from utilities.exceptions import AbortTransaction, PermissionsViolation
|
||||||
@ -25,7 +20,8 @@ from utilities.htmx import is_htmx
|
|||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
from utilities.tables import configure_table
|
from utilities.tables import configure_table
|
||||||
from utilities.utils import normalize_querydict, prepare_cloned_fields
|
from utilities.utils import normalize_querydict, prepare_cloned_fields
|
||||||
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin
|
from utilities.views import GetReturnURLMixin
|
||||||
|
from .base import BaseObjectView
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ComponentCreateView',
|
'ComponentCreateView',
|
||||||
@ -33,49 +29,41 @@ __all__ = (
|
|||||||
'ObjectDeleteView',
|
'ObjectDeleteView',
|
||||||
'ObjectEditView',
|
'ObjectEditView',
|
||||||
'ObjectImportView',
|
'ObjectImportView',
|
||||||
'ObjectListView',
|
|
||||||
'ObjectView',
|
'ObjectView',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ObjectView(ObjectPermissionRequiredMixin, View):
|
class ObjectView(BaseObjectView):
|
||||||
"""
|
"""
|
||||||
Retrieve a single object for display.
|
Retrieve a single object for display.
|
||||||
|
|
||||||
queryset: The base queryset for retrieving the object
|
Note: If `template_name` is not specified, it will be determined automatically based on the queryset model.
|
||||||
template_name: Name of the template to use
|
|
||||||
"""
|
"""
|
||||||
queryset = None
|
|
||||||
template_name = None
|
|
||||||
|
|
||||||
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')
|
||||||
|
|
||||||
def get_template_name(self):
|
def get_template_name(self):
|
||||||
"""
|
"""
|
||||||
Return self.template_name if set. Otherwise, resolve the template path by model app_label and name.
|
Return self.template_name if defined. Otherwise, dynamically resolve the template name using the queryset
|
||||||
|
model's `app_label` and `model_name`.
|
||||||
"""
|
"""
|
||||||
if self.template_name is not None:
|
if self.template_name is not None:
|
||||||
return self.template_name
|
return self.template_name
|
||||||
model_opts = self.queryset.model._meta
|
model_opts = self.queryset.model._meta
|
||||||
return f'{model_opts.app_label}/{model_opts.model_name}.html'
|
return f'{model_opts.app_label}/{model_opts.model_name}.html'
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
#
|
||||||
"""
|
# Request handlers
|
||||||
Return any additional context data for the template.
|
#
|
||||||
|
|
||||||
:param request: The current request
|
def get(self, request, **kwargs):
|
||||||
:param instance: The object being viewed
|
|
||||||
"""
|
"""
|
||||||
return {}
|
GET request handler. `*args` and `**kwargs` are passed to identify the object being queried.
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
Args:
|
||||||
|
request: The current request
|
||||||
"""
|
"""
|
||||||
GET request handler. *args and **kwargs are passed to identify the object being queried.
|
instance = self.get_object(**kwargs)
|
||||||
|
|
||||||
:param request: The current request
|
|
||||||
"""
|
|
||||||
instance = get_object_or_404(self.queryset, **kwargs)
|
|
||||||
|
|
||||||
return render(request, self.get_template_name(), {
|
return render(request, self.get_template_name(), {
|
||||||
'object': instance,
|
'object': instance,
|
||||||
@ -87,15 +75,12 @@ class ObjectChildrenView(ObjectView):
|
|||||||
"""
|
"""
|
||||||
Display a table of child objects associated with the parent object.
|
Display a table of child objects associated with the parent object.
|
||||||
|
|
||||||
queryset: The base queryset for retrieving the *parent* object
|
Attributes:
|
||||||
table: Table class used to render child objects list
|
table: Table class used to render child objects list
|
||||||
template_name: Name of the template to use
|
|
||||||
"""
|
"""
|
||||||
queryset = None
|
|
||||||
child_model = None
|
child_model = None
|
||||||
table = None
|
table = None
|
||||||
filterset = None
|
filterset = None
|
||||||
template_name = None
|
|
||||||
|
|
||||||
def get_children(self, request, parent):
|
def get_children(self, request, parent):
|
||||||
"""
|
"""
|
||||||
@ -110,17 +95,22 @@ class ObjectChildrenView(ObjectView):
|
|||||||
"""
|
"""
|
||||||
Provides a hook for subclassed views to modify data before initializing the table.
|
Provides a hook for subclassed views to modify data before initializing the table.
|
||||||
|
|
||||||
:param request: The current request
|
Args:
|
||||||
:param queryset: The filtered queryset of child objects
|
request: The current request
|
||||||
:param parent: The parent object
|
queryset: The filtered queryset of child objects
|
||||||
|
parent: The parent object
|
||||||
"""
|
"""
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
#
|
||||||
|
# Request handlers
|
||||||
|
#
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
GET handler for rendering child objects.
|
GET handler for rendering child objects.
|
||||||
"""
|
"""
|
||||||
instance = get_object_or_404(self.queryset, **kwargs)
|
instance = self.get_object(**kwargs)
|
||||||
child_objects = self.get_children(request, instance)
|
child_objects = self.get_children(request, instance)
|
||||||
|
|
||||||
if self.filterset:
|
if self.filterset:
|
||||||
@ -152,171 +142,17 @@ class ObjectChildrenView(ObjectView):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class ObjectListView(ObjectPermissionRequiredMixin, View):
|
class ObjectImportView(GetReturnURLMixin, BaseObjectView):
|
||||||
"""
|
|
||||||
List a series of objects.
|
|
||||||
|
|
||||||
queryset: The queryset of objects to display. Note: Prefetching related objects is not necessary, as the
|
|
||||||
table will prefetch objects as needed depending on the columns being displayed.
|
|
||||||
filterset: A django-filter FilterSet that is applied to the queryset
|
|
||||||
filterset_form: The form used to render filter options
|
|
||||||
table: The django-tables2 Table used to render the objects list
|
|
||||||
template_name: The name of the template
|
|
||||||
action_buttons: A list of buttons to include at the top of the page
|
|
||||||
"""
|
|
||||||
queryset = None
|
|
||||||
filterset = None
|
|
||||||
filterset_form = None
|
|
||||||
table = None
|
|
||||||
template_name = 'generic/object_list.html'
|
|
||||||
action_buttons = ('add', 'import', 'export')
|
|
||||||
|
|
||||||
def get_required_permission(self):
|
|
||||||
return get_permission_for_model(self.queryset.model, 'view')
|
|
||||||
|
|
||||||
def get_table(self, request, permissions):
|
|
||||||
"""
|
|
||||||
Return the django-tables2 Table instance to be used for rendering the objects list.
|
|
||||||
|
|
||||||
:param request: The current request
|
|
||||||
:param permissions: A dictionary mapping of the view, add, change, and delete permissions to booleans indicating
|
|
||||||
whether the user has each
|
|
||||||
"""
|
|
||||||
table = self.table(self.queryset, user=request.user)
|
|
||||||
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
|
|
||||||
table.columns.show('pk')
|
|
||||||
|
|
||||||
return table
|
|
||||||
|
|
||||||
def export_yaml(self):
|
|
||||||
"""
|
|
||||||
Export the queryset of objects as concatenated YAML documents.
|
|
||||||
"""
|
|
||||||
yaml_data = [obj.to_yaml() for obj in self.queryset]
|
|
||||||
|
|
||||||
return '---\n'.join(yaml_data)
|
|
||||||
|
|
||||||
def export_table(self, table, columns=None):
|
|
||||||
"""
|
|
||||||
Export all table data in CSV format.
|
|
||||||
|
|
||||||
:param table: The Table instance to export
|
|
||||||
:param columns: A list of specific columns to include. If not specified, all columns will be exported.
|
|
||||||
"""
|
|
||||||
exclude_columns = {'pk', 'actions'}
|
|
||||||
if columns:
|
|
||||||
all_columns = [col_name for col_name, _ in table.selected_columns + table.available_columns]
|
|
||||||
exclude_columns.update({
|
|
||||||
col for col in all_columns if col not in columns
|
|
||||||
})
|
|
||||||
exporter = TableExport(
|
|
||||||
export_format=TableExport.CSV,
|
|
||||||
table=table,
|
|
||||||
exclude_columns=exclude_columns
|
|
||||||
)
|
|
||||||
return exporter.response(
|
|
||||||
filename=f'netbox_{self.queryset.model._meta.verbose_name_plural}.csv'
|
|
||||||
)
|
|
||||||
|
|
||||||
def export_template(self, template, request):
|
|
||||||
"""
|
|
||||||
Render an ExportTemplate using the current queryset.
|
|
||||||
|
|
||||||
:param template: ExportTemplate instance
|
|
||||||
:param request: The current request
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return template.render_to_response(self.queryset)
|
|
||||||
except Exception as e:
|
|
||||||
messages.error(request, f"There was an error rendering the selected export template ({template.name}): {e}")
|
|
||||||
return redirect(request.path)
|
|
||||||
|
|
||||||
def get_extra_context(self, request):
|
|
||||||
"""
|
|
||||||
Return any additional context data for the template.
|
|
||||||
|
|
||||||
:param request: The current request
|
|
||||||
"""
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def get(self, request):
|
|
||||||
"""
|
|
||||||
GET request handler.
|
|
||||||
|
|
||||||
:param request: The current request
|
|
||||||
"""
|
|
||||||
model = self.queryset.model
|
|
||||||
content_type = ContentType.objects.get_for_model(model)
|
|
||||||
|
|
||||||
if self.filterset:
|
|
||||||
self.queryset = self.filterset(request.GET, self.queryset).qs
|
|
||||||
|
|
||||||
# Compile a dictionary indicating which permissions are available to the current user for this model
|
|
||||||
permissions = {}
|
|
||||||
for action in ('add', 'change', 'delete', 'view'):
|
|
||||||
perm_name = get_permission_for_model(model, action)
|
|
||||||
permissions[action] = request.user.has_perm(perm_name)
|
|
||||||
|
|
||||||
if 'export' in request.GET:
|
|
||||||
|
|
||||||
# Export the current table view
|
|
||||||
if request.GET['export'] == 'table':
|
|
||||||
table = self.get_table(request, permissions)
|
|
||||||
columns = [name for name, _ in table.selected_columns]
|
|
||||||
return self.export_table(table, columns)
|
|
||||||
|
|
||||||
# Render an ExportTemplate
|
|
||||||
elif request.GET['export']:
|
|
||||||
template = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export'])
|
|
||||||
return self.export_template(template, request)
|
|
||||||
|
|
||||||
# Check for YAML export support on the model
|
|
||||||
elif hasattr(model, 'to_yaml'):
|
|
||||||
response = HttpResponse(self.export_yaml(), content_type='text/yaml')
|
|
||||||
filename = 'netbox_{}.yaml'.format(self.queryset.model._meta.verbose_name_plural)
|
|
||||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
|
||||||
return response
|
|
||||||
|
|
||||||
# Fall back to default table/YAML export
|
|
||||||
else:
|
|
||||||
table = self.get_table(request, permissions)
|
|
||||||
return self.export_table(table)
|
|
||||||
|
|
||||||
# Render the objects table
|
|
||||||
table = self.get_table(request, permissions)
|
|
||||||
configure_table(table, request)
|
|
||||||
|
|
||||||
# If this is an HTMX request, return only the rendered table HTML
|
|
||||||
if is_htmx(request):
|
|
||||||
return render(request, 'htmx/table.html', {
|
|
||||||
'table': table,
|
|
||||||
})
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'content_type': content_type,
|
|
||||||
'table': table,
|
|
||||||
'permissions': permissions,
|
|
||||||
'action_buttons': self.action_buttons,
|
|
||||||
'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
|
|
||||||
}
|
|
||||||
context.update(self.get_extra_context(request))
|
|
||||||
|
|
||||||
return render(request, self.template_name, context)
|
|
||||||
|
|
||||||
|
|
||||||
class ObjectImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|
||||||
"""
|
"""
|
||||||
Import a single object (YAML or JSON format).
|
Import a single object (YAML or JSON format).
|
||||||
|
|
||||||
queryset: Base queryset for the objects being created
|
Attributes:
|
||||||
model_form: The ModelForm used to create individual objects
|
model_form: The ModelForm used to create individual objects
|
||||||
related_object_forms: A dictionary mapping of forms to be used for the creation of related (child) objects
|
related_object_forms: A dictionary mapping of forms to be used for the creation of related (child) objects
|
||||||
template_name: The name of the template
|
|
||||||
"""
|
"""
|
||||||
queryset = None
|
template_name = 'generic/object_import.html'
|
||||||
model_form = None
|
model_form = None
|
||||||
related_object_forms = dict()
|
related_object_forms = dict()
|
||||||
template_name = 'generic/object_import.html'
|
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return get_permission_for_model(self.queryset.model, 'add')
|
return get_permission_for_model(self.queryset.model, 'add')
|
||||||
@ -367,6 +203,10 @@ class ObjectImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
#
|
||||||
|
# Request handlers
|
||||||
|
#
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
form = ImportForm()
|
form = ImportForm()
|
||||||
|
|
||||||
@ -445,17 +285,21 @@ class ObjectImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
||||||
"""
|
"""
|
||||||
Create or edit a single object.
|
Create or edit a single object.
|
||||||
|
|
||||||
queryset: The base QuerySet for the object being modified
|
Attributes:
|
||||||
model_form: The form used to create or edit the object
|
model_form: The form used to create or edit the object
|
||||||
template_name: The name of the template
|
|
||||||
"""
|
"""
|
||||||
queryset = None
|
|
||||||
model_form = None
|
|
||||||
template_name = 'generic/object_edit.html'
|
template_name = 'generic/object_edit.html'
|
||||||
|
model_form = None
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
# Determine required permission based on whether we are editing an existing object
|
||||||
|
self._permission_action = 'change' if kwargs else 'add'
|
||||||
|
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
# self._permission_action is set by dispatch() to either "add" or "change" depending on whether
|
# self._permission_action is set by dispatch() to either "add" or "change" depending on whether
|
||||||
@ -464,42 +308,36 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
|
|
||||||
def get_object(self, **kwargs):
|
def get_object(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Return an instance for editing. If a PK has been specified, this will be an existing object.
|
Return an object for editing. If no keyword arguments have been specified, this will be a new instance.
|
||||||
|
|
||||||
:param kwargs: URL path kwargs
|
|
||||||
"""
|
"""
|
||||||
if 'pk' in kwargs:
|
if not kwargs:
|
||||||
obj = get_object_or_404(self.queryset, **kwargs)
|
# We're creating a new object
|
||||||
# Take a snapshot of change-logged models
|
|
||||||
if hasattr(obj, 'snapshot'):
|
|
||||||
obj.snapshot()
|
|
||||||
return obj
|
|
||||||
|
|
||||||
return self.queryset.model()
|
return self.queryset.model()
|
||||||
|
return super().get_object(**kwargs)
|
||||||
|
|
||||||
def alter_object(self, obj, request, url_args, url_kwargs):
|
def alter_object(self, obj, request, url_args, url_kwargs):
|
||||||
"""
|
"""
|
||||||
Provides a hook for views to modify an object before it is processed. For example, a parent object can be
|
Provides a hook for views to modify an object before it is processed. For example, a parent object can be
|
||||||
defined given some parameter from the request URL.
|
defined given some parameter from the request URL.
|
||||||
|
|
||||||
:param obj: The object being edited
|
Args:
|
||||||
:param request: The current request
|
obj: The object being edited
|
||||||
:param url_args: URL path args
|
request: The current request
|
||||||
:param url_kwargs: URL path kwargs
|
url_args: URL path args
|
||||||
|
url_kwargs: URL path kwargs
|
||||||
"""
|
"""
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
#
|
||||||
# Determine required permission based on whether we are editing an existing object
|
# Request handlers
|
||||||
self._permission_action = 'change' if kwargs else 'add'
|
#
|
||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
GET request handler.
|
GET request handler.
|
||||||
|
|
||||||
:param request: The current request
|
Args:
|
||||||
|
request: The current request
|
||||||
"""
|
"""
|
||||||
obj = self.get_object(**kwargs)
|
obj = self.get_object(**kwargs)
|
||||||
obj = self.alter_object(obj, request, args, kwargs)
|
obj = self.alter_object(obj, request, args, kwargs)
|
||||||
@ -513,16 +351,23 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
'obj_type': self.queryset.model._meta.verbose_name,
|
'obj_type': self.queryset.model._meta.verbose_name,
|
||||||
'form': form,
|
'form': form,
|
||||||
'return_url': self.get_return_url(request, obj),
|
'return_url': self.get_return_url(request, obj),
|
||||||
|
**self.get_extra_context(request, obj),
|
||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
POST request handler.
|
POST request handler.
|
||||||
|
|
||||||
:param request: The current request
|
Args:
|
||||||
|
request: The current request
|
||||||
"""
|
"""
|
||||||
logger = logging.getLogger('netbox.views.ObjectEditView')
|
logger = logging.getLogger('netbox.views.ObjectEditView')
|
||||||
obj = self.get_object(**kwargs)
|
obj = self.get_object(**kwargs)
|
||||||
|
|
||||||
|
# Take a snapshot for change logging (if editing an existing object)
|
||||||
|
if obj.pk and hasattr(obj, 'snapshot'):
|
||||||
|
obj.snapshot()
|
||||||
|
|
||||||
obj = self.alter_object(obj, request, args, kwargs)
|
obj = self.alter_object(obj, request, args, kwargs)
|
||||||
|
|
||||||
form = self.model_form(
|
form = self.model_form(
|
||||||
@ -585,41 +430,29 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
'obj_type': self.queryset.model._meta.verbose_name,
|
'obj_type': self.queryset.model._meta.verbose_name,
|
||||||
'form': form,
|
'form': form,
|
||||||
'return_url': self.get_return_url(request, obj),
|
'return_url': self.get_return_url(request, obj),
|
||||||
|
**self.get_extra_context(request, obj),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
class ObjectDeleteView(GetReturnURLMixin, BaseObjectView):
|
||||||
"""
|
"""
|
||||||
Delete a single object.
|
Delete a single object.
|
||||||
|
|
||||||
queryset: The base queryset for the object being deleted
|
|
||||||
template_name: The name of the template
|
|
||||||
"""
|
"""
|
||||||
queryset = None
|
|
||||||
template_name = 'generic/object_delete.html'
|
template_name = 'generic/object_delete.html'
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return get_permission_for_model(self.queryset.model, 'delete')
|
return get_permission_for_model(self.queryset.model, 'delete')
|
||||||
|
|
||||||
def get_object(self, **kwargs):
|
#
|
||||||
"""
|
# Request handlers
|
||||||
Return an instance for deletion. If a PK has been specified, this will be an existing object.
|
#
|
||||||
|
|
||||||
:param kwargs: URL path kwargs
|
|
||||||
"""
|
|
||||||
obj = get_object_or_404(self.queryset, **kwargs)
|
|
||||||
|
|
||||||
# Take a snapshot of change-logged models
|
|
||||||
if hasattr(obj, 'snapshot'):
|
|
||||||
obj.snapshot()
|
|
||||||
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
GET request handler.
|
GET request handler.
|
||||||
|
|
||||||
:param request: The current request
|
Args:
|
||||||
|
request: The current request
|
||||||
"""
|
"""
|
||||||
obj = self.get_object(**kwargs)
|
obj = self.get_object(**kwargs)
|
||||||
form = ConfirmationForm(initial=request.GET)
|
form = ConfirmationForm(initial=request.GET)
|
||||||
@ -633,6 +466,7 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
'object_type': self.queryset.model._meta.verbose_name,
|
'object_type': self.queryset.model._meta.verbose_name,
|
||||||
'form': form,
|
'form': form,
|
||||||
'form_url': form_url,
|
'form_url': form_url,
|
||||||
|
**self.get_extra_context(request, obj),
|
||||||
})
|
})
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
@ -640,18 +474,24 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
'object_type': self.queryset.model._meta.verbose_name,
|
'object_type': self.queryset.model._meta.verbose_name,
|
||||||
'form': form,
|
'form': form,
|
||||||
'return_url': self.get_return_url(request, obj),
|
'return_url': self.get_return_url(request, obj),
|
||||||
|
**self.get_extra_context(request, obj),
|
||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
POST request handler.
|
POST request handler.
|
||||||
|
|
||||||
:param request: The current request
|
Args:
|
||||||
|
request: The current request
|
||||||
"""
|
"""
|
||||||
logger = logging.getLogger('netbox.views.ObjectDeleteView')
|
logger = logging.getLogger('netbox.views.ObjectDeleteView')
|
||||||
obj = self.get_object(**kwargs)
|
obj = self.get_object(**kwargs)
|
||||||
form = ConfirmationForm(request.POST)
|
form = ConfirmationForm(request.POST)
|
||||||
|
|
||||||
|
# Take a snapshot of change-logged models
|
||||||
|
if hasattr(obj, 'snapshot'):
|
||||||
|
obj.snapshot()
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
logger.debug("Form validation was successful")
|
logger.debug("Form validation was successful")
|
||||||
|
|
||||||
@ -680,6 +520,7 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
'object_type': self.queryset.model._meta.verbose_name,
|
'object_type': self.queryset.model._meta.verbose_name,
|
||||||
'form': form,
|
'form': form,
|
||||||
'return_url': self.get_return_url(request, obj),
|
'return_url': self.get_return_url(request, obj),
|
||||||
|
**self.get_extra_context(request, obj),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -687,14 +528,13 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
# Device/VirtualMachine components
|
# Device/VirtualMachine components
|
||||||
#
|
#
|
||||||
|
|
||||||
class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
|
||||||
"""
|
"""
|
||||||
Add one or more components (e.g. interfaces, console ports, etc.) to a Device or VirtualMachine.
|
Add one or more components (e.g. interfaces, console ports, etc.) to a Device or VirtualMachine.
|
||||||
"""
|
"""
|
||||||
queryset = None
|
template_name = 'dcim/component_create.html'
|
||||||
form = None
|
form = None
|
||||||
model_form = None
|
model_form = None
|
||||||
template_name = 'dcim/component_create.html'
|
|
||||||
patterned_fields = ('name', 'label')
|
patterned_fields = ('name', 'label')
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
|
Reference in New Issue
Block a user