From a74ed33b0ed53eddddad615835adc42534d246cc Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 21 Jan 2022 14:41:37 -0500 Subject: [PATCH] Move get_object() to BaseObjectView --- docs/plugins/development/generic-views.md | 1 - mkdocs.yml | 1 + netbox/netbox/views/generic/base.py | 12 ++ netbox/netbox/views/generic/bulk_views.py | 128 +++++++++++--------- netbox/netbox/views/generic/object_views.py | 70 ++++++----- 5 files changed, 118 insertions(+), 94 deletions(-) diff --git a/docs/plugins/development/generic-views.md b/docs/plugins/development/generic-views.md index d8ad0e7a4..1a444ca2c 100644 --- a/docs/plugins/development/generic-views.md +++ b/docs/plugins/development/generic-views.md @@ -71,7 +71,6 @@ Below is the class definition for NetBox's BaseMultiObjectView. The attributes a selection: members: - get_table - - export_yaml - export_table - export_template rendering: diff --git a/mkdocs.yml b/mkdocs.yml index c36d3f467..dbd31cb50 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 diff --git a/netbox/netbox/views/generic/base.py b/netbox/netbox/views/generic/base.py index 7d7c305dd..3ad3bcf67 100644 --- a/netbox/netbox/views/generic/base.py +++ b/netbox/netbox/views/generic/base.py @@ -1,3 +1,4 @@ +from django.shortcuts import get_object_or_404 from django.views.generic import View from utilities.views import ObjectPermissionRequiredMixin @@ -14,6 +15,15 @@ class BaseObjectView(ObjectPermissionRequiredMixin, View): 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. @@ -31,9 +41,11 @@ class BaseMultiObjectView(ObjectPermissionRequiredMixin, View): 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): diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 3025818fa..5286de314 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -43,13 +43,11 @@ class ObjectListView(BaseMultiObjectView): 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): @@ -70,6 +68,61 @@ class ObjectListView(BaseMultiObjectView): 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. @@ -135,57 +188,6 @@ class ObjectListView(BaseMultiObjectView): 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, BaseMultiObjectView): """ @@ -227,6 +229,10 @@ class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView): return new_objects + # + # Request handlers + # + def get(self, request): # Set initial values for visible form fields from query args initial = {} @@ -297,12 +303,10 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView): 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) """ template_name = 'generic/object_bulk_import.html' model_form = None - table = None widget_attrs = {} def _import_form(self, *args, **kwargs): @@ -361,6 +365,10 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView): 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, { @@ -427,12 +435,10 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView): 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 """ template_name = 'generic/object_bulk_edit.html' filterset = None - table = None form = None def get_required_permission(self): @@ -495,6 +501,10 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView): return updated_objects + # + # Request handlers + # + def get(self, request): return redirect(self.get_return_url(request)) @@ -692,6 +702,10 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView): return BulkDeleteForm + # + # Request handlers + # + def get(self, request): return redirect(self.get_return_url(request)) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index c681767c2..09a102442 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -6,7 +6,7 @@ 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.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 @@ -42,13 +42,6 @@ class ObjectView(BaseObjectView): 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 defined. Otherwise, dynamically resolve the template name using the queryset @@ -59,6 +52,10 @@ class ObjectView(BaseObjectView): model_opts = self.queryset.model._meta return f'{model_opts.app_label}/{model_opts.model_name}.html' + # + # Request handlers + # + def get(self, request, **kwargs): """ GET request handler. `*args` and `**kwargs` are passed to identify the object being queried. @@ -105,6 +102,10 @@ class ObjectChildrenView(ObjectView): """ return queryset + # + # Request handlers + # + def get(self, request, *args, **kwargs): """ GET handler for rendering child objects. @@ -202,6 +203,10 @@ class ObjectImportView(GetReturnURLMixin, BaseObjectView): return obj + # + # Request handlers + # + def get(self, request): form = ImportForm() @@ -303,21 +308,12 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): def get_object(self, **kwargs): """ - Return an instance for editing. If a PK has been specified, this will be an existing object. - - Args: - 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): """ @@ -332,6 +328,10 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): """ return obj + # + # Request handlers + # + def get(self, request, *args, **kwargs): """ GET request handler. @@ -363,6 +363,11 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): """ 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( @@ -438,20 +443,9 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView): 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. - - Args: - 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): """ @@ -494,6 +488,10 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView): 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")