Refactor generic views; add plugins dev documentation

This commit is contained in:
jeremystretch 2022-01-20 16:31:55 -05:00
parent 7002319cc8
commit 54834c47f8
5 changed files with 360 additions and 254 deletions

View File

@ -0,0 +1,91 @@
# 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 |
### 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'
...
```
## Generic Views Reference
Below is the class definition for NetBox's base GenericView. The attributes and methods defined here are available on all generic views.
::: netbox.views.generic.base.GenericView
rendering:
show_source: false
!!! 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.
::: netbox.views.generic.ObjectView
selection:
members:
- get_object
- get_template_name
- get_extra_context
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
::: netbox.views.generic.ObjectListView
selection:
members:
- get_table
- get_extra_context
- export_yaml
- 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

View File

@ -102,6 +102,7 @@ nav:
- Developing Plugins:
- Introduction: 'plugins/development/index.md'
- Model Features: 'plugins/development/model-features.md'
- Generic Views: 'plugins/development/generic-views.md'
- Developing Plugins (Old): 'plugins/development.md'
- Administration:
- Authentication: 'administration/authentication.md'

View File

@ -0,0 +1,15 @@
from django.views.generic import View
from utilities.views import ObjectPermissionRequiredMixin
class GenericView(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

View File

@ -3,21 +3,27 @@ import re
from copy import deepcopy
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldDoesNotExist, ValidationError
from django.db import transaction, IntegrityError
from django.db.models import ManyToManyField, ProtectedError
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
from django.shortcuts import redirect, render
from django.views.generic import View
from django.http import HttpResponse
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 utilities.error_handlers import handle_protectederror
from utilities.exceptions import PermissionsViolation
from utilities.forms import (
BootstrapMixin, BulkRenameForm, ConfirmationForm, CSVDataField, CSVFileField, restrict_form_fields,
)
from utilities.htmx import is_htmx
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 GenericView
__all__ = (
'BulkComponentCreateView',
@ -26,24 +32,181 @@ __all__ = (
'BulkEditView',
'BulkImportView',
'BulkRenameView',
'ObjectListView',
)
class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
class ObjectListView(GenericView):
"""
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
table: The django-tables2 Table used to render the objects list
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
table = 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
def get_extra_context(self, request):
"""
Return any additional context data for the template.
Agrs:
request: The current request
"""
return {}
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,
}
context.update(self.get_extra_context(request))
return render(request, self.template_name, context)
#
# 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)
class BulkCreateView(GetReturnURLMixin, GenericView):
"""
Create new objects in bulk.
queryset: Base queryset for the objects being created
form: Form class which provides the `pattern` field
model_form: The ModelForm used to create individual objects
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
model_form = None
pattern_target = ''
template_name = None
def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'add')
@ -135,20 +298,18 @@ class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
})
class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
class BulkImportView(GetReturnURLMixin, GenericView):
"""
Import objects in bulk (CSV format).
queryset: Base queryset for the model
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)
Attributes:
model_form: The form used to create each imported object
table: The django-tables2 Table used to render the list of imported objects
widget_attrs: A dict of attributes to apply to the import widget (e.g. to require a session key)
"""
queryset = None
template_name = 'generic/object_bulk_import.html'
model_form = None
table = None
template_name = 'generic/object_bulk_import.html'
widget_attrs = {}
def _import_form(self, *args, **kwargs):
@ -265,21 +426,19 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
})
class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
class BulkEditView(GetReturnURLMixin, GenericView):
"""
Edit 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
table: The table used to display devices being edited
form: The form class used to edit objects in bulk
template_name: The name of the template
Attributes:
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
"""
queryset = None
template_name = 'generic/object_bulk_edit.html'
filterset = None
table = None
form = None
template_name = 'generic/object_bulk_edit.html'
def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'change')
@ -422,14 +581,10 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
})
class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
class BulkRenameView(GetReturnURLMixin, GenericView):
"""
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'
def __init__(self, *args, **kwargs):
@ -513,21 +668,18 @@ class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
})
class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
class BulkDeleteView(GetReturnURLMixin, GenericView):
"""
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
table: The table used to display devices being deleted
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
table = None
form = None
template_name = 'generic/object_bulk_delete.html'
def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'delete')
@ -613,18 +765,17 @@ class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
# Device/VirtualMachine components
#
class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
class BulkComponentCreateView(GetReturnURLMixin, GenericView):
"""
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_field = None
form = None
queryset = None
model_form = None
filterset = None
table = None
template_name = 'generic/object_bulk_add_component.html'
def get_required_permission(self):
return f'dcim.add_{self.queryset.model._meta.model_name}'

View File

@ -2,21 +2,16 @@ import logging
from copy import deepcopy
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.db.models import ProtectedError
from django.forms.widgets import HiddenInput
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.html import escape
from django.utils.http import is_safe_url
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 utilities.error_handlers import handle_protectederror
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.tables import configure_table
from utilities.utils import normalize_querydict, prepare_cloned_fields
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin
from utilities.views import GetReturnURLMixin
from .base import GenericView
__all__ = (
'ComponentCreateView',
@ -33,27 +29,31 @@ __all__ = (
'ObjectDeleteView',
'ObjectEditView',
'ObjectImportView',
'ObjectListView',
'ObjectView',
)
class ObjectView(ObjectPermissionRequiredMixin, View):
class ObjectView(GenericView):
"""
Retrieve a single object for display.
queryset: The base queryset for retrieving the object
template_name: Name of the template to use
Note: If `template_name` is not specified, it will be determined automatically based on the queryset model.
"""
queryset = None
template_name = None
def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'view')
def get_object(self, **kwargs):
"""
Return the object being viewed, identified by the keyword arguments passed. If no matching object is found,
raise a 404 error.
"""
return get_object_or_404(self.queryset, **kwargs)
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:
return self.template_name
@ -64,18 +64,20 @@ class ObjectView(ObjectPermissionRequiredMixin, View):
"""
Return any additional context data for the template.
:param request: The current request
:param instance: The object being viewed
Args:
request: The current request
instance: The object being viewed
"""
return {}
def get(self, request, *args, **kwargs):
def get(self, request, **kwargs):
"""
GET request handler. *args and **kwargs are passed to identify the object being queried.
GET request handler. `*args` and `**kwargs` are passed to identify the object being queried.
:param request: The current request
Args:
request: The current request
"""
instance = get_object_or_404(self.queryset, **kwargs)
instance = self.get_object(**kwargs)
return render(request, self.get_template_name(), {
'object': instance,
@ -87,15 +89,12 @@ class ObjectChildrenView(ObjectView):
"""
Display a table of child objects associated with the parent object.
queryset: The base queryset for retrieving the *parent* object
table: Table class used to render child objects list
template_name: Name of the template to use
Attributes:
table: Table class used to render child objects list
"""
queryset = None
child_model = None
table = None
filterset = None
template_name = None
def get_children(self, request, parent):
"""
@ -110,9 +109,10 @@ class ObjectChildrenView(ObjectView):
"""
Provides a hook for subclassed views to modify data before initializing the table.
:param request: The current request
:param queryset: The filtered queryset of child objects
:param parent: The parent object
Args:
request: The current request
queryset: The filtered queryset of child objects
parent: The parent object
"""
return queryset
@ -120,7 +120,7 @@ class ObjectChildrenView(ObjectView):
"""
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)
if self.filterset:
@ -152,171 +152,17 @@ class ObjectChildrenView(ObjectView):
})
class ObjectListView(ObjectPermissionRequiredMixin, View):
"""
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):
class ObjectImportView(GetReturnURLMixin, GenericView):
"""
Import a single object (YAML or JSON format).
queryset: Base queryset for the objects being created
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
template_name: The name of the template
Attributes:
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
"""
queryset = None
template_name = 'generic/object_import.html'
model_form = None
related_object_forms = dict()
template_name = 'generic/object_import.html'
def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'add')
@ -445,17 +291,21 @@ class ObjectImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
})
class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
class ObjectEditView(GetReturnURLMixin, GenericView):
"""
Create or edit a single object.
queryset: The base QuerySet for the object being modified
model_form: The form used to create or edit the object
template_name: The name of the template
Attributes:
model_form: The form used to create or edit the object
"""
queryset = None
model_form = None
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):
# self._permission_action is set by dispatch() to either "add" or "change" depending on whether
@ -466,13 +316,16 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
"""
Return an instance for editing. If a PK has been specified, this will be an existing object.
:param kwargs: URL path kwargs
Args:
kwargs: URL path kwargs
"""
if 'pk' in kwargs:
obj = get_object_or_404(self.queryset, **kwargs)
# Take a snapshot of change-logged models
if hasattr(obj, 'snapshot'):
obj.snapshot()
return obj
return self.queryset.model()
@ -482,24 +335,20 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
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.
:param obj: The object being edited
:param request: The current request
:param url_args: URL path args
:param url_kwargs: URL path kwargs
Args:
obj: The object being edited
request: The current request
url_args: URL path args
url_kwargs: URL path kwargs
"""
return obj
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(self, request, *args, **kwargs):
"""
GET request handler.
:param request: The current request
Args:
request: The current request
"""
obj = self.get_object(**kwargs)
obj = self.alter_object(obj, request, args, kwargs)
@ -519,7 +368,8 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
"""
POST request handler.
:param request: The current request
Args:
request: The current request
"""
logger = logging.getLogger('netbox.views.ObjectEditView')
obj = self.get_object(**kwargs)
@ -588,14 +438,10 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
})
class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
class ObjectDeleteView(GetReturnURLMixin, GenericView):
"""
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'
def get_required_permission(self):
@ -605,7 +451,8 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
"""
Return an instance for deletion. If a PK has been specified, this will be an existing object.
:param kwargs: URL path kwargs
Args:
kwargs: URL path kwargs
"""
obj = get_object_or_404(self.queryset, **kwargs)
@ -619,7 +466,8 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
"""
GET request handler.
:param request: The current request
Args:
request: The current request
"""
obj = self.get_object(**kwargs)
form = ConfirmationForm(initial=request.GET)
@ -646,7 +494,8 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
"""
POST request handler.
:param request: The current request
Args:
request: The current request
"""
logger = logging.getLogger('netbox.views.ObjectDeleteView')
obj = self.get_object(**kwargs)
@ -687,14 +536,13 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
# Device/VirtualMachine components
#
class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
class ComponentCreateView(GetReturnURLMixin, GenericView):
"""
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
model_form = None
template_name = 'dcim/component_create.html'
patterned_fields = ('name', 'label')
def get_required_permission(self):