From 54834c47f8870e7faabcd847c3270da0bd3d2884 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 20 Jan 2022 16:31:55 -0500 Subject: [PATCH] Refactor generic views; add plugins dev documentation --- docs/plugins/development/generic-views.md | 91 +++++++ mkdocs.yml | 1 + netbox/netbox/views/generic/base.py | 15 ++ netbox/netbox/views/generic/bulk_views.py | 225 +++++++++++++--- netbox/netbox/views/generic/object_views.py | 282 +++++--------------- 5 files changed, 360 insertions(+), 254 deletions(-) create mode 100644 docs/plugins/development/generic-views.md create mode 100644 netbox/netbox/views/generic/base.py diff --git a/docs/plugins/development/generic-views.md b/docs/plugins/development/generic-views.md new file mode 100644 index 000000000..ced7e3807 --- /dev/null +++ b/docs/plugins/development/generic-views.md @@ -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 diff --git a/mkdocs.yml b/mkdocs.yml index 585e6d76f..c36d3f467 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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' diff --git a/netbox/netbox/views/generic/base.py b/netbox/netbox/views/generic/base.py new file mode 100644 index 000000000..3861a93aa --- /dev/null +++ b/netbox/netbox/views/generic/base.py @@ -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 diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index e9b213a95..c1ae2038e 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -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}' diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index b6df5e3c2..79732572d 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -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):