1
0
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:
Jeremy Stretch
2022-01-21 15:37:20 -05:00
committed by GitHub
5 changed files with 456 additions and 301 deletions

View 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

View File

@ -28,6 +28,7 @@ plugins:
- django.setup()
rendering:
heading_level: 3
members_order: source
show_root_heading: true
show_root_full_path: false
show_root_toc_entry: false
@ -102,6 +103,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,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 {}

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 BaseMultiObjectView
__all__ = (
'BulkComponentCreateView',
@ -26,24 +32,174 @@ __all__ = (
'BulkEditView',
'BulkImportView',
'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.
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')
@ -73,6 +229,10 @@ class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
return new_objects
#
# Request handlers
#
def get(self, request):
# Set initial values for visible form fields from query args
initial = {}
@ -88,6 +248,7 @@ class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
'form': form,
'model_form': model_form,
'return_url': self.get_return_url(request),
**self.get_extra_context(request),
})
def post(self, request):
@ -132,31 +293,25 @@ class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
'model_form': model_form,
'obj_type': model._meta.verbose_name,
'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).
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
"""
queryset = None
model_form = None
table = None
template_name = 'generic/object_bulk_import.html'
widget_attrs = {}
model_form = None
def _import_form(self, *args, **kwargs):
class ImportForm(BootstrapMixin, Form):
csv = CSVDataField(
from_form=self.model_form,
widget=Textarea(attrs=self.widget_attrs)
from_form=self.model_form
)
csv_file = CSVFileField(
label="CSV file",
@ -207,6 +362,10 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'add')
#
# Request handlers
#
def get(self, request):
return render(request, self.template_name, {
@ -214,6 +373,7 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
'fields': self.model_form().fields,
'obj_type': self.model_form._meta.model._meta.verbose_name,
'return_url': self.get_return_url(request),
**self.get_extra_context(request),
})
def post(self, request):
@ -262,24 +422,21 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
'fields': self.model_form().fields,
'obj_type': self.model_form._meta.model._meta.verbose_name,
'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.
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
form: The form class used to edit objects in bulk
"""
queryset = None
filterset = None
table = None
form = None
template_name = 'generic/object_bulk_edit.html'
filterset = None
form = None
def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'change')
@ -341,6 +498,10 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
return updated_objects
#
# Request handlers
#
def get(self, request):
return redirect(self.get_return_url(request))
@ -419,17 +580,14 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
'table': table,
'obj_type_plural': model._meta.verbose_name_plural,
'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.
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,25 +671,38 @@ class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
})
class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
"""
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')
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):
return redirect(self.get_return_url(request))
@ -594,37 +765,25 @@ class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
'obj_type_plural': model._meta.verbose_name_plural,
'table': table,
'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
#
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.
"""
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.shortcuts import 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 BaseObjectView
__all__ = (
'ComponentCreateView',
@ -33,49 +29,41 @@ __all__ = (
'ObjectDeleteView',
'ObjectEditView',
'ObjectImportView',
'ObjectListView',
'ObjectView',
)
class ObjectView(ObjectPermissionRequiredMixin, View):
class ObjectView(BaseObjectView):
"""
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_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
model_opts = self.queryset.model._meta
return f'{model_opts.app_label}/{model_opts.model_name}.html'
def get_extra_context(self, request, instance):
"""
Return any additional context data for the template.
#
# Request handlers
#
:param request: The current request
:param instance: The object being viewed
def get(self, request, **kwargs):
"""
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.
:param 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 +75,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,17 +95,22 @@ 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
#
# Request handlers
#
def get(self, request, *args, **kwargs):
"""
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 +142,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, BaseObjectView):
"""
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')
@ -367,6 +203,10 @@ class ObjectImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
return obj
#
# Request handlers
#
def get(self, request):
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.
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
@ -464,42 +308,36 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
def get_object(self, **kwargs):
"""
Return an instance for editing. If a PK has been specified, this will be an existing object.
:param kwargs: URL path kwargs
Return an object for editing. If no keyword arguments have been specified, this will be a new instance.
"""
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()
if not kwargs:
# We're creating a new object
return self.queryset.model()
return super().get_object(**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
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)
#
# Request handlers
#
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)
@ -513,16 +351,23 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
'obj_type': self.queryset.model._meta.verbose_name,
'form': form,
'return_url': self.get_return_url(request, obj),
**self.get_extra_context(request, obj),
})
def post(self, request, *args, **kwargs):
"""
POST request handler.
:param request: The current request
Args:
request: The current request
"""
logger = logging.getLogger('netbox.views.ObjectEditView')
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)
form = self.model_form(
@ -585,41 +430,29 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
'obj_type': self.queryset.model._meta.verbose_name,
'form': form,
'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.
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):
return get_permission_for_model(self.queryset.model, 'delete')
def get_object(self, **kwargs):
"""
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
#
# Request handlers
#
def get(self, request, *args, **kwargs):
"""
GET request handler.
:param request: The current request
Args:
request: The current request
"""
obj = self.get_object(**kwargs)
form = ConfirmationForm(initial=request.GET)
@ -633,6 +466,7 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
'object_type': self.queryset.model._meta.verbose_name,
'form': form,
'form_url': form_url,
**self.get_extra_context(request, obj),
})
return render(request, self.template_name, {
@ -640,18 +474,24 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
'object_type': self.queryset.model._meta.verbose_name,
'form': form,
'return_url': self.get_return_url(request, obj),
**self.get_extra_context(request, obj),
})
def post(self, request, *args, **kwargs):
"""
POST request handler.
:param request: The current request
Args:
request: The current request
"""
logger = logging.getLogger('netbox.views.ObjectDeleteView')
obj = self.get_object(**kwargs)
form = ConfirmationForm(request.POST)
# Take a snapshot of change-logged models
if hasattr(obj, 'snapshot'):
obj.snapshot()
if form.is_valid():
logger.debug("Form validation was successful")
@ -680,6 +520,7 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
'object_type': self.queryset.model._meta.verbose_name,
'form': form,
'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
#
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.
"""
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):