From e19ce692388c2242ad271dfe2e85b269d7101d31 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 9 Jan 2023 10:08:26 -0500 Subject: [PATCH 001/174] Closes #10923: Remove unused NetBoxModelCSVForm class --- docs/release-notes/version-3.5.md | 7 +++++++ mkdocs.yml | 1 + netbox/netbox/forms/base.py | 9 --------- netbox/netbox/settings.py | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) create mode 100644 docs/release-notes/version-3.5.md diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md new file mode 100644 index 000000000..1e3ea18a9 --- /dev/null +++ b/docs/release-notes/version-3.5.md @@ -0,0 +1,7 @@ +# NetBox v3.4 + +## v3.5.0 (FUTURE) + +### Other Changes + +* [#10923](https://github.com/netbox-community/netbox/issues/10923) - Remove unused `NetBoxModelCSVForm` class (replaced by `NetBoxModelImportForm`) diff --git a/mkdocs.yml b/mkdocs.yml index 2317dad6d..ff9174455 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -259,6 +259,7 @@ nav: - git Cheat Sheet: 'development/git-cheat-sheet.md' - Release Notes: - Summary: 'release-notes/index.md' + - Version 3.5: 'release-notes/version-3.5.md' - Version 3.4: 'release-notes/version-3.4.md' - Version 3.3: 'release-notes/version-3.3.md' - Version 3.2: 'release-notes/version-3.2.md' diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index b4ad39b5e..83c238e0f 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -10,7 +10,6 @@ from utilities.forms import BootstrapMixin, CSVModelForm from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField __all__ = ( - 'NetBoxModelCSVForm', 'NetBoxModelForm', 'NetBoxModelImportForm', 'NetBoxModelBulkEditForm', @@ -86,14 +85,6 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm): return customfield.to_form_field(for_csv_import=True) -class NetBoxModelCSVForm(NetBoxModelImportForm): - """ - Maintains backward compatibility for NetBoxModelImportForm for plugins. - """ - # TODO: Remove in NetBox v3.5 - pass - - class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, forms.Form): """ Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index f6ce7ff33..cc8aa44e8 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -24,7 +24,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.4.5-dev' +VERSION = '3.5.0-dev' # Hostname HOSTNAME = platform.node() From 2381317eb32beef69703d7c67fa3a76896390ad5 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 9 Jan 2023 10:13:40 -0500 Subject: [PATCH 002/174] Closes #10604: Remove unused extra_tabs block from object.html generic template --- docs/plugins/development/templates.md | 1 - docs/release-notes/version-3.5.md | 3 ++- netbox/templates/generic/object.html | 4 ---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/plugins/development/templates.md b/docs/plugins/development/templates.md index 20838149f..0e67a8ae0 100644 --- a/docs/plugins/development/templates.md +++ b/docs/plugins/development/templates.md @@ -74,7 +74,6 @@ This template is used by the `ObjectView` generic view to display a single objec | `breadcrumbs` | - | Breadcrumb list items (HTML `
  • ` elements) | | `object_identifier` | - | A unique identifier (string) for the object | | `extra_controls` | - | Additional action buttons to display | -| `extra_tabs` | - | Additional tabs to include | #### Context diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 1e3ea18a9..b8453e5f3 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -1,7 +1,8 @@ -# NetBox v3.4 +# NetBox v3.5 ## v3.5.0 (FUTURE) ### Other Changes +* [#10604](https://github.com/netbox-community/netbox/issues/10604) - Remove unused `extra_tabs` block from `object.html` generic template * [#10923](https://github.com/netbox-community/netbox/issues/10923) - Remove unused `NetBoxModelCSVForm` class (replaced by `NetBoxModelImportForm`) diff --git a/netbox/templates/generic/object.html b/netbox/templates/generic/object.html index 023726a30..d3a617455 100644 --- a/netbox/templates/generic/object.html +++ b/netbox/templates/generic/object.html @@ -11,7 +11,6 @@ Blocks: breadcrumbs: Breadcrumb list items (HTML
  • elements) object_identifier: Unique identifier for the object extra_controls: Additional action buttons to display - extra_tabs: Additional tabs to include content: Page content Context: @@ -84,9 +83,6 @@ Context: {{ object|meta:"verbose_name"|bettertitle }}
  • - {# Include any extra tabs passed by the view #} - {% block extra_tabs %}{% endblock %} - {# Include tabs for registered model views #} {% model_view_tabs object %} From 0b4ea14e9ad8bf286d7b9d94057881d6a58483ce Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 13 Jan 2023 10:00:49 -0500 Subject: [PATCH 003/174] Closes #11489: Refactor & combine core middleware --- netbox/netbox/middleware.py | 169 +++++++++++++----------------------- netbox/netbox/settings.py | 8 +- 2 files changed, 62 insertions(+), 115 deletions(-) diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py index edf88a234..0b1d77484 100644 --- a/netbox/netbox/middleware.py +++ b/netbox/netbox/middleware.py @@ -14,24 +14,73 @@ from netbox.config import clear_config from netbox.views import handler_500 from utilities.api import is_api_request, rest_api_server_error +__all__ = ( + 'CoreMiddleware', + 'RemoteUserMiddleware', +) + + +class CoreMiddleware: -class LoginRequiredMiddleware: - """ - If LOGIN_REQUIRED is True, redirect all non-authenticated users to the login page. - """ def __init__(self, get_response): self.get_response = get_response def __call__(self, request): - # Redirect unauthenticated requests (except those exempted) to the login page if LOGIN_REQUIRED is true - if settings.LOGIN_REQUIRED and not request.user.is_authenticated: - # Redirect unauthenticated requests - if not request.path_info.startswith(settings.EXEMPT_PATHS): - login_url = f'{settings.LOGIN_URL}?next={parse.quote(request.get_full_path_info())}' - return HttpResponseRedirect(login_url) + # Assign a random unique ID to the request. This will be used for change logging. + request.id = uuid.uuid4() - return self.get_response(request) + # Enforce the LOGIN_REQUIRED config parameter. If true, redirect all non-exempt unauthenticated requests + # to the login page. + if ( + settings.LOGIN_REQUIRED and + not request.user.is_authenticated and + not request.path_info.startswith(settings.AUTH_EXEMPT_PATHS) + ): + login_url = f'{settings.LOGIN_URL}?next={parse.quote(request.get_full_path_info())}' + return HttpResponseRedirect(login_url) + + # Enable the change_logging context manager and process the request. + with change_logging(request): + response = self.get_response(request) + + # If this is an API request, attach an HTTP header annotating the API version (e.g. '3.5'). + if is_api_request(request): + response['API-Version'] = settings.REST_FRAMEWORK_VERSION + + # Clear any cached dynamic config parameters after each request. + clear_config() + + return response + + def process_exception(self, request, exception): + """ + Implement custom error handling logic for production deployments. + """ + # Don't catch exceptions when in debug mode + if settings.DEBUG: + return + + # Cleanly handle exceptions that occur from REST API requests + if is_api_request(request): + return rest_api_server_error(request) + + # Ignore Http404s (defer to Django's built-in 404 handling) + if isinstance(exception, Http404): + return + + # Determine the type of exception. If it's a common issue, return a custom error page with instructions. + custom_template = None + if isinstance(exception, ProgrammingError): + custom_template = 'exceptions/programming_error.html' + elif isinstance(exception, ImportError): + custom_template = 'exceptions/import_error.html' + elif isinstance(exception, PermissionError): + custom_template = 'exceptions/permission_error.html' + + # Return a custom error message, or fall back to Django's default 500 error handling + if custom_template: + return handler_500(request, template_name=custom_template) class RemoteUserMiddleware(RemoteUserMiddleware_): @@ -104,101 +153,3 @@ class RemoteUserMiddleware(RemoteUserMiddleware_): groups = [] logger.debug(f"Groups are {groups}") return groups - - -class ObjectChangeMiddleware: - """ - This middleware performs three functions in response to an object being created, updated, or deleted: - - 1. Create an ObjectChange to reflect the modification to the object in the changelog. - 2. Enqueue any relevant webhooks. - 3. Increment the metric counter for the event type. - - The post_save and post_delete signals are employed to catch object modifications, however changes are recorded a bit - differently for each. Objects being saved are cached into thread-local storage for action *after* the response has - completed. This ensures that serialization of the object is performed only after any related objects (e.g. tags) - have been created. Conversely, deletions are acted upon immediately, so that the serialized representation of the - object is recorded before it (and any related objects) are actually deleted from the database. - """ - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - # Assign a random unique ID to the request. This will be used to associate multiple object changes made during - # the same request. - request.id = uuid.uuid4() - - # Process the request with change logging enabled - with change_logging(request): - response = self.get_response(request) - - return response - - -class APIVersionMiddleware: - """ - If the request is for an API endpoint, include the API version as a response header. - """ - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - response = self.get_response(request) - if is_api_request(request): - response['API-Version'] = settings.REST_FRAMEWORK_VERSION - return response - - -class DynamicConfigMiddleware: - """ - Store the cached NetBox configuration in thread-local storage for the duration of the request. - """ - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - response = self.get_response(request) - clear_config() - return response - - -class ExceptionHandlingMiddleware: - """ - Intercept certain exceptions which are likely indicative of installation issues and provide helpful instructions - to the user. - """ - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - return self.get_response(request) - - def process_exception(self, request, exception): - - # Handle exceptions that occur from REST API requests - # if is_api_request(request): - # return rest_api_server_error(request) - - # Don't catch exceptions when in debug mode - if settings.DEBUG: - return - - # Ignore Http404s (defer to Django's built-in 404 handling) - if isinstance(exception, Http404): - return - - # Determine the type of exception. If it's a common issue, return a custom error page with instructions. - custom_template = None - if isinstance(exception, ProgrammingError): - custom_template = 'exceptions/programming_error.html' - elif isinstance(exception, ImportError): - custom_template = 'exceptions/import_error.html' - elif isinstance(exception, PermissionError): - custom_template = 'exceptions/permission_error.html' - - # Return a custom error message, or fall back to Django's default 500 error handling - if custom_template: - return handler_500(request, template_name=custom_template) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index cc8aa44e8..7f55463df 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -358,12 +358,8 @@ MIDDLEWARE = [ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', - 'netbox.middleware.ExceptionHandlingMiddleware', 'netbox.middleware.RemoteUserMiddleware', - 'netbox.middleware.LoginRequiredMiddleware', - 'netbox.middleware.DynamicConfigMiddleware', - 'netbox.middleware.APIVersionMiddleware', - 'netbox.middleware.ObjectChangeMiddleware', + 'netbox.middleware.CoreMiddleware', 'django_prometheus.middleware.PrometheusAfterMiddleware', ] @@ -448,7 +444,7 @@ EXEMPT_EXCLUDE_MODELS = ( ) # All URLs starting with a string listed here are exempt from login enforcement -EXEMPT_PATHS = ( +AUTH_EXEMPT_PATHS = ( f'/{BASE_PATH}api/', f'/{BASE_PATH}graphql/', f'/{BASE_PATH}login/', From ef3ac25406947b502d3a6a92c050200de6bcf7a6 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 13 Jan 2023 10:58:20 -0500 Subject: [PATCH 004/174] Remove old feature version notices --- docs/administration/permissions.md | 2 -- docs/customization/custom-fields.md | 6 ------ docs/customization/custom-scripts.md | 2 -- docs/customization/reports.md | 2 -- docs/integrations/rest-api.md | 3 --- docs/plugins/development/navigation.md | 3 --- docs/plugins/development/search.md | 3 --- docs/plugins/development/staged-changes.md | 3 --- docs/plugins/development/views.md | 3 --- 9 files changed, 27 deletions(-) diff --git a/docs/administration/permissions.md b/docs/administration/permissions.md index 21f259979..bcfbf0ba4 100644 --- a/docs/administration/permissions.md +++ b/docs/administration/permissions.md @@ -58,8 +58,6 @@ Additionally, where multiple permissions have been assigned for an object type, ### User Token -!!! info "This feature was introduced in NetBox v3.3" - When defining a permission constraint, administrators may use the special token `$user` to reference the current user at the time of evaluation. This can be helpful to restrict users to editing only their own journal entries, for example. Such a constraint might be defined as: ```json diff --git a/docs/customization/custom-fields.md b/docs/customization/custom-fields.md index 81aaa5247..7dc82e179 100644 --- a/docs/customization/custom-fields.md +++ b/docs/customization/custom-fields.md @@ -35,18 +35,12 @@ The filter logic controls how values are matched when filtering objects by the c ### Grouping -!!! note - This feature was introduced in NetBox v3.3. - Related custom fields can be grouped together within the UI by assigning each the same group name. When at least one custom field for an object type has a group defined, it will appear under the group heading within the custom fields panel under the object view. All custom fields with the same group name will appear under that heading. (Note that the group names must match exactly, or each will appear as a separate heading.) This parameter has no effect on the API representation of custom field data. ### Visibility -!!! note - This feature was introduced in NetBox v3.3. - When creating a custom field, there are three options for UI visibility. These control how and whether the custom field is displayed within the NetBox UI. * **Read/write** (default): The custom field is included when viewing and editing objects. diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md index af1e9b5b6..eb4a8626b 100644 --- a/docs/customization/custom-scripts.md +++ b/docs/customization/custom-scripts.md @@ -93,8 +93,6 @@ commit_default = False Set the maximum allowed runtime for the script. If not set, `RQ_DEFAULT_TIMEOUT` will be used. -!!! info "This feature was introduced in v3.2.1" - ## Accessing Request Data Details of the current HTTP request (the one being made to execute the script) are available as the instance attribute `self.request`. This can be used to infer, for example, the user executing the script and the client IP address: diff --git a/docs/customization/reports.md b/docs/customization/reports.md index b83c4a177..9db436961 100644 --- a/docs/customization/reports.md +++ b/docs/customization/reports.md @@ -95,8 +95,6 @@ A human-friendly description of what your report does. Set the maximum allowed runtime for the report. If not set, `RQ_DEFAULT_TIMEOUT` will be used. -!!! info "This feature was introduced in v3.2.1" - ## Logging The following methods are available to log results within a report: diff --git a/docs/integrations/rest-api.md b/docs/integrations/rest-api.md index 6f54a8cb0..25741ce6c 100644 --- a/docs/integrations/rest-api.md +++ b/docs/integrations/rest-api.md @@ -584,9 +584,6 @@ Additionally, a token can be set to expire at a specific time. This can be usefu #### Client IP Restriction -!!! note - This feature was introduced in NetBox v3.3. - Each API token can optionally be restricted by client IP address. If one or more allowed IP prefixes/addresses is defined for a token, authentication will fail for any client connecting from an IP address outside the defined range(s). This enables restricting the use a token to a specific client. (By default, any client IP address is permitted.) diff --git a/docs/plugins/development/navigation.md b/docs/plugins/development/navigation.md index 5f4a8a0dc..3e7762184 100644 --- a/docs/plugins/development/navigation.md +++ b/docs/plugins/development/navigation.md @@ -2,9 +2,6 @@ ## Menus -!!! note - This feature was introduced in NetBox v3.4. - A plugin can register its own submenu as part of NetBox's navigation menu. This is done by defining a variable named `menu` in `navigation.py`, pointing to an instance of the `PluginMenu` class. Each menu must define a label and grouped menu items (discussed below), and may optionally specify an icon. An example is shown below. ```python title="navigation.py" diff --git a/docs/plugins/development/search.md b/docs/plugins/development/search.md index b6f24f58d..e3b861f00 100644 --- a/docs/plugins/development/search.md +++ b/docs/plugins/development/search.md @@ -1,8 +1,5 @@ # Search -!!! note - This feature was introduced in NetBox v3.4. - Plugins can define and register their own models to extend NetBox's core search functionality. Typically, a plugin will include a file named `search.py`, which holds all search indexes for its models (see the example below). ```python diff --git a/docs/plugins/development/staged-changes.md b/docs/plugins/development/staged-changes.md index 7a4446eea..64a1a43e0 100644 --- a/docs/plugins/development/staged-changes.md +++ b/docs/plugins/development/staged-changes.md @@ -3,9 +3,6 @@ !!! danger "Experimental Feature" This feature is still under active development and considered experimental in nature. Its use in production is strongly discouraged at this time. -!!! note - This feature was introduced in NetBox v3.4. - NetBox provides a programmatic API to stage the creation, modification, and deletion of objects without actually committing those changes to the active database. This can be useful for performing a "dry run" of bulk operations, or preparing a set of changes for administrative approval, for example. To begin staging changes, first create a [branch](../../models/extras/branch.md): diff --git a/docs/plugins/development/views.md b/docs/plugins/development/views.md index 7f8a64744..3d0e87a68 100644 --- a/docs/plugins/development/views.md +++ b/docs/plugins/development/views.md @@ -157,9 +157,6 @@ These views are provided to enable or enhance certain NetBox model features, suc ### Additional Tabs -!!! note - This feature was introduced in NetBox v3.4. - Plugins can "attach" a custom view to a core NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`: ```python From f74a2536f1ca24dd8984c7bff7b386244c29f8a7 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 13 Jan 2023 11:41:57 -0500 Subject: [PATCH 005/174] Closes #11254: Introduce the X-Request-ID HTTP header to annotate the unique ID of each request for change logging --- docs/features/change-logging.md | 6 +++++- docs/integrations/rest-api.md | 20 +++++++++++++++++++- docs/release-notes/version-3.5.md | 4 ++++ netbox/netbox/middleware.py | 3 +++ netbox/netbox/tests/test_api.py | 14 ++++++++++++-- 5 files changed, 43 insertions(+), 4 deletions(-) diff --git a/docs/features/change-logging.md b/docs/features/change-logging.md index 3eb99c94c..919f59110 100644 --- a/docs/features/change-logging.md +++ b/docs/features/change-logging.md @@ -1,9 +1,13 @@ # Change Logging -Every time an object in NetBox is created, updated, or deleted, a serialized copy of that object taken both before and after the change is saved to the database, along with meta data including the current time and the user associated with the change. These records form a persistent record of changes both for each individual object as well as NetBox as a whole. The global change log can be viewed by navigating to Other > Change Log. +Every time an object in NetBox is created, updated, or deleted, a serialized copy of that object taken both before and after the change is saved to the database, along with metadata including the current time and the user associated with the change. These records form a persistent record of changes both for each individual object as well as NetBox as a whole. The global change log can be viewed by navigating to Other > Change Log. A serialized representation of the instance being modified is included in JSON format. This is similar to how objects are conveyed within the REST API, but does not include any nested representations. For instance, the `tenant` field of a site will record only the tenant's ID, not a representation of the tenant. When a request is made, a UUID is generated and attached to any change records resulting from that request. For example, editing three objects in bulk will create a separate change record for each (three in total), and each of those objects will be associated with the same UUID. This makes it easy to identify all the change records resulting from a particular request. Change records are exposed in the API via the read-only endpoint `/api/extras/object-changes/`. They may also be exported via the web UI in CSV format. + +## Correlating Changes by Request + +Every request made to NetBox is assigned a random unique ID that can be used to correlate change records. For example, if you change the status of three sites using the UI's bulk edit feature, you will see three new change records (one for each site) all referencing the same request ID. This shows that all three changes were made as part of the same request. diff --git a/docs/integrations/rest-api.md b/docs/integrations/rest-api.md index 25741ce6c..342f01d74 100644 --- a/docs/integrations/rest-api.md +++ b/docs/integrations/rest-api.md @@ -586,7 +586,6 @@ Additionally, a token can be set to expire at a specific time. This can be usefu Each API token can optionally be restricted by client IP address. If one or more allowed IP prefixes/addresses is defined for a token, authentication will fail for any client connecting from an IP address outside the defined range(s). This enables restricting the use a token to a specific client. (By default, any client IP address is permitted.) - ### Authenticating to the API An authentication token is attached to a request by setting the `Authorization` header to the string `Token` followed by a space and the user's token: @@ -654,3 +653,22 @@ Note that we are _not_ passing an existing REST API token with this request. If "description": "" } ``` + +## HTTP Headers + +### `API-Version` + +This header specifies the API version in use. This will always match the version of NetBox installed. For example, NetBox v3.4.2 will report an API version of `3.4`. + +### `X-Request-ID` + +!!! info "This feature was introduced in NetBox v3.5." + +This header specifies the unique ID assigned to the received API request. It can be very handy for correlating a request with change records. For example, after creating several new objects, you can filter against the object changes API endpoint to retrieve the resulting change records: + +``` +GET /api/extras/object-changes/?request_id=e39c84bc-f169-4d5f-bc1c-94487a1b18b5 +``` + +!!! note + This header is included with _all_ NetBox responses, although it is most practical when working with an API. diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index b8453e5f3..0c0765405 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -2,6 +2,10 @@ ## v3.5.0 (FUTURE) +### Enhancements + +* [#11254](https://github.com/netbox-community/netbox/issues/11254) - Introduce the `X-Request-ID` HTTP header to annotate the unique ID of each request for change logging + ### Other Changes * [#10604](https://github.com/netbox-community/netbox/issues/10604) - Remove unused `extra_tabs` block from `object.html` generic template diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py index 0b1d77484..e14b0781e 100644 --- a/netbox/netbox/middleware.py +++ b/netbox/netbox/middleware.py @@ -44,6 +44,9 @@ class CoreMiddleware: with change_logging(request): response = self.get_response(request) + # Attach the unique request ID as an HTTP header. + response['X-Request-ID'] = request.id + # If this is an API request, attach an HTTP header annotating the API version (e.g. '3.5'). if is_api_request(request): response['API-Version'] = settings.REST_FRAMEWORK_VERSION diff --git a/netbox/netbox/tests/test_api.py b/netbox/netbox/tests/test_api.py index 2ea12e72f..d087910b5 100644 --- a/netbox/netbox/tests/test_api.py +++ b/netbox/netbox/tests/test_api.py @@ -1,3 +1,5 @@ +import uuid + from django.urls import reverse from utilities.testing import APITestCase @@ -5,14 +7,22 @@ from utilities.testing import APITestCase class AppTest(APITestCase): + def test_http_headers(self): + response = self.client.get(reverse('api-root'), **self.header) + + # Check that all custom response headers are present and valid + self.assertEqual(response.status_code, 200) + request_id = response.headers['X-Request-ID'] + uuid.UUID(request_id) + def test_root(self): url = reverse('api-root') - response = self.client.get('{}?format=api'.format(url), **self.header) + response = self.client.get(f'{url}?format=api', **self.header) self.assertEqual(response.status_code, 200) def test_status(self): url = reverse('api-status') - response = self.client.get('{}?format=api'.format(url), **self.header) + response = self.client.get(f'{url}?format=api', **self.header) self.assertEqual(response.status_code, 200) From 1a2dae3471c4c7102339175f352c4c94efec10f9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 16 Jan 2023 15:50:45 -0500 Subject: [PATCH 006/174] Closes #8184: Enable HTMX for embedded tables (#11518) * Enable HTMX rendering for embedded tables * Start converting embedded tables to use HTMX (WIP) * Additional table conversions (WIP) * Standardize HTMX usage for nested group models * Enable HTMX for additional emebedded tables * Fix HTMX table rendering for ObjectChildrenView * Standardize usage of inc/panel_table.html * Hide selection boxes in embedded tables --- netbox/circuits/views.py | 38 ----- netbox/dcim/views.py | 145 ++---------------- netbox/ipam/views.py | 60 +------- netbox/netbox/tables/tables.py | 17 +- netbox/netbox/views/generic/bulk_views.py | 7 +- netbox/templates/circuits/circuittype.html | 10 +- netbox/templates/circuits/provider.html | 10 +- .../templates/circuits/providernetwork.html | 8 +- netbox/templates/dcim/connections_list.html | 2 +- .../templates/dcim/device/consoleports.html | 2 +- .../dcim/device/consoleserverports.html | 2 +- netbox/templates/dcim/device/devicebays.html | 2 +- netbox/templates/dcim/device/frontports.html | 2 +- netbox/templates/dcim/device/interfaces.html | 2 +- netbox/templates/dcim/device/inventory.html | 2 +- netbox/templates/dcim/device/modulebays.html | 2 +- .../templates/dcim/device/poweroutlets.html | 2 +- netbox/templates/dcim/device/powerports.html | 2 +- netbox/templates/dcim/device/rearports.html | 2 +- netbox/templates/dcim/devicerole.html | 4 +- .../dcim/devicetype/component_templates.html | 4 +- netbox/templates/dcim/interface.html | 11 +- netbox/templates/dcim/location.html | 10 +- netbox/templates/dcim/manufacturer.html | 14 +- .../dcim/moduletype/component_templates.html | 4 +- netbox/templates/dcim/platform.html | 10 +- netbox/templates/dcim/powerpanel.html | 70 +++++---- netbox/templates/dcim/rackrole.html | 10 +- netbox/templates/dcim/region.html | 25 ++- netbox/templates/dcim/sitegroup.html | 25 ++- .../templates/dcim/virtualdevicecontext.html | 14 +- netbox/templates/generic/object_list.html | 2 +- netbox/templates/home.html | 7 +- netbox/templates/htmx/table.html | 4 +- netbox/templates/inc/paginator_htmx.html | 24 +-- netbox/templates/inc/panel_table.html | 22 ++- netbox/templates/inc/table_htmx.html | 109 +++++++------ netbox/templates/ipam/aggregate/prefixes.html | 2 +- netbox/templates/ipam/asn.html | 16 +- netbox/templates/ipam/fhrpgroup.html | 22 +-- netbox/templates/ipam/ipaddress.html | 25 +-- .../templates/ipam/iprange/ip_addresses.html | 2 +- netbox/templates/ipam/l2vpn.html | 7 +- .../templates/ipam/prefix/ip_addresses.html | 2 +- netbox/templates/ipam/prefix/ip_ranges.html | 2 +- netbox/templates/ipam/prefix/prefixes.html | 2 +- netbox/templates/ipam/rir.html | 12 +- netbox/templates/ipam/role.html | 28 +--- netbox/templates/ipam/vlan.html | 32 ++-- netbox/templates/ipam/vlan/interfaces.html | 2 +- netbox/templates/ipam/vlan/vminterfaces.html | 2 +- netbox/templates/search.html | 2 +- netbox/templates/tenancy/contactgroup.html | 31 ++-- netbox/templates/tenancy/tenantgroup.html | 24 ++- .../virtualization/cluster/devices.html | 2 +- .../cluster/virtual_machines.html | 2 +- .../virtualization/clustergroup.html | 10 +- .../templates/virtualization/clustertype.html | 10 +- .../virtualmachine/interfaces.html | 2 +- .../templates/virtualization/vminterface.html | 15 +- .../templates/wireless/wirelesslangroup.html | 26 +++- netbox/tenancy/views.py | 37 ----- netbox/utilities/htmx.py | 14 ++ netbox/virtualization/views.py | 20 --- netbox/wireless/views.py | 11 -- 65 files changed, 381 insertions(+), 667 deletions(-) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 3168509ba..021709be1 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -29,20 +29,6 @@ class ProviderListView(generic.ObjectListView): class ProviderView(generic.ObjectView): queryset = Provider.objects.all() - def get_extra_context(self, request, instance): - circuits = Circuit.objects.restrict(request.user, 'view').filter( - provider=instance - ).prefetch_related( - 'tenant__group', 'termination_a__site', 'termination_z__site', - 'termination_a__provider_network', 'termination_z__provider_network', - ) - circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('provider',)) - circuits_table.configure(request) - - return { - 'circuits_table': circuits_table, - } - @register_model_view(Provider, 'edit') class ProviderEditView(generic.ObjectEditView): @@ -93,21 +79,6 @@ class ProviderNetworkListView(generic.ObjectListView): class ProviderNetworkView(generic.ObjectView): queryset = ProviderNetwork.objects.all() - def get_extra_context(self, request, instance): - circuits = Circuit.objects.restrict(request.user, 'view').filter( - Q(termination_a__provider_network=instance.pk) | - Q(termination_z__provider_network=instance.pk) - ).prefetch_related( - 'tenant__group', 'termination_a__site', 'termination_z__site', - 'termination_a__provider_network', 'termination_z__provider_network', - ) - circuits_table = tables.CircuitTable(circuits, user=request.user) - circuits_table.configure(request) - - return { - 'circuits_table': circuits_table, - } - @register_model_view(ProviderNetwork, 'edit') class ProviderNetworkEditView(generic.ObjectEditView): @@ -156,15 +127,6 @@ class CircuitTypeListView(generic.ObjectListView): class CircuitTypeView(generic.ObjectView): queryset = CircuitType.objects.all() - def get_extra_context(self, request, instance): - circuits = Circuit.objects.restrict(request.user, 'view').filter(type=instance) - circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('type',)) - circuits_table.configure(request) - - return { - 'circuits_table': circuits_table, - } - @register_model_view(CircuitType, 'edit') class CircuitTypeEditView(generic.ObjectEditView): diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 9b49e799c..63fdc47e0 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -14,7 +14,7 @@ from django.views.generic import View from circuits.models import Circuit, CircuitTermination from extras.views import ObjectConfigContextView from ipam.models import ASN, IPAddress, Prefix, Service, VLAN, VLANGroup -from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable +from ipam.tables import InterfaceVLANTable from netbox.views import generic from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator, get_paginate_count @@ -212,30 +212,6 @@ class RegionListView(generic.ObjectListView): class RegionView(generic.ObjectView): queryset = Region.objects.all() - def get_extra_context(self, request, instance): - child_regions = Region.objects.add_related_count( - Region.objects.all(), - Site, - 'region', - 'site_count', - cumulative=True - ).restrict(request.user, 'view').filter( - parent__in=instance.get_descendants(include_self=True) - ) - child_regions_table = tables.RegionTable(child_regions) - child_regions_table.columns.hide('actions') - - sites = Site.objects.restrict(request.user, 'view').filter( - region=instance - ) - sites_table = tables.SiteTable(sites, user=request.user, exclude=('region',)) - sites_table.configure(request) - - return { - 'child_regions_table': child_regions_table, - 'sites_table': sites_table, - } - @register_model_view(Region, 'edit') class RegionEditView(generic.ObjectEditView): @@ -300,30 +276,6 @@ class SiteGroupListView(generic.ObjectListView): class SiteGroupView(generic.ObjectView): queryset = SiteGroup.objects.all() - def get_extra_context(self, request, instance): - child_groups = SiteGroup.objects.add_related_count( - SiteGroup.objects.all(), - Site, - 'group', - 'site_count', - cumulative=True - ).restrict(request.user, 'view').filter( - parent__in=instance.get_descendants(include_self=True) - ) - child_groups_table = tables.SiteGroupTable(child_groups) - child_groups_table.columns.hide('actions') - - sites = Site.objects.restrict(request.user, 'view').filter( - group=instance - ) - sites_table = tables.SiteTable(sites, user=request.user, exclude=('group',)) - sites_table.configure(request) - - return { - 'child_groups_table': child_groups_table, - 'sites_table': sites_table, - } - @register_model_view(SiteGroup, 'edit') class SiteGroupEditView(generic.ObjectEditView): @@ -493,22 +445,6 @@ class LocationView(generic.ObjectView): rack_count = Rack.objects.filter(location__in=location_ids).count() device_count = Device.objects.filter(location__in=location_ids).count() - child_locations = Location.objects.add_related_count( - Location.objects.add_related_count( - Location.objects.all(), - Device, - 'location', - 'device_count', - cumulative=True - ), - Rack, - 'location', - 'rack_count', - cumulative=True - ).filter(pk__in=location_ids).exclude(pk=instance.pk) - child_locations_table = tables.LocationTable(child_locations, user=request.user) - child_locations_table.configure(request) - nonracked_devices = Device.objects.filter( location=instance, rack__isnull=True, @@ -518,7 +454,6 @@ class LocationView(generic.ObjectView): return { 'rack_count': rack_count, 'device_count': device_count, - 'child_locations_table': child_locations_table, 'nonracked_devices': nonracked_devices.order_by('-pk')[:10], 'total_nonracked_devices_count': nonracked_devices.count(), } @@ -583,20 +518,6 @@ class RackRoleListView(generic.ObjectListView): class RackRoleView(generic.ObjectView): queryset = RackRole.objects.all() - def get_extra_context(self, request, instance): - racks = Rack.objects.restrict(request.user, 'view').filter(role=instance).annotate( - device_count=count_related(Device, 'rack') - ) - - racks_table = tables.RackTable(racks, user=request.user, exclude=( - 'role', 'get_utilization', 'get_power_utilization', - )) - racks_table.configure(request) - - return { - 'racks_table': racks_table, - } - @register_model_view(RackRole, 'edit') class RackRoleEditView(generic.ObjectEditView): @@ -859,8 +780,6 @@ class ManufacturerView(generic.ObjectView): def get_extra_context(self, request, instance): device_types = DeviceType.objects.restrict(request.user, 'view').filter( manufacturer=instance - ).annotate( - instance_count=count_related(Device, 'device_type') ) module_types = ModuleType.objects.restrict(request.user, 'view').filter( manufacturer=instance @@ -869,13 +788,10 @@ class ManufacturerView(generic.ObjectView): manufacturer=instance ) - devicetypes_table = tables.DeviceTypeTable(device_types, user=request.user, exclude=('manufacturer',)) - devicetypes_table.configure(request) - return { - 'devicetypes_table': devicetypes_table, - 'inventory_item_count': inventory_items.count(), - 'module_type_count': module_types.count(), + 'devicetype_count': device_types.count(), + 'inventoryitem_count': inventory_items.count(), + 'moduletype_count': module_types.count(), } @@ -1726,19 +1642,6 @@ class DeviceRoleListView(generic.ObjectListView): class DeviceRoleView(generic.ObjectView): queryset = DeviceRole.objects.all() - def get_extra_context(self, request, instance): - devices = Device.objects.restrict(request.user, 'view').filter( - device_role=instance - ) - devices_table = tables.DeviceTable(devices, user=request.user, exclude=('device_role',)) - devices_table.configure(request) - - return { - 'devices_table': devices_table, - 'device_count': Device.objects.filter(device_role=instance).count(), - 'virtualmachine_count': VirtualMachine.objects.filter(role=instance).count(), - } - @register_model_view(DeviceRole, 'devices', path='devices') class DeviceRoleDevicesView(generic.ObjectChildrenView): @@ -1833,12 +1736,13 @@ class PlatformView(generic.ObjectView): devices = Device.objects.restrict(request.user, 'view').filter( platform=instance ) - devices_table = tables.DeviceTable(devices, user=request.user, exclude=('platform',)) - devices_table.configure(request) + virtual_machines = VirtualMachine.objects.restrict(request.user, 'view').filter( + platform=instance + ) return { - 'devices_table': devices_table, - 'virtualmachine_count': VirtualMachine.objects.filter(platform=instance).count() + 'device_count': devices.count(), + 'virtualmachine_count': virtual_machines.count() } @@ -2520,12 +2424,6 @@ class InterfaceView(generic.ObjectView): orderable=False ) - # Get assigned IP addresses - ipaddress_table = AssignedIPAddressesTable( - data=instance.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant'), - orderable=False - ) - # Get bridge interfaces bridge_interfaces = Interface.objects.restrict(request.user, 'view').filter(bridge=instance) bridge_interfaces_tables = tables.InterfaceTable( @@ -2558,7 +2456,6 @@ class InterfaceView(generic.ObjectView): return { 'vdc_table': vdc_table, - 'ipaddress_table': ipaddress_table, 'bridge_interfaces_table': bridge_interfaces_tables, 'child_interfaces_table': child_interfaces_tables, 'vlan_table': vlan_table, @@ -3533,20 +3430,6 @@ class PowerPanelListView(generic.ObjectListView): class PowerPanelView(generic.ObjectView): queryset = PowerPanel.objects.all() - def get_extra_context(self, request, instance): - power_feeds = PowerFeed.objects.restrict(request.user).filter(power_panel=instance) - powerfeed_table = tables.PowerFeedTable( - data=power_feeds, - orderable=False - ) - if request.user.has_perm('dcim.delete_cable'): - powerfeed_table.columns.show('pk') - powerfeed_table.exclude = ['power_panel'] - - return { - 'powerfeed_table': powerfeed_table, - } - @register_model_view(PowerPanel, 'edit') class PowerPanelEditView(generic.ObjectEditView): @@ -3648,16 +3531,6 @@ class VirtualDeviceContextListView(generic.ObjectListView): class VirtualDeviceContextView(generic.ObjectView): queryset = VirtualDeviceContext.objects.all() - def get_extra_context(self, request, instance): - interfaces_table = tables.InterfaceTable(instance.interfaces, user=request.user) - interfaces_table.configure(request) - interfaces_table.columns.hide('device') - - return { - 'interfaces_table': interfaces_table, - 'interface_count': instance.interfaces.count(), - } - @register_model_view(VirtualDeviceContext, 'edit') class VirtualDeviceContextEditView(generic.ObjectEditView): diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 130014f3f..9741be66b 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -5,11 +5,9 @@ from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.translation import gettext as _ -from circuits.models import Provider, Circuit -from circuits.tables import ProviderTable +from circuits.models import Provider from dcim.filtersets import InterfaceFilterSet from dcim.models import Interface, Site, Device -from dcim.tables import SiteTable from netbox.views import generic from utilities.utils import count_related from utilities.views import ViewTab, register_model_view @@ -167,17 +165,6 @@ class RIRListView(generic.ObjectListView): class RIRView(generic.ObjectView): queryset = RIR.objects.all() - def get_extra_context(self, request, instance): - aggregates = Aggregate.objects.restrict(request.user, 'view').filter(rir=instance).annotate( - child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ()) - ) - aggregates_table = tables.AggregateTable(aggregates, user=request.user, exclude=('rir', 'utilization')) - aggregates_table.configure(request) - - return { - 'aggregates_table': aggregates_table, - } - @register_model_view(RIR, 'edit') class RIREditView(generic.ObjectEditView): @@ -232,22 +219,11 @@ class ASNView(generic.ObjectView): queryset = ASN.objects.all() def get_extra_context(self, request, instance): - # Gather assigned Sites sites = instance.sites.restrict(request.user, 'view') - sites_table = SiteTable(sites, user=request.user) - sites_table.configure(request) - - # Gather assigned Providers - providers = instance.providers.restrict(request.user, 'view').annotate( - count_circuits=count_related(Circuit, 'provider') - ) - providers_table = ProviderTable(providers, user=request.user) - providers_table.configure(request) + providers = instance.providers.restrict(request.user, 'view') return { - 'sites_table': sites_table, 'sites_count': sites.count(), - 'providers_table': providers_table, 'providers_count': providers.count(), } @@ -392,18 +368,6 @@ class RoleListView(generic.ObjectListView): class RoleView(generic.ObjectView): queryset = Role.objects.all() - def get_extra_context(self, request, instance): - prefixes = Prefix.objects.restrict(request.user, 'view').filter( - role=instance - ) - - prefixes_table = tables.PrefixTable(prefixes, user=request.user, exclude=('role', 'utilization')) - prefixes_table.configure(request) - - return { - 'prefixes_table': prefixes_table, - } - @register_model_view(Role, 'edit') class RoleEditView(generic.ObjectEditView): @@ -750,7 +714,6 @@ class IPAddressView(generic.ObjectView): return { 'parent_prefixes_table': parent_prefixes_table, 'duplicate_ips_table': duplicate_ips_table, - 'more_duplicate_ips': duplicate_ips.count() > 10, 'related_ips_table': related_ips_table, 'services': services, } @@ -888,17 +851,9 @@ class VLANGroupView(generic.ObjectView): vlans_table.columns.show('pk') vlans_table.configure(request) - # Compile permissions list for rendering the object table - permissions = { - 'add': request.user.has_perm('ipam.add_vlan'), - 'change': request.user.has_perm('ipam.change_vlan'), - 'delete': request.user.has_perm('ipam.delete_vlan'), - } - return { 'vlans_count': vlans_count, 'vlans_table': vlans_table, - 'permissions': permissions, } @@ -954,11 +909,6 @@ class FHRPGroupView(generic.ObjectView): queryset = FHRPGroup.objects.all() def get_extra_context(self, request, instance): - # Get assigned IP addresses - ipaddress_table = tables.AssignedIPAddressesTable( - data=instance.ip_addresses.restrict(request.user, 'view'), - orderable=False - ) # Get assigned interfaces members_table = tables.FHRPGroupAssignmentTable( @@ -968,7 +918,6 @@ class FHRPGroupView(generic.ObjectView): members_table.columns.hide('group') return { - 'ipaddress_table': ipaddress_table, 'members_table': members_table, 'member_count': FHRPGroupAssignment.objects.filter(group=instance).count(), } @@ -1250,10 +1199,6 @@ class L2VPNView(generic.ObjectView): queryset = L2VPN.objects.all() def get_extra_context(self, request, instance): - terminations = L2VPNTermination.objects.restrict(request.user, 'view').filter(l2vpn=instance) - terminations_table = tables.L2VPNTerminationTable(terminations, user=request.user, exclude=('l2vpn', )) - terminations_table.configure(request) - import_targets_table = tables.RouteTargetTable( instance.import_targets.prefetch_related('tenant'), orderable=False @@ -1264,7 +1209,6 @@ class L2VPNView(generic.ObjectView): ) return { - 'terminations_table': terminations_table, 'import_targets_table': import_targets_table, 'export_targets_table': export_targets_table, } diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 3a2e71084..4060f0e48 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -4,6 +4,8 @@ from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldDoesNotExist from django.db.models.fields.related import RelatedField +from django.urls import reverse +from django.urls.exceptions import NoReverseMatch from django.utils.safestring import mark_safe from django.utils.translation import gettext as _ from django_tables2.data import TableQuerysetData @@ -12,7 +14,7 @@ from extras.models import CustomField, CustomLink from extras.choices import CustomFieldVisibilityChoices from netbox.tables import columns from utilities.paginator import EnhancedPaginator, get_paginate_count -from utilities.utils import highlight_string, title +from utilities.utils import get_viewname, highlight_string, title __all__ = ( 'BaseTable', @@ -197,6 +199,19 @@ class NetBoxTable(BaseTable): super().__init__(*args, extra_columns=extra_columns, **kwargs) + @property + def htmx_url(self): + """ + Return the base HTML request URL for embedded tables. + """ + if getattr(self, 'embedded', False): + viewname = get_viewname(self._meta.model, action='list') + try: + return reverse(viewname) + except NoReverseMatch: + pass + return '' + class SearchTable(tables.Table): object_type = columns.ContentTypeColumn( diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 6060475d8..325d10338 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -20,7 +20,7 @@ from utilities.choices import ImportFormatChoices from utilities.error_handlers import handle_protectederror from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation from utilities.forms import BulkRenameForm, ConfirmationForm, ImportForm, restrict_form_fields -from utilities.htmx import is_htmx +from utilities.htmx import is_embedded, is_htmx from utilities.permissions import get_permission_for_model from utilities.views import GetReturnURLMixin from .base import BaseMultiObjectView @@ -161,6 +161,11 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin): # If this is an HTMX request, return only the rendered table HTML if is_htmx(request): + if is_embedded(request): + table.embedded = True + # Hide selection checkboxes + if 'pk' in table.base_columns: + table.columns.hide('pk') return render(request, 'htmx/table.html', { 'table': table, }) diff --git a/netbox/templates/circuits/circuittype.html b/netbox/templates/circuits/circuittype.html index c2ab235e4..4cefecc87 100644 --- a/netbox/templates/circuits/circuittype.html +++ b/netbox/templates/circuits/circuittype.html @@ -31,7 +31,7 @@ Circuits - {{ circuits_table.rows|length }} + {{ object.circuits.count }} @@ -49,10 +49,10 @@
    Circuits
    -
    - {% render_table circuits_table 'inc/table.html' %} - {% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %} -
    +
    {% plugin_full_width_page object %}
    diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index 51f911350..8cd7e59fb 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -40,7 +40,7 @@ Circuits - {{ circuits_table.rows|length }} + {{ object.circuits.count }} @@ -60,10 +60,10 @@
    Circuits
    -
    - {% render_table circuits_table 'inc/table.html' %} - {% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %} -
    +
    {% plugin_full_width_page object %}
    diff --git a/netbox/templates/circuits/providernetwork.html b/netbox/templates/circuits/providernetwork.html index 4987722a5..29c31ab47 100644 --- a/netbox/templates/circuits/providernetwork.html +++ b/netbox/templates/circuits/providernetwork.html @@ -50,10 +50,10 @@
    Circuits
    -
    - {% render_table circuits_table 'inc/table.html' %} - {% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %} -
    +
    {% plugin_full_width_page object %}
    diff --git a/netbox/templates/dcim/connections_list.html b/netbox/templates/dcim/connections_list.html index ef8bef828..0d67dcaf0 100644 --- a/netbox/templates/dcim/connections_list.html +++ b/netbox/templates/dcim/connections_list.html @@ -12,7 +12,7 @@
    {% include 'inc/table_controls_htmx.html' %}
    -
    +
    {% include 'htmx/table.html' %}
    diff --git a/netbox/templates/dcim/device/consoleports.html b/netbox/templates/dcim/device/consoleports.html index 1f7cd037e..ccd12f61c 100644 --- a/netbox/templates/dcim/device/consoleports.html +++ b/netbox/templates/dcim/device/consoleports.html @@ -10,7 +10,7 @@ {% csrf_token %}
    -
    +
    {% include 'htmx/table.html' %}
    diff --git a/netbox/templates/dcim/device/consoleserverports.html b/netbox/templates/dcim/device/consoleserverports.html index 259a072b4..43396651d 100644 --- a/netbox/templates/dcim/device/consoleserverports.html +++ b/netbox/templates/dcim/device/consoleserverports.html @@ -10,7 +10,7 @@ {% csrf_token %}
    -
    +
    {% include 'htmx/table.html' %}
    diff --git a/netbox/templates/dcim/device/devicebays.html b/netbox/templates/dcim/device/devicebays.html index 5081b752b..9453b9a59 100644 --- a/netbox/templates/dcim/device/devicebays.html +++ b/netbox/templates/dcim/device/devicebays.html @@ -10,7 +10,7 @@ {% csrf_token %}
    -
    +
    {% include 'htmx/table.html' %}
    diff --git a/netbox/templates/dcim/device/frontports.html b/netbox/templates/dcim/device/frontports.html index 044337d00..dd0767d95 100644 --- a/netbox/templates/dcim/device/frontports.html +++ b/netbox/templates/dcim/device/frontports.html @@ -10,7 +10,7 @@ {% csrf_token %}
    -
    +
    {% include 'htmx/table.html' %}
    diff --git a/netbox/templates/dcim/device/interfaces.html b/netbox/templates/dcim/device/interfaces.html index 9de486a6f..c0e9a38b6 100644 --- a/netbox/templates/dcim/device/interfaces.html +++ b/netbox/templates/dcim/device/interfaces.html @@ -10,7 +10,7 @@ {% csrf_token %}
    -
    +
    {% include 'htmx/table.html' %}
    diff --git a/netbox/templates/dcim/device/inventory.html b/netbox/templates/dcim/device/inventory.html index 065fd92f6..9e11031ec 100644 --- a/netbox/templates/dcim/device/inventory.html +++ b/netbox/templates/dcim/device/inventory.html @@ -10,7 +10,7 @@ {% csrf_token %}
    -
    +
    {% include 'htmx/table.html' %}
    diff --git a/netbox/templates/dcim/device/modulebays.html b/netbox/templates/dcim/device/modulebays.html index 6358a3815..7f0aacf1f 100644 --- a/netbox/templates/dcim/device/modulebays.html +++ b/netbox/templates/dcim/device/modulebays.html @@ -10,7 +10,7 @@ {% csrf_token %}
    -
    +
    {% include 'htmx/table.html' %}
    diff --git a/netbox/templates/dcim/device/poweroutlets.html b/netbox/templates/dcim/device/poweroutlets.html index 35a9795d5..66b21b7af 100644 --- a/netbox/templates/dcim/device/poweroutlets.html +++ b/netbox/templates/dcim/device/poweroutlets.html @@ -10,7 +10,7 @@ {% csrf_token %}
    -
    +
    {% include 'htmx/table.html' %}
    diff --git a/netbox/templates/dcim/device/powerports.html b/netbox/templates/dcim/device/powerports.html index 69485c985..d9e1e121a 100644 --- a/netbox/templates/dcim/device/powerports.html +++ b/netbox/templates/dcim/device/powerports.html @@ -10,7 +10,7 @@ {% csrf_token %}
    -
    +
    {% include 'htmx/table.html' %}
    diff --git a/netbox/templates/dcim/device/rearports.html b/netbox/templates/dcim/device/rearports.html index 109e195dc..ce194cc78 100644 --- a/netbox/templates/dcim/device/rearports.html +++ b/netbox/templates/dcim/device/rearports.html @@ -10,7 +10,7 @@ {% csrf_token %}
    -
    +
    {% include 'htmx/table.html' %}
    diff --git a/netbox/templates/dcim/devicerole.html b/netbox/templates/dcim/devicerole.html index 6724333d9..2e0794582 100644 --- a/netbox/templates/dcim/devicerole.html +++ b/netbox/templates/dcim/devicerole.html @@ -45,14 +45,14 @@ Devices - {{ device_count }} + {{ object.devices.count }} Virtual Machines {% if object.vm_role %} - {{ virtualmachine_count }} + {{ object.virtual_machines.count }} {% else %} {{ ''|placeholder }} {% endif %} diff --git a/netbox/templates/dcim/devicetype/component_templates.html b/netbox/templates/dcim/devicetype/component_templates.html index 002a2044b..ca552a555 100644 --- a/netbox/templates/dcim/devicetype/component_templates.html +++ b/netbox/templates/dcim/devicetype/component_templates.html @@ -8,7 +8,7 @@ {% csrf_token %}
    {{ title }}
    -
    +
    {% include 'htmx/table.html' %}
    + {% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/contacts.html' %} {% plugin_right_page object %} @@ -74,13 +57,6 @@
    -
    -
    Device Types
    -
    -
    {% plugin_full_width_page object %}
    diff --git a/netbox/templates/dcim/platform.html b/netbox/templates/dcim/platform.html index f134ac649..17a313d82 100644 --- a/netbox/templates/dcim/platform.html +++ b/netbox/templates/dcim/platform.html @@ -43,46 +43,26 @@ NAPALM Driver {{ object.napalm_driver|placeholder }} - - Devices - - {{ device_count }} - - - - Virtual Machines - - {{ virtualmachine_count }} - -
    {% include 'inc/panels/tags.html' %} - {% plugin_left_page object %} -
    -
    -
    - NAPALM Arguments -
    +
    NAPALM Arguments
    {{ object.napalm_args|json }}
    + {% plugin_left_page object %} +
    +
    + {% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
    -
    -
    Devices
    -
    -
    {% plugin_full_width_page object %}
    diff --git a/netbox/templates/dcim/rackrole.html b/netbox/templates/dcim/rackrole.html index 0f229e910..2d2945025 100644 --- a/netbox/templates/dcim/rackrole.html +++ b/netbox/templates/dcim/rackrole.html @@ -34,12 +34,6 @@   - - Racks - - {{ object.racks.count }} - -
    @@ -47,19 +41,13 @@ {% plugin_left_page object %}
    + {% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
    -
    -
    Racks
    -
    -
    {% plugin_full_width_page object %}
    diff --git a/netbox/templates/ipam/rir.html b/netbox/templates/ipam/rir.html index a0355b99c..35b3c6b06 100644 --- a/netbox/templates/ipam/rir.html +++ b/netbox/templates/ipam/rir.html @@ -32,32 +32,20 @@ Private {% checkmark object.is_private %} - - Aggregates - - {{ object.aggregates.count }} - -
    + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
    - {% include 'inc/panels/tags.html' %} + {% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
    -
    -
    Aggregates
    -
    -
    {% plugin_full_width_page object %}
    diff --git a/netbox/templates/ipam/role.html b/netbox/templates/ipam/role.html index 1018824e9..12b73c1a9 100644 --- a/netbox/templates/ipam/role.html +++ b/netbox/templates/ipam/role.html @@ -32,44 +32,20 @@ Weight {{ object.weight }} - - Prefixes - - {{ object.prefixes.count }} - - - - IP Ranges - - {{ object.ip_ranges.count }} - - - - VLANs - - {{ object.vlans.count }} - -
    + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
    - {% include 'inc/panels/tags.html' %} + {% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
    -
    -
    Prefixes
    -
    -
    {% plugin_full_width_page object %}
    diff --git a/netbox/templates/ipam/vlangroup.html b/netbox/templates/ipam/vlangroup.html index 822b4a046..2917536be 100644 --- a/netbox/templates/ipam/vlangroup.html +++ b/netbox/templates/ipam/vlangroup.html @@ -42,12 +42,6 @@ Permitted VIDs {{ object.min_vid }} - {{ object.max_vid }} - - VLANs - - {{ vlans_count }} - - @@ -55,6 +49,7 @@ {% plugin_left_page object %}
    + {% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
    diff --git a/netbox/templates/virtualization/clustergroup.html b/netbox/templates/virtualization/clustergroup.html index 7d7d5a677..510433068 100644 --- a/netbox/templates/virtualization/clustergroup.html +++ b/netbox/templates/virtualization/clustergroup.html @@ -28,12 +28,6 @@ Description {{ object.description|placeholder }} - - Clusters - - {{ object.clusters.count }} - - @@ -41,6 +35,7 @@ {% plugin_left_page object %}
    + {% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/contacts.html' %} {% plugin_right_page object %} @@ -48,13 +43,6 @@
    -
    -
    Clusters
    -
    -
    {% plugin_full_width_page object %}
    diff --git a/netbox/templates/virtualization/clustertype.html b/netbox/templates/virtualization/clustertype.html index 5a5379160..2881fc1da 100644 --- a/netbox/templates/virtualization/clustertype.html +++ b/netbox/templates/virtualization/clustertype.html @@ -41,19 +41,13 @@ {% plugin_left_page object %}
    + {% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
    -
    -
    Clusters
    -
    -
    {% plugin_full_width_page object %}
    diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index bbb46face..d7a4856f2 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -10,7 +10,7 @@ from dcim.models import Device from dcim.tables import DeviceTable from extras.views import ObjectConfigContextView from ipam.models import IPAddress, Service -from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable +from ipam.tables import InterfaceVLANTable from netbox.views import generic from utilities.utils import count_related from utilities.views import ViewTab, register_model_view @@ -36,17 +36,12 @@ class ClusterTypeView(generic.ObjectView): queryset = ClusterType.objects.all() def get_extra_context(self, request, instance): - clusters = Cluster.objects.restrict(request.user, 'view').filter( - type=instance - ).annotate( - device_count=count_related(Device, 'cluster'), - vm_count=count_related(VirtualMachine, 'cluster') + related_models = ( + (Cluster.objects.restrict(request.user, 'view').filter(type=instance), 'type_id'), ) - clusters_table = tables.ClusterTable(clusters, user=request.user, exclude=('type',)) - clusters_table.configure(request) return { - 'clusters_table': clusters_table, + 'related_models': related_models, } @@ -100,6 +95,15 @@ class ClusterGroupListView(generic.ObjectListView): class ClusterGroupView(generic.ObjectView): queryset = ClusterGroup.objects.all() + def get_extra_context(self, request, instance): + related_models = ( + (Cluster.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'), + ) + + return { + 'related_models': related_models, + } + @register_model_view(ClusterGroup, 'edit') class ClusterGroupEditView(generic.ObjectEditView): From 48e5b395b276f0fced2748125d24b2ad7d1e89d7 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 25 Jan 2023 16:20:52 -0500 Subject: [PATCH 012/174] Standardize linking to related objects in tables --- netbox/circuits/tables/circuits.py | 4 +++- netbox/dcim/tables/devices.py | 3 +++ netbox/dcim/tables/devicetypes.py | 1 - netbox/dcim/tables/racks.py | 6 +++++- netbox/dcim/views.py | 10 ++++++++-- 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/netbox/circuits/tables/circuits.py b/netbox/circuits/tables/circuits.py index 477f9c1ab..b3f62d5fc 100644 --- a/netbox/circuits/tables/circuits.py +++ b/netbox/circuits/tables/circuits.py @@ -28,7 +28,9 @@ class CircuitTypeTable(NetBoxTable): tags = columns.TagColumn( url_name='circuits:circuittype_list' ) - circuit_count = tables.Column( + circuit_count = columns.LinkedCountColumn( + viewname='circuits:circuit_list', + url_params={'type_id': 'pk'}, verbose_name='Circuits' ) diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 730309156..904e96b83 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -107,6 +107,9 @@ class PlatformTable(NetBoxTable): name = tables.Column( linkify=True ) + manufacturer = tables.Column( + linkify=True + ) device_count = columns.LinkedCountColumn( viewname='dcim:device_list', url_params={'platform_id': 'pk'}, diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index 9bcc9c47f..dff697588 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/netbox/dcim/tables/devicetypes.py @@ -49,7 +49,6 @@ class ManufacturerTable(ContactsColumnMixin, NetBoxTable): url_params={'manufacturer_id': 'pk'}, verbose_name='Platforms' ) - slug = tables.Column() tags = columns.TagColumn( url_name='dcim:manufacturer_list' ) diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index cb9aae6fd..657754017 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -19,7 +19,11 @@ __all__ = ( class RackRoleTable(NetBoxTable): name = tables.Column(linkify=True) - rack_count = tables.Column(verbose_name='Racks') + rack_count = columns.LinkedCountColumn( + viewname='dcim:rack_list', + url_params={'role_id': 'pk'}, + verbose_name='Racks' + ) color = columns.ColorColumn() tags = columns.TagColumn( url_name='dcim:rackrole_list' diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 693b7c2d2..4683a6084 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -843,7 +843,10 @@ class ManufacturerBulkImportView(generic.BulkImportView): class ManufacturerBulkEditView(generic.BulkEditView): queryset = Manufacturer.objects.annotate( - devicetype_count=count_related(DeviceType, 'manufacturer') + devicetype_count=count_related(DeviceType, 'manufacturer'), + moduletype_count=count_related(ModuleType, 'manufacturer'), + inventoryitem_count=count_related(InventoryItem, 'manufacturer'), + platform_count=count_related(Platform, 'manufacturer') ) filterset = filtersets.ManufacturerFilterSet table = tables.ManufacturerTable @@ -852,7 +855,10 @@ class ManufacturerBulkEditView(generic.BulkEditView): class ManufacturerBulkDeleteView(generic.BulkDeleteView): queryset = Manufacturer.objects.annotate( - devicetype_count=count_related(DeviceType, 'manufacturer') + devicetype_count=count_related(DeviceType, 'manufacturer'), + moduletype_count=count_related(ModuleType, 'manufacturer'), + inventoryitem_count=count_related(InventoryItem, 'manufacturer'), + platform_count=count_related(Platform, 'manufacturer') ) table = tables.ManufacturerTable From 8f7c100e2210828dae80ea31878d24aedfb095d2 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 25 Jan 2023 17:27:57 -0500 Subject: [PATCH 013/174] Standard related object links across all models --- netbox/circuits/views.py | 21 ++++ netbox/dcim/views.py | 88 ++++++++++----- netbox/ipam/views.py | 19 ++-- netbox/templates/circuits/circuittype.html | 2 +- netbox/templates/circuits/provider.html | 9 +- .../templates/circuits/providernetwork.html | 5 +- netbox/templates/dcim/device.html | 26 +---- netbox/templates/dcim/devicetype.html | 7 +- netbox/templates/dcim/module.html | 100 +----------------- netbox/templates/dcim/moduletype.html | 11 +- netbox/templates/dcim/powerpanel.html | 11 +- netbox/templates/dcim/rack.html | 41 +------ .../templates/dcim/virtualdevicecontext.html | 3 +- netbox/templates/ipam/asn.html | 37 +------ netbox/templates/ipam/vrf.html | 15 +-- netbox/templates/virtualization/cluster.html | 4 - 16 files changed, 124 insertions(+), 275 deletions(-) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 4806fd954..228b70bb1 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -29,6 +29,15 @@ class ProviderListView(generic.ObjectListView): class ProviderView(generic.ObjectView): queryset = Provider.objects.all() + def get_extra_context(self, request, instance): + related_models = ( + (Circuit.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'), + ) + + return { + 'related_models': related_models, + } + @register_model_view(Provider, 'edit') class ProviderEditView(generic.ObjectEditView): @@ -79,6 +88,18 @@ class ProviderNetworkListView(generic.ObjectListView): class ProviderNetworkView(generic.ObjectView): queryset = ProviderNetwork.objects.all() + def get_extra_context(self, request, instance): + related_models = ( + ( + Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance), + 'providernetwork_id', + ), + ) + + return { + 'related_models': related_models, + } + @register_model_view(ProviderNetwork, 'edit') class ProviderNetworkEditView(generic.ObjectEditView): diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 4683a6084..741194712 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -21,9 +21,7 @@ from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.permissions import get_permission_for_model from utilities.utils import count_related from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view -from virtualization.filtersets import VirtualMachineFilterSet from virtualization.models import VirtualMachine -from virtualization.tables import VirtualMachineTable from . import filtersets, forms, tables from .choices import DeviceFaceChoices from .constants import NONCONNECTABLE_IFACE_TYPES @@ -359,24 +357,24 @@ class SiteView(generic.ObjectView): queryset = Site.objects.prefetch_related('tenant__group') def get_extra_context(self, request, instance): - related_models = [ + related_models = ( # DCIM - Location.objects.restrict(request.user, 'view').filter(site=instance), - Rack.objects.restrict(request.user, 'view').filter(site=instance), - Device.objects.restrict(request.user, 'view').filter(site=instance), + (Location.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'), + (Rack.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'), + (Device.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'), # Virtualization - VirtualMachine.objects.restrict(request.user, 'view').filter(cluster__site=instance), + (VirtualMachine.objects.restrict(request.user, 'view').filter(cluster__site=instance), 'site_id'), # IPAM - Prefix.objects.restrict(request.user, 'view').filter(site=instance), - ASN.objects.restrict(request.user, 'view').filter(sites=instance), - VLANGroup.objects.restrict(request.user, 'view').filter( + (Prefix.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'), + (ASN.objects.restrict(request.user, 'view').filter(sites=instance), 'site_id'), + (VLANGroup.objects.restrict(request.user, 'view').filter( scope_type=ContentType.objects.get_for_model(Site), scope_id=instance.pk - ), - VLAN.objects.restrict(request.user, 'view').filter(site=instance), + ), 'site_id'), + (VLAN.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'), # Circuits - Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), - ] + (Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), 'site_id'), + ) locations = Location.objects.add_related_count( Location.objects.all(), @@ -658,6 +656,11 @@ class RackView(generic.ObjectView): queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'location', 'role') def get_extra_context(self, request, instance): + related_models = ( + (Device.objects.restrict(request.user, 'view').filter(rack=instance), 'rack_id'), + (PowerFeed.objects.restrict(request.user).filter(rack=instance), 'rack_id'), + ) + # Get 0U devices located within the rack nonracked_devices = Device.objects.filter( rack=instance, @@ -675,11 +678,6 @@ class RackView(generic.ObjectView): prev_rack = peer_racks.filter(_name__lt=instance._name).reverse().first() reservations = RackReservation.objects.restrict(request.user, 'view').filter(rack=instance) - power_feeds = PowerFeed.objects.restrict(request.user, 'view').filter(rack=instance).prefetch_related( - 'power_panel' - ) - - device_count = Device.objects.restrict(request.user, 'view').filter(rack=instance).count() # Determine any additional parameters to pass when embedding the rack elevations svg_extra = '&'.join([ @@ -687,9 +685,8 @@ class RackView(generic.ObjectView): ]) return { - 'device_count': device_count, + 'related_models': related_models, 'reservations': reservations, - 'power_feeds': power_feeds, 'nonracked_devices': nonracked_devices, 'next_rack': next_rack, 'prev_rack': prev_rack, @@ -881,10 +878,12 @@ class DeviceTypeView(generic.ObjectView): queryset = DeviceType.objects.all() def get_extra_context(self, request, instance): - instance_count = Device.objects.restrict(request.user).filter(device_type=instance).count() + related_models = ( + (Device.objects.restrict(request.user).filter(device_type=instance), 'device_type_id'), + ) return { - 'instance_count': instance_count, + 'related_models': related_models, } @@ -1119,10 +1118,12 @@ class ModuleTypeView(generic.ObjectView): queryset = ModuleType.objects.all() def get_extra_context(self, request, instance): - instance_count = Module.objects.restrict(request.user).filter(module_type=instance).count() + related_models = ( + (Module.objects.restrict(request.user).filter(module_type=instance), 'module_type_id'), + ) return { - 'instance_count': instance_count, + 'related_models': related_models, } @@ -1807,13 +1808,9 @@ class DeviceView(generic.ObjectView): vc_members = [] services = Service.objects.restrict(request.user, 'view').filter(device=instance) - vdcs = VirtualDeviceContext.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related( - 'tenant' - ) return { 'services': services, - 'vdcs': vdcs, 'vc_members': vc_members, 'svg_extra': f'highlight=id:{instance.pk}' } @@ -2114,6 +2111,21 @@ class ModuleListView(generic.ObjectListView): class ModuleView(generic.ObjectView): queryset = Module.objects.all() + def get_extra_context(self, request, instance): + related_models = ( + (Interface.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'), + (ConsolePort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'), + (ConsoleServerPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'), + (PowerPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'), + (PowerOutlet.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'), + (FrontPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'), + (RearPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'), + ) + + return { + 'related_models': related_models, + } + @register_model_view(Module, 'edit') class ModuleEditView(generic.ObjectEditView): @@ -3436,6 +3448,15 @@ class PowerPanelListView(generic.ObjectListView): class PowerPanelView(generic.ObjectView): queryset = PowerPanel.objects.all() + def get_extra_context(self, request, instance): + related_models = ( + (PowerFeed.objects.restrict(request.user).filter(power_panel=instance), 'power_panel_id'), + ) + + return { + 'related_models': related_models, + } + @register_model_view(PowerPanel, 'edit') class PowerPanelEditView(generic.ObjectEditView): @@ -3537,6 +3558,15 @@ class VirtualDeviceContextListView(generic.ObjectListView): class VirtualDeviceContextView(generic.ObjectView): queryset = VirtualDeviceContext.objects.all() + def get_extra_context(self, request, instance): + related_models = ( + (Interface.objects.restrict(request.user, 'view').filter(vdcs__in=[instance]), 'vdc_id'), + ) + + return { + 'related_models': related_models, + } + @register_model_view(VirtualDeviceContext, 'edit') class VirtualDeviceContextEditView(generic.ObjectEditView): diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index bcafacc45..b7cd72d67 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -37,8 +37,10 @@ class VRFView(generic.ObjectView): queryset = VRF.objects.all() def get_extra_context(self, request, instance): - prefix_count = Prefix.objects.restrict(request.user, 'view').filter(vrf=instance).count() - ipaddress_count = IPAddress.objects.restrict(request.user, 'view').filter(vrf=instance).count() + related_models = ( + (Prefix.objects.restrict(request.user, 'view').filter(vrf=instance), 'vrf_id'), + (IPAddress.objects.restrict(request.user, 'view').filter(vrf=instance), 'vrf_id'), + ) import_targets_table = tables.RouteTargetTable( instance.import_targets.all(), @@ -50,8 +52,7 @@ class VRFView(generic.ObjectView): ) return { - 'prefix_count': prefix_count, - 'ipaddress_count': ipaddress_count, + 'related_models': related_models, 'import_targets_table': import_targets_table, 'export_targets_table': export_targets_table, } @@ -228,12 +229,13 @@ class ASNView(generic.ObjectView): queryset = ASN.objects.all() def get_extra_context(self, request, instance): - sites = instance.sites.restrict(request.user, 'view') - providers = instance.providers.restrict(request.user, 'view') + related_models = ( + (Site.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'), + (Provider.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'), + ) return { - 'sites_count': sites.count(), - 'providers_count': providers.count(), + 'related_models': related_models, } @@ -868,7 +870,6 @@ class VLANGroupView(generic.ObjectView): Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user)), 'tenant', 'site', 'role', ).order_by('vid') - vlans_count = vlans.count() vlans = add_available_vlans(vlans, vlan_group=instance) vlans_table = tables.VLANTable(vlans, user=request.user, exclude=('group',)) diff --git a/netbox/templates/circuits/circuittype.html b/netbox/templates/circuits/circuittype.html index 3d5ba5a6c..39c1f1541 100644 --- a/netbox/templates/circuits/circuittype.html +++ b/netbox/templates/circuits/circuittype.html @@ -35,7 +35,7 @@ {% plugin_left_page object %}
    - {% include 'inc/panels/related_objects.html' %} + {% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
    diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index 8cd7e59fb..3973d2867 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -37,21 +37,16 @@ Description {{ object.description|placeholder }} - - Circuits - - {{ object.circuits.count }} - - {% include 'inc/panels/tags.html' %} + {% include 'inc/panels/comments.html' %} {% plugin_left_page object %}
    + {% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/comments.html' %} {% include 'inc/panels/contacts.html' %} {% plugin_right_page object %}
    diff --git a/netbox/templates/circuits/providernetwork.html b/netbox/templates/circuits/providernetwork.html index 29c31ab47..f478058ec 100644 --- a/netbox/templates/circuits/providernetwork.html +++ b/netbox/templates/circuits/providernetwork.html @@ -37,12 +37,13 @@ + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
    - {% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' %} + {% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/comments.html' %} + {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
    diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index aa31db97c..6a0d00d6d 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -157,28 +157,10 @@ {% include 'inc/panels/comments.html' %}
    Virtual Device Contexts
    -
    - {% if vdcs %} - - - - - - - - {% for vdc in vdcs %} - - - - - - - {% endfor %} -
    NameStatusIdentifierTenant
    {{ vdc|linkify }}{% badge vdc.get_status_display bg_color=vdc.get_status_color %}{{ vdc.identifier|placeholder }}{{ vdc.tenant|linkify|placeholder }}
    - {% else %} -
    None
    - {% endif %} -
    +
    {% if perms.dcim.add_virtualdevicecontext %}
    + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
    + {% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' %} {% include 'inc/panels/comments.html' %} {% plugin_right_page object %}
    diff --git a/netbox/templates/dcim/module.html b/netbox/templates/dcim/module.html index 78d5a1a05..e46bc65f5 100644 --- a/netbox/templates/dcim/module.html +++ b/netbox/templates/dcim/module.html @@ -81,104 +81,14 @@ - {% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/tags.html' %} {% include 'inc/panels/comments.html' %} {% plugin_left_page object %} - -
    -
    -
    Components
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Interfaces - {% with component_count=object.interfaces.count %} - {% if component_count %} - {{ component_count }} - {% else %} - {{ ''|placeholder }} - {% endif %} - {% endwith %} -
    Console Ports - {% with component_count=object.consoleports.count %} - {% if component_count %} - {{ component_count }} - {% else %} - {{ ''|placeholder }} - {% endif %} - {% endwith %} -
    Console Server Ports - {% with component_count=object.consoleserverports.count %} - {% if component_count %} - {{ component_count }} - {% else %} - {{ ''|placeholder }} - {% endif %} - {% endwith %} -
    Power Ports - {% with component_count=object.powerports.count %} - {% if component_count %} - {{ component_count }} - {% else %} - {{ ''|placeholder }} - {% endif %} - {% endwith %} -
    Power Outlets - {% with component_count=object.poweroutlets.count %} - {% if component_count %} - {{ component_count }} - {% else %} - {{ ''|placeholder }} - {% endif %} - {% endwith %} -
    Front Ports - {% with component_count=object.frontports.count %} - {% if component_count %} - {{ component_count }} - {% else %} - {{ ''|placeholder }} - {% endif %} - {% endwith %} -
    Rear Ports - {% with component_count=object.rearports.count %} - {% if component_count %} - {{ component_count }} - {% else %} - {{ ''|placeholder }} - {% endif %} - {% endwith %} -
    -
    -
    - {% plugin_right_page object %} +
    +
    + {% include 'inc/panels/related_objects.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% plugin_right_page object %}
    diff --git a/netbox/templates/dcim/moduletype.html b/netbox/templates/dcim/moduletype.html index fd0148c2f..8929678b7 100644 --- a/netbox/templates/dcim/moduletype.html +++ b/netbox/templates/dcim/moduletype.html @@ -36,19 +36,16 @@ {% endif %} - - Instances - {{ instance_count }} -
    - {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' %} + {% include 'inc/panels/comments.html' %} {% plugin_left_page object %}
    - {% include 'inc/panels/tags.html' %} - {% include 'inc/panels/comments.html' %} + {% include 'inc/panels/related_objects.html' %} + {% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/image_attachments.html' %} {% plugin_right_page object %}
    diff --git a/netbox/templates/dcim/powerpanel.html b/netbox/templates/dcim/powerpanel.html index c73e33b13..af08f3023 100644 --- a/netbox/templates/dcim/powerpanel.html +++ b/netbox/templates/dcim/powerpanel.html @@ -38,11 +38,12 @@ {% plugin_left_page object %}
    - {% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/contacts.html' %} - {% include 'inc/panels/image_attachments.html' %} - {% plugin_right_page object %} -
    + {% include 'inc/panels/related_objects.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/contacts.html' %} + {% include 'inc/panels/image_attachments.html' %} + {% plugin_right_page object %} +
    diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index e2cb1597e..c155f2796 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -90,12 +90,6 @@ Asset Tag {{ object.asset_tag|placeholder }} - - Devices - - {{ device_count }} - - Space Utilization {% utilization_graph object.get_utilization %} @@ -192,40 +186,6 @@ {% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/tags.html' %} {% include 'inc/panels/comments.html' %} - {% if power_feeds %} -
    -
    - Power Feeds -
    -
    - - - - - - - - - {% for powerfeed in power_feeds %} - - - - - - {% with power_port=powerfeed.connected_endpoints.0 %} - {% if power_port %} - - {% else %} - - {% endif %} - {% endwith %} - - {% endfor %} -
    PanelFeedStatusTypeUtilization
    {{ powerfeed.power_panel|linkify }}{{ powerfeed|linkify }}{% badge powerfeed.get_status_display bg_color=powerfeed.get_status_color %}{% badge powerfeed.get_type_display bg_color=powerfeed.get_type_color %}{% utilization_graph power_port.get_power_draw.allocated|percentage:powerfeed.available_power %}N/A
    -
    -
    - {% endif %} - {% include 'inc/panels/image_attachments.html' %}
    @@ -300,6 +260,7 @@
    + {% include 'inc/panels/related_objects.html' %} {% include 'dcim/inc/nonracked_devices.html' %} {% include 'inc/panels/contacts.html' %} {% plugin_right_page object %} diff --git a/netbox/templates/dcim/virtualdevicecontext.html b/netbox/templates/dcim/virtualdevicecontext.html index ee30db19e..d6e3e0c63 100644 --- a/netbox/templates/dcim/virtualdevicecontext.html +++ b/netbox/templates/dcim/virtualdevicecontext.html @@ -59,10 +59,11 @@ {% plugin_left_page object %} + {% include 'inc/panels/tags.html' %}
    + {% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/comments.html' %} - {% include 'inc/panels/tags.html' %} {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
    diff --git a/netbox/templates/ipam/asn.html b/netbox/templates/ipam/asn.html index 26903b71c..a54a0aee5 100644 --- a/netbox/templates/ipam/asn.html +++ b/netbox/templates/ipam/asn.html @@ -39,54 +39,21 @@ Description {{ object.description|placeholder }} - - Sites - - {% if sites_count %} - {{ sites_count }} - {% else %} - {{ ''|placeholder }} - {% endif %} - - - - Providers - - {% if providers_count %} - {{ providers_count }} - {% else %} - {{ ''|placeholder }} - {% endif %} - - {% plugin_left_page object %} + {% include 'inc/panels/tags.html' %}
    + {% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:asn_list' %} {% include 'inc/panels/comments.html' %} {% plugin_right_page object %}
    -
    -
    Sites
    -
    -
    -
    -
    Providers
    -
    -
    {% plugin_full_width_page object %}
    diff --git a/netbox/templates/ipam/vrf.html b/netbox/templates/ipam/vrf.html index b53862f9e..c365efae3 100644 --- a/netbox/templates/ipam/vrf.html +++ b/netbox/templates/ipam/vrf.html @@ -35,25 +35,14 @@ Description {{ object.description|placeholder }} - - Prefixes - - {{ prefix_count }} - - - - IP Addresses - - {{ ipaddress_count }} - - + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
    - {% include 'inc/panels/tags.html' %} + {% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/comments.html' %} {% plugin_right_page object %} diff --git a/netbox/templates/virtualization/cluster.html b/netbox/templates/virtualization/cluster.html index 5f34a82c5..3dfef108b 100644 --- a/netbox/templates/virtualization/cluster.html +++ b/netbox/templates/virtualization/cluster.html @@ -44,10 +44,6 @@ Site {{ object.site|linkify|placeholder }} - - Virtual Machines - {{ object.virtual_machines.count }} -
    From 0f6995e92a50799f6d2640f0b9dbb2cfe99ef962 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 25 Jan 2023 20:25:06 -0500 Subject: [PATCH 014/174] Use embedded table to show assigned services under object view --- netbox/dcim/views.py | 3 -- netbox/ipam/views.py | 18 ------- netbox/templates/dcim/device.html | 15 +++++- netbox/templates/inc/panels/services.html | 50 ------------------- netbox/templates/ipam/ipaddress.html | 15 ++++-- .../virtualization/virtualmachine.html | 15 +++++- netbox/virtualization/views.py | 27 +--------- 7 files changed, 39 insertions(+), 104 deletions(-) delete mode 100644 netbox/templates/inc/panels/services.html diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 741194712..35ab0ee20 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1807,10 +1807,7 @@ class DeviceView(generic.ObjectView): else: vc_members = [] - services = Service.objects.restrict(request.user, 'view').filter(device=instance) - return { - 'services': services, 'vc_members': vc_members, 'svg_extra': f'highlight=id:{instance.pk}' } diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index b7cd72d67..e3245ef39 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -716,28 +716,10 @@ class IPAddressView(generic.ObjectView): related_ips_table = tables.IPAddressTable(related_ips, orderable=False) related_ips_table.configure(request) - # Find services belonging to the IP - service_filter = Q(ipaddresses=instance) - - # Find services listening on all IPs on the assigned device/vm - try: - if instance.assigned_object and instance.assigned_object.parent_object: - parent_object = instance.assigned_object.parent_object - - if isinstance(parent_object, VirtualMachine): - service_filter |= (Q(virtual_machine=parent_object) & Q(ipaddresses=None)) - elif isinstance(parent_object, Device): - service_filter |= (Q(device=parent_object) & Q(ipaddresses=None)) - except AttributeError: - pass - - services = Service.objects.restrict(request.user, 'view').filter(service_filter) - return { 'parent_prefixes_table': parent_prefixes_table, 'duplicate_ips_table': duplicate_ips_table, 'related_ips_table': related_ips_table, - 'services': services, } diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 6a0d00d6d..3c2cc6299 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -282,7 +282,20 @@ {% endif %} - {% include 'inc/panels/services.html' %} +
    +
    Services
    +
    + {% if perms.ipam.add_service %} + + {% endif %} +
    {% include 'inc/panels/contacts.html' %} {% include 'inc/panels/image_attachments.html' %} {% if object.rack and object.position %} diff --git a/netbox/templates/inc/panels/services.html b/netbox/templates/inc/panels/services.html deleted file mode 100644 index b7109f497..000000000 --- a/netbox/templates/inc/panels/services.html +++ /dev/null @@ -1,50 +0,0 @@ -
    -
    Services
    -
    - {% if services %} - - {% for service in services %} - - - - - - - - - {% endfor %} -
    {{ service|linkify:"name" }}{{ service.get_protocol_display }}{{ service.port_list }} - {% for ip in service.ipaddresses.all %} - {{ ip.address.ip }}
    - {% empty %} - All IPs - {% endfor %} -
    {{ service.description }} - - - - {% if perms.ipam.change_service %} - - - - {% endif %} - {% if perms.ipam.delete_service %} - - - - {% endif %} -
    - {% else %} -
    None
    - {% endif %} -
    - {% if perms.ipam.add_service %} - {% with object|meta:"model_name" as object_type %} - - {% endwith %} - {% endif %} -
    diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index 74c1131ca..c649f1dad 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -117,14 +117,19 @@ {% include 'inc/panel_table.html' with table=duplicate_ips_table heading='Duplicate IPs' panel_class='danger' %} {% endif %} {% include 'inc/panel_table.html' with table=related_ips_table heading='Related IPs' %} - {% include 'inc/panels/services.html' %} +
    +
    Services
    +
    +
    {% plugin_right_page object %} -
    -
    - {% plugin_full_width_page object %} -
    +
    + {% plugin_full_width_page object %} +
    {% endblock %} diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 9b5708486..5098a2f8f 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -144,7 +144,20 @@ - {% include 'inc/panels/services.html' %} +
    +
    Services
    +
    + {% if perms.ipam.add_service %} + + {% endif %} +
    {% include 'inc/panels/contacts.html' %} {% plugin_right_page object %} diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index d7a4856f2..7feff18d5 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -327,32 +327,7 @@ class VirtualMachineListView(generic.ObjectListView): @register_model_view(VirtualMachine) class VirtualMachineView(generic.ObjectView): - queryset = VirtualMachine.objects.prefetch_related('tenant__group') - - def get_extra_context(self, request, instance): - # Interfaces - vminterfaces = VMInterface.objects.restrict(request.user, 'view').filter( - virtual_machine=instance - ).prefetch_related( - Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)) - ) - vminterface_table = tables.VirtualMachineVMInterfaceTable(vminterfaces, user=request.user, orderable=False) - if request.user.has_perm('virtualization.change_vminterface') or \ - request.user.has_perm('virtualization.delete_vminterface'): - vminterface_table.columns.show('pk') - - # Services - services = Service.objects.restrict(request.user, 'view').filter( - virtual_machine=instance - ).prefetch_related( - Prefetch('ipaddresses', queryset=IPAddress.objects.restrict(request.user)), - 'virtual_machine' - ) - - return { - 'vminterface_table': vminterface_table, - 'services': services, - } + queryset = VirtualMachine.objects.all() @register_model_view(VirtualMachine, 'interfaces') From 2525eefefdaf0c5de5b506e357f8f4de61b007cd Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 25 Jan 2023 20:45:30 -0500 Subject: [PATCH 015/174] Move rack reservations panel to separate tab --- netbox/dcim/views.py | 22 +++++- netbox/templates/dcim/rack.html | 73 +------------------- netbox/templates/dcim/rack/base.html | 23 ++++++ netbox/templates/dcim/rack/reservations.html | 43 ++++++++++++ 4 files changed, 86 insertions(+), 75 deletions(-) create mode 100644 netbox/templates/dcim/rack/base.html create mode 100644 netbox/templates/dcim/rack/reservations.html diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 35ab0ee20..095314e7b 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -677,8 +677,6 @@ class RackView(generic.ObjectView): next_rack = peer_racks.filter(_name__gt=instance._name).first() prev_rack = peer_racks.filter(_name__lt=instance._name).reverse().first() - reservations = RackReservation.objects.restrict(request.user, 'view').filter(rack=instance) - # Determine any additional parameters to pass when embedding the rack elevations svg_extra = '&'.join([ f'highlight=id:{pk}' for pk in request.GET.getlist('device') @@ -686,7 +684,6 @@ class RackView(generic.ObjectView): return { 'related_models': related_models, - 'reservations': reservations, 'nonracked_devices': nonracked_devices, 'next_rack': next_rack, 'prev_rack': prev_rack, @@ -694,6 +691,25 @@ class RackView(generic.ObjectView): } +@register_model_view(Rack, 'reservations') +class RackRackReservationsView(generic.ObjectChildrenView): + queryset = Rack.objects.all() + child_model = RackReservation + table = tables.RackReservationTable + filterset = filtersets.RackReservationFilterSet + template_name = 'dcim/rack/reservations.html' + tab = ViewTab( + label=_('Reservations'), + badge=lambda obj: obj.reservations.count(), + permission='dcim.view_rackreservation', + weight=510, + hide_if_empty=True + ) + + def get_children(self, request, parent): + return parent.reservations.restrict(request.user, 'view') + + @register_model_view(Rack, 'edit') class RackEditView(generic.ObjectEditView): queryset = Rack.objects.all() diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index c155f2796..9cb046b4e 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -1,31 +1,9 @@ -{% extends 'generic/object.html' %} +{% extends 'dcim/rack/base.html' %} {% load buttons %} {% load helpers %} {% load static %} {% load plugins %} -{% block title %}Rack {{ object }}{% endblock %} - -{% block breadcrumbs %} - {{ block.super }} - - {% if object.location %} - {% for location in object.location.get_ancestors %} - - {% endfor %} - - {% endif %} -{% endblock %} - -{% block extra_controls %} - - Previous - - - Next - -{% endblock %} - {% block content %}
    @@ -187,55 +165,6 @@ {% include 'inc/panels/tags.html' %} {% include 'inc/panels/comments.html' %} {% include 'inc/panels/image_attachments.html' %} -
    -
    - Reservations -
    -
    - {% if reservations %} - - - - - - - - {% for resv in reservations %} - - - - - - - {% endfor %} -
    UnitsTenantDescription
    {{ resv|linkify:"unit_list" }}{{ resv.tenant|linkify|placeholder }} - {{ resv.description }}
    - {{ resv.user }} · {{ resv.created|annotated_date }} -
    - {% if perms.dcim.change_rackreservation %} - - - - {% endif %} - {% if perms.dcim.delete_rackreservation %} - - - - {% endif %} -
    - {% else %} -
    None
    - {% endif %} -
    - {% if perms.dcim.add_rackreservation %} - - {% endif %} -
    {% plugin_left_page object %}
    diff --git a/netbox/templates/dcim/rack/base.html b/netbox/templates/dcim/rack/base.html new file mode 100644 index 000000000..8ac7b70d0 --- /dev/null +++ b/netbox/templates/dcim/rack/base.html @@ -0,0 +1,23 @@ +{% extends 'generic/object.html' %} + +{% block title %}Rack {{ object }}{% endblock %} + +{% block breadcrumbs %} + {{ block.super }} + + {% if object.location %} + {% for location in object.location.get_ancestors %} + + {% endfor %} + + {% endif %} +{% endblock %} + +{% block extra_controls %} + + Previous + + + Next + +{% endblock %} diff --git a/netbox/templates/dcim/rack/reservations.html b/netbox/templates/dcim/rack/reservations.html new file mode 100644 index 000000000..fb357e592 --- /dev/null +++ b/netbox/templates/dcim/rack/reservations.html @@ -0,0 +1,43 @@ +{% extends 'dcim/rack/base.html' %} +{% load helpers %} + +{% block content %} + {% include 'inc/table_controls_htmx.html' with table_modal="RackReservationTable_config" %} + +
    + {% csrf_token %} + +
    +
    + {% include 'htmx/table.html' %} +
    +
    + +
    +
    + {% if 'bulk_edit' in actions %} + + {% endif %} + {% if 'bulk_delete' in actions %} + + {% endif %} +
    + {% if perms.dcim.add_rackreservation %} + + {% endif %} +
    +
    +{% endblock %} + +{% block modals %} + {{ block.super }} + {% table_config_form table %} +{% endblock modals %} From 6e264562ee06a72be7b73d4e30edb45c684f39d8 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 25 Jan 2023 21:09:34 -0500 Subject: [PATCH 016/174] Use embedded tables for importing/export VRFs & L2VPNs under route target view --- netbox/ipam/views.py | 15 -------- netbox/templates/ipam/routetarget.html | 48 ++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index e3245ef39..c80ca7d74 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -103,21 +103,6 @@ class RouteTargetListView(generic.ObjectListView): class RouteTargetView(generic.ObjectView): queryset = RouteTarget.objects.all() - def get_extra_context(self, request, instance): - importing_vrfs_table = tables.VRFTable( - instance.importing_vrfs.all(), - orderable=False - ) - exporting_vrfs_table = tables.VRFTable( - instance.exporting_vrfs.all(), - orderable=False - ) - - return { - 'importing_vrfs_table': importing_vrfs_table, - 'exporting_vrfs_table': exporting_vrfs_table, - } - @register_model_view(RouteTarget, 'edit') class RouteTargetEditView(generic.ObjectEditView): diff --git a/netbox/templates/ipam/routetarget.html b/netbox/templates/ipam/routetarget.html index ea7a98c97..fae9866b5 100644 --- a/netbox/templates/ipam/routetarget.html +++ b/netbox/templates/ipam/routetarget.html @@ -25,18 +25,54 @@
    {% include 'inc/panels/tags.html' %} - {% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/comments.html' %} {% plugin_left_page object %}
    -
    - {% include 'inc/panel_table.html' with table=importing_vrfs_table heading="Importing VRFs" %} -
    - {% include 'inc/panel_table.html' with table=exporting_vrfs_table heading="Exporting VRFs" %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/comments.html' %} {% plugin_right_page object %}
    +
    +
    +
    +
    Importing VRFs
    +
    +
    +
    +
    +
    +
    Exporting VRFs
    +
    +
    +
    +
    +
    +
    +
    +
    Importing L2VPNs
    +
    +
    +
    +
    +
    +
    Exporting L2VPNs
    +
    +
    +
    +
    {% plugin_full_width_page object %} From 157bf89e899874d2ed28a603ac7cd7ddae0b59f8 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 26 Jan 2023 09:46:28 -0500 Subject: [PATCH 017/174] Closes #11584: Add a list view for contact assignments --- docs/release-notes/version-3.5.md | 1 + netbox/netbox/navigation/menu.py | 1 + netbox/tenancy/forms/filtersets.py | 41 +++++++++++++++++++++++++++++- netbox/tenancy/urls.py | 1 + netbox/tenancy/views.py | 7 +++++ 5 files changed, 50 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index be0dca39c..69a0b8d31 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -4,6 +4,7 @@ ### Enhancements +* [#11584](https://github.com/netbox-community/netbox/issues/11584) - Add a list view for contact assignments * [#11254](https://github.com/netbox-community/netbox/issues/11254) - Introduce the `X-Request-ID` HTTP header to annotate the unique ID of each request for change logging * [#11440](https://github.com/netbox-community/netbox/issues/11440) - Add an `enabled` field for device type interfaces diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 09a35489d..83a81690f 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -46,6 +46,7 @@ ORGANIZATION_MENU = Menu( get_model_item('tenancy', 'contact', _('Contacts')), get_model_item('tenancy', 'contactgroup', _('Contact Groups')), get_model_item('tenancy', 'contactrole', _('Contact Roles')), + get_model_item('tenancy', 'contactassignment', _('Contact Assignments'), actions=[]), ), ), ), diff --git a/netbox/tenancy/forms/filtersets.py b/netbox/tenancy/forms/filtersets.py index c5d7fca0c..7f843d9a4 100644 --- a/netbox/tenancy/forms/filtersets.py +++ b/netbox/tenancy/forms/filtersets.py @@ -1,11 +1,17 @@ +from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ +from extras.utils import FeatureQuery from netbox.forms import NetBoxModelFilterSetForm +from tenancy.choices import * from tenancy.models import * from tenancy.forms import ContactModelFilterForm -from utilities.forms import DynamicModelMultipleChoiceField, TagFilterField +from utilities.forms.fields import ( + ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, MultipleChoiceField, TagFilterField, +) __all__ = ( + 'ContactAssignmentFilterForm', 'ContactFilterForm', 'ContactGroupFilterForm', 'ContactRoleFilterForm', @@ -71,3 +77,36 @@ class ContactFilterForm(NetBoxModelFilterSetForm): label=_('Group') ) tag = TagFilterField(model) + + +class ContactAssignmentFilterForm(NetBoxModelFilterSetForm): + model = ContactAssignment + fieldsets = ( + (None, ('q', 'filter_id')), + ('Assignment', ('content_type_id', 'group_id', 'contact_id', 'role_id', 'priority')), + ) + content_type_id = ContentTypeMultipleChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields'), + required=False, + label=_('Object type') + ) + group_id = DynamicModelMultipleChoiceField( + queryset=ContactGroup.objects.all(), + required=False, + label=_('Group') + ) + contact_id = DynamicModelMultipleChoiceField( + queryset=Contact.objects.all(), + required=False, + label=_('Contact') + ) + role_id = DynamicModelMultipleChoiceField( + queryset=ContactRole.objects.all(), + required=False, + label=_('Role') + ) + priority = MultipleChoiceField( + choices=ContactPriorityChoices, + required=False + ) diff --git a/netbox/tenancy/urls.py b/netbox/tenancy/urls.py index 3b5addaec..cb8715f70 100644 --- a/netbox/tenancy/urls.py +++ b/netbox/tenancy/urls.py @@ -47,6 +47,7 @@ urlpatterns = [ path('contacts//', include(get_model_urls('tenancy', 'contact'))), # Contact assignments + path('contact-assignments/', views.ContactAssignmentListView.as_view(), name='contactassignment_list'), path('contact-assignments/add/', views.ContactAssignmentEditView.as_view(), name='contactassignment_add'), path('contact-assignments//', include(get_model_urls('tenancy', 'contactassignment'))), diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 93830bd55..b13a9c12c 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -366,6 +366,13 @@ class ContactBulkDeleteView(generic.BulkDeleteView): # Contact assignments # +class ContactAssignmentListView(generic.ObjectListView): + queryset = ContactAssignment.objects.all() + filterset = filtersets.ContactAssignmentFilterSet + filterset_form = forms.ContactAssignmentFilterForm + table = tables.ContactAssignmentTable + + @register_model_view(ContactAssignment, 'edit') class ContactAssignmentEditView(generic.ObjectEditView): queryset = ContactAssignment.objects.all() From 266906842971e677c8c02e5277e396f2ef2e2777 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 26 Jan 2023 09:54:24 -0500 Subject: [PATCH 018/174] #11517: Standardize display of contact assignments --- docs/release-notes/version-3.5.md | 1 + netbox/templates/tenancy/contact.html | 16 ++++++---------- netbox/templates/tenancy/contactrole.html | 14 +------------- netbox/tenancy/views.py | 23 +++-------------------- 4 files changed, 11 insertions(+), 43 deletions(-) diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 69a0b8d31..ab3031c0d 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -4,6 +4,7 @@ ### Enhancements +* [#11517](https://github.com/netbox-community/netbox/issues/11517) - Standardize the inclusion of related objects across the entire UI * [#11584](https://github.com/netbox-community/netbox/issues/11584) - Add a list view for contact assignments * [#11254](https://github.com/netbox-community/netbox/issues/11254) - Introduce the `X-Request-ID` HTTP header to annotate the unique ID of each request for change logging * [#11440](https://github.com/netbox-community/netbox/issues/11440) - Add an `enabled` field for device type interfaces diff --git a/netbox/templates/tenancy/contact.html b/netbox/templates/tenancy/contact.html index d92226137..f249a8858 100644 --- a/netbox/templates/tenancy/contact.html +++ b/netbox/templates/tenancy/contact.html @@ -67,19 +67,15 @@ Description {{ object.description|placeholder }} - - Assignments - {{ assignment_count }} -
    - {% include 'inc/panels/comments.html' %} + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
    + {% include 'inc/panels/comments.html' %} {% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' %} {% plugin_right_page object %}
    @@ -87,10 +83,10 @@
    Assignments
    -
    - {% render_table assignments_table 'inc/table.html' %} - {% include 'inc/paginator.html' with paginator=assignments_table.paginator page=assignments_table.page %} -
    +
    {% plugin_full_width_page object %}
    diff --git a/netbox/templates/tenancy/contactrole.html b/netbox/templates/tenancy/contactrole.html index 85b78578a..bb4802423 100644 --- a/netbox/templates/tenancy/contactrole.html +++ b/netbox/templates/tenancy/contactrole.html @@ -22,12 +22,6 @@ Description {{ object.description|placeholder }} - - Assignments - - {{ assignment_count }} - - @@ -35,19 +29,13 @@ {% plugin_left_page object %}
    + {% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
    -
    -
    Assigned Contacts
    -
    - {% render_table contacts_table 'inc/table.html' %} - {% include 'inc/paginator.html' with paginator=contacts_table.paginator page=contacts_table.page %} -
    -
    {% plugin_full_width_page object %}
    diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index b13a9c12c..b7585b8d7 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -255,16 +255,12 @@ class ContactRoleView(generic.ObjectView): queryset = ContactRole.objects.all() def get_extra_context(self, request, instance): - contact_assignments = ContactAssignment.objects.restrict(request.user, 'view').filter( - role=instance + related_models = ( + (ContactAssignment.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'), ) - contacts_table = tables.ContactAssignmentTable(contact_assignments, user=request.user) - contacts_table.columns.hide('role') - contacts_table.configure(request) return { - 'contacts_table': contacts_table, - 'assignment_count': ContactAssignment.objects.filter(role=instance).count(), + 'related_models': related_models, } @@ -314,19 +310,6 @@ class ContactListView(generic.ObjectListView): class ContactView(generic.ObjectView): queryset = Contact.objects.all() - def get_extra_context(self, request, instance): - contact_assignments = ContactAssignment.objects.restrict(request.user, 'view').filter( - contact=instance - ) - assignments_table = tables.ContactAssignmentTable(contact_assignments, user=request.user) - assignments_table.columns.hide('contact') - assignments_table.configure(request) - - return { - 'assignments_table': assignments_table, - 'assignment_count': ContactAssignment.objects.filter(contact=instance).count(), - } - @register_model_view(Contact, 'edit') class ContactEditView(generic.ObjectEditView): From 7accdd52d836d3e3b50c6c2fe064439fd4783232 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 27 Jan 2023 16:30:31 -0500 Subject: [PATCH 019/174] Closes #11611: Refactor API viewset classes and introduce NetBoxReadOnlyModelViewSet --- docs/release-notes/version-3.5.md | 1 + netbox/netbox/api/viewsets/__init__.py | 135 ++++++++++--------------- netbox/netbox/api/viewsets/mixins.py | 82 +++++++++++++++ 3 files changed, 134 insertions(+), 84 deletions(-) diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index ab3031c0d..6d0ab1834 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -13,3 +13,4 @@ * [#10604](https://github.com/netbox-community/netbox/issues/10604) - Remove unused `extra_tabs` block from `object.html` generic template * [#10923](https://github.com/netbox-community/netbox/issues/10923) - Remove unused `NetBoxModelCSVForm` class (replaced by `NetBoxModelImportForm`) +* [#11611](https://github.com/netbox-community/netbox/issues/11611) - Refactor API viewset classes and introduce NetBoxReadOnlyModelViewSet diff --git a/netbox/netbox/api/viewsets/__init__.py b/netbox/netbox/api/viewsets/__init__.py index d7e226c04..5fe81b1f5 100644 --- a/netbox/netbox/api/viewsets/__init__.py +++ b/netbox/netbox/api/viewsets/__init__.py @@ -1,21 +1,17 @@ import logging -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import transaction from django.db.models import ProtectedError -from django.http import Http404 +from rest_framework import mixins as drf_mixins from rest_framework.response import Response -from rest_framework.viewsets import ModelViewSet +from rest_framework.viewsets import GenericViewSet -from extras.models import ExportTemplate -from netbox.api.exceptions import SerializerNotFound -from netbox.constants import NESTED_SERIALIZER_PREFIX -from utilities.api import get_serializer_for_model from utilities.exceptions import AbortRequest -from .mixins import * +from . import mixins __all__ = ( + 'NetBoxReadOnlyModelViewSet', 'NetBoxModelViewSet', ) @@ -30,13 +26,47 @@ HTTP_ACTIONS = { } -class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectValidationMixin, ModelViewSet): +class BaseViewSet(GenericViewSet): + """ + Base class for all API ViewSets. This is responsible for the enforcement of object-based permissions. + """ + def initial(self, request, *args, **kwargs): + super().initial(request, *args, **kwargs) + + # Restrict the view's QuerySet to allow only the permitted objects + if request.user.is_authenticated: + if action := HTTP_ACTIONS[request.method]: + self.queryset = self.queryset.restrict(request.user, action) + + +class NetBoxReadOnlyModelViewSet( + mixins.BriefModeMixin, + mixins.CustomFieldsMixin, + mixins.ExportTemplatesMixin, + drf_mixins.RetrieveModelMixin, + drf_mixins.ListModelMixin, + BaseViewSet +): + pass + + +class NetBoxModelViewSet( + mixins.BulkUpdateModelMixin, + mixins.BulkDestroyModelMixin, + mixins.ObjectValidationMixin, + mixins.BriefModeMixin, + mixins.CustomFieldsMixin, + mixins.ExportTemplatesMixin, + drf_mixins.CreateModelMixin, + drf_mixins.RetrieveModelMixin, + drf_mixins.UpdateModelMixin, + drf_mixins.DestroyModelMixin, + drf_mixins.ListModelMixin, + BaseViewSet +): """ Extend DRF's ModelViewSet to support bulk update and delete functions. """ - brief = False - brief_prefetch_fields = [] - def get_object_with_snapshot(self): """ Save a pre-change snapshot of the object immediately after retrieving it. This snapshot will be used to @@ -48,71 +78,14 @@ class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectVali return obj def get_serializer(self, *args, **kwargs): - # If a list of objects has been provided, initialize the serializer with many=True if isinstance(kwargs.get('data', {}), list): kwargs['many'] = True return super().get_serializer(*args, **kwargs) - def get_serializer_class(self): - logger = logging.getLogger('netbox.api.views.ModelViewSet') - - # If using 'brief' mode, find and return the nested serializer for this model, if one exists - if self.brief: - logger.debug("Request is for 'brief' format; initializing nested serializer") - try: - serializer = get_serializer_for_model(self.queryset.model, prefix=NESTED_SERIALIZER_PREFIX) - logger.debug(f"Using serializer {serializer}") - return serializer - except SerializerNotFound: - logger.debug(f"Nested serializer for {self.queryset.model} not found!") - - # Fall back to the hard-coded serializer class - logger.debug(f"Using serializer {self.serializer_class}") - return self.serializer_class - - def get_serializer_context(self): - """ - For models which support custom fields, populate the `custom_fields` context. - """ - context = super().get_serializer_context() - - if hasattr(self.queryset.model, 'custom_fields'): - content_type = ContentType.objects.get_for_model(self.queryset.model) - context.update({ - 'custom_fields': content_type.custom_fields.all(), - }) - - return context - - def get_queryset(self): - # If using brief mode, clear all prefetches from the queryset and append only brief_prefetch_fields (if any) - if self.brief: - return super().get_queryset().prefetch_related(None).prefetch_related(*self.brief_prefetch_fields) - - return super().get_queryset() - - def initialize_request(self, request, *args, **kwargs): - # Check if brief=True has been passed - if request.method == 'GET' and request.GET.get('brief'): - self.brief = True - - return super().initialize_request(request, *args, **kwargs) - - def initial(self, request, *args, **kwargs): - super().initial(request, *args, **kwargs) - - if not request.user.is_authenticated: - return - - # Restrict the view's QuerySet to allow only the permitted objects - action = HTTP_ACTIONS[request.method] - if action: - self.queryset = self.queryset.restrict(request.user, action) - def dispatch(self, request, *args, **kwargs): - logger = logging.getLogger('netbox.api.views.ModelViewSet') + logger = logging.getLogger(f'netbox.api.views.{self.__class__.__name__}') try: return super().dispatch(request, *args, **kwargs) @@ -136,21 +109,11 @@ class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectVali **kwargs ) - def list(self, request, *args, **kwargs): - # Overrides ListModelMixin to allow processing ExportTemplates. - if 'export' in request.GET: - content_type = ContentType.objects.get_for_model(self.get_serializer_class().Meta.model) - et = ExportTemplate.objects.filter(content_types=content_type, name=request.GET['export']).first() - if et is None: - raise Http404 - queryset = self.filter_queryset(self.get_queryset()) - return et.render_to_response(queryset) - - return super().list(request, *args, **kwargs) + # Creates def perform_create(self, serializer): model = self.queryset.model - logger = logging.getLogger('netbox.api.views.ModelViewSet') + logger = logging.getLogger(f'netbox.api.views.{self.__class__.__name__}') logger.info(f"Creating new {model._meta.verbose_name}") # Enforce object-level permissions on save() @@ -161,6 +124,8 @@ class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectVali except ObjectDoesNotExist: raise PermissionDenied() + # Updates + def update(self, request, *args, **kwargs): # Hotwire get_object() to ensure we save a pre-change snapshot self.get_object = self.get_object_with_snapshot @@ -168,7 +133,7 @@ class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectVali def perform_update(self, serializer): model = self.queryset.model - logger = logging.getLogger('netbox.api.views.ModelViewSet') + logger = logging.getLogger(f'netbox.api.views.{self.__class__.__name__}') logger.info(f"Updating {model._meta.verbose_name} {serializer.instance} (PK: {serializer.instance.pk})") # Enforce object-level permissions on save() @@ -179,6 +144,8 @@ class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectVali except ObjectDoesNotExist: raise PermissionDenied() + # Deletes + def destroy(self, request, *args, **kwargs): # Hotwire get_object() to ensure we save a pre-change snapshot self.get_object = self.get_object_with_snapshot @@ -186,7 +153,7 @@ class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectVali def perform_destroy(self, instance): model = self.queryset.model - logger = logging.getLogger('netbox.api.views.ModelViewSet') + logger = logging.getLogger(f'netbox.api.views.{self.__class__.__name__}') logger.info(f"Deleting {model._meta.verbose_name} {instance} (PK: {instance.pk})") return super().perform_destroy(instance) diff --git a/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py index b47c88a4e..8b629bbc6 100644 --- a/netbox/netbox/api/viewsets/mixins.py +++ b/netbox/netbox/api/viewsets/mixins.py @@ -1,17 +1,99 @@ +import logging + +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.db import transaction +from django.http import Http404 from rest_framework import status from rest_framework.response import Response +from extras.models import ExportTemplate +from netbox.api.exceptions import SerializerNotFound from netbox.api.serializers import BulkOperationSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX +from utilities.api import get_serializer_for_model __all__ = ( + 'BriefModeMixin', 'BulkUpdateModelMixin', + 'CustomFieldsMixin', + 'ExportTemplatesMixin', 'BulkDestroyModelMixin', 'ObjectValidationMixin', ) +class BriefModeMixin: + """ + Enables brief mode support, so that the client can invoke a model's nested serializer by passing e.g. + GET /api/dcim/sites/?brief=True + """ + brief = False + brief_prefetch_fields = [] + + def initialize_request(self, request, *args, **kwargs): + # Annotate whether brief mode is active + self.brief = request.method == 'GET' and request.GET.get('brief') + + return super().initialize_request(request, *args, **kwargs) + + def get_serializer_class(self): + logger = logging.getLogger(f'netbox.api.views.{self.__class__.__name__}') + + # If using 'brief' mode, find and return the nested serializer for this model, if one exists + if self.brief: + logger.debug("Request is for 'brief' format; initializing nested serializer") + try: + return get_serializer_for_model(self.queryset.model, prefix=NESTED_SERIALIZER_PREFIX) + except SerializerNotFound: + logger.debug( + f"Nested serializer for {self.queryset.model} not found! Using serializer {self.serializer_class}" + ) + + return self.serializer_class + + def get_queryset(self): + qs = super().get_queryset() + + # If using brief mode, clear all prefetches from the queryset and append only brief_prefetch_fields (if any) + if self.brief: + return qs.prefetch_related(None).prefetch_related(*self.brief_prefetch_fields) + + return qs + + +class CustomFieldsMixin: + """ + For models which support custom fields, populate the `custom_fields` context. + """ + def get_serializer_context(self): + context = super().get_serializer_context() + + if hasattr(self.queryset.model, 'custom_fields'): + content_type = ContentType.objects.get_for_model(self.queryset.model) + context.update({ + 'custom_fields': content_type.custom_fields.all(), + }) + + return context + + +class ExportTemplatesMixin: + """ + Enable ExportTemplate support for list views. + """ + def list(self, request, *args, **kwargs): + if 'export' in request.GET: + content_type = ContentType.objects.get_for_model(self.get_serializer_class().Meta.model) + et = ExportTemplate.objects.filter(content_types=content_type, name=request.GET['export']).first() + if et is None: + raise Http404 + queryset = self.filter_queryset(self.get_queryset()) + return et.render_to_response(queryset) + + return super().list(request, *args, **kwargs) + + class BulkUpdateModelMixin: """ Support bulk modification of objects using the list endpoint for a model. Accepts a PATCH action with a list of one From e65b2a9fb353f042ebaf3a46a3bdc13f2fd14751 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 31 Jan 2023 10:07:24 -0500 Subject: [PATCH 020/174] Closes #11625: Add HTMX support to ObjectEditView --- docs/release-notes/version-3.5.md | 5 +- netbox/netbox/views/generic/object_views.py | 6 +++ netbox/templates/generic/object_edit.html | 57 ++------------------- netbox/templates/htmx/form.html | 51 ++++++++++++++++++ 4 files changed, 65 insertions(+), 54 deletions(-) create mode 100644 netbox/templates/htmx/form.html diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 6d0ab1834..ae2d319b3 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -4,10 +4,11 @@ ### Enhancements -* [#11517](https://github.com/netbox-community/netbox/issues/11517) - Standardize the inclusion of related objects across the entire UI -* [#11584](https://github.com/netbox-community/netbox/issues/11584) - Add a list view for contact assignments * [#11254](https://github.com/netbox-community/netbox/issues/11254) - Introduce the `X-Request-ID` HTTP header to annotate the unique ID of each request for change logging * [#11440](https://github.com/netbox-community/netbox/issues/11440) - Add an `enabled` field for device type interfaces +* [#11517](https://github.com/netbox-community/netbox/issues/11517) - Standardize the inclusion of related objects across the entire UI +* [#11584](https://github.com/netbox-community/netbox/issues/11584) - Add a list view for contact assignments +* [#11625](https://github.com/netbox-community/netbox/issues/11625) - Add HTMX support to ObjectEditView ### Other Changes diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 475cca9d3..2dff8b274 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -218,6 +218,12 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): form = self.form(instance=obj, initial=initial_data) restrict_form_fields(form, request.user) + # If this is an HTMX request, return only the rendered form HTML + if is_htmx(request): + return render(request, 'htmx/form.html', { + 'form': form, + }) + return render(request, self.template_name, { 'model': model, 'object': obj, diff --git a/netbox/templates/generic/object_edit.html b/netbox/templates/generic/object_edit.html index c61fb723f..8531ad6df 100644 --- a/netbox/templates/generic/object_edit.html +++ b/netbox/templates/generic/object_edit.html @@ -1,6 +1,4 @@ {% extends 'base/layout.html' %} -{% load form_helpers %} -{% load helpers %} {% comment %} Blocks: @@ -48,56 +46,11 @@ Context:
    {% csrf_token %} - {% block form %} - {% if form.fieldsets %} - - {# Render hidden fields #} - {% for field in form.hidden_fields %} - {{ field }} - {% endfor %} - - {# Render grouped fields according to Form #} - {% for group, fields in form.fieldsets %} -
    - {% if group %} -
    -
    {{ group }}
    -
    - {% endif %} - {% for name in fields %} - {% with field=form|getfield:name %} - {% if not field.field.widget.is_hidden %} - {% render_field field %} - {% endif %} - {% endwith %} - {% endfor %} -
    - {% endfor %} - - {% if form.custom_fields %} -
    -
    -
    Custom Fields
    -
    - {% render_custom_fields form %} -
    - {% endif %} - - {% if form.comments %} -
    -
    Comments
    - {% render_field form.comments %} -
    - {% endif %} - - {% else %} - {# Render all fields in a single group #} -
    - {% render_form form %} -
    - {% endif %} - - {% endblock form %} +
    + {% block form %} + {% include 'htmx/form.html' %} + {% endblock form %} +
    {% block buttons %} diff --git a/netbox/templates/htmx/form.html b/netbox/templates/htmx/form.html new file mode 100644 index 000000000..e5a2ab6c6 --- /dev/null +++ b/netbox/templates/htmx/form.html @@ -0,0 +1,51 @@ +{% load form_helpers %} + +{% if form.fieldsets %} + + {# Render hidden fields #} + {% for field in form.hidden_fields %} + {{ field }} + {% endfor %} + + {# Render grouped fields according to Form #} + {% for group, fields in form.fieldsets %} +
    + {% if group %} +
    +
    {{ group }}
    +
    + {% endif %} + {% for name in fields %} + {% with field=form|getfield:name %} + {% if not field.field.widget.is_hidden %} + {% render_field field %} + {% endif %} + {% endwith %} + {% endfor %} +
    + {% endfor %} + + {% if form.custom_fields %} +
    +
    +
    Custom Fields
    +
    + {% render_custom_fields form %} +
    + {% endif %} + + {% if form.comments %} +
    +
    Comments
    + {% render_field form.comments %} +
    + {% endif %} + +{% else %} + + {# Render all fields in a single group #} +
    + {% render_form form %} +
    + +{% endif %} From d8784d4155f5544d7046e17f7310a6b011336964 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 Feb 2023 10:06:23 -0500 Subject: [PATCH 021/174] Closes #11558: Add support for remote data sources (#11646) * WIP * WIP * Add git sync * Fix file hashing * Add last_synced to DataSource * Build out UI & API resources * Add status field to DataSource * Add UI control to sync data source * Add API endpoint to sync data sources * Fix display of DataSource job results * DataSource password should be write-only * General cleanup * Add data file UI view * Punt on HTTP, FTP support for now * Add DataSource URL validation * Add HTTP proxy support to git fetcher * Add management command to sync data sources * DataFile REST API endpoints should be read-only * Refactor fetch methods into backend classes * Replace auth & git branch fields with general-purpose parameters * Fix last_synced time * Render discrete form fields for backend parameters * Enable dynamic edit form for DataSource * Register DataBackend classes in application registry * Add search indexers for DataSource, DataFile * Add single & bulk delete views for DataFile * Add model documentation * Convert DataSource to a primary model * Introduce pre_sync & post_sync signals * Clean up migrations * Rename url to source_url * Clean up filtersets * Add API & filterset tests * Add view tests * Add initSelect() to HTMX refresh handler * Render DataSourceForm fieldsets dynamically * Update compiled static resources --- docs/models/core/datafile.md | 25 ++ docs/models/core/datasource.md | 47 +++ netbox/core/__init__.py | 0 netbox/core/api/__init__.py | 0 netbox/core/api/nested_serializers.py | 25 ++ netbox/core/api/serializers.py | 51 +++ netbox/core/api/urls.py | 13 + netbox/core/api/views.py | 52 +++ netbox/core/apps.py | 8 + netbox/core/choices.py | 34 ++ netbox/core/data_backends.py | 117 +++++++ netbox/core/exceptions.py | 2 + netbox/core/filtersets.py | 64 ++++ netbox/core/forms/__init__.py | 4 + netbox/core/forms/bulk_edit.py | 50 +++ netbox/core/forms/bulk_import.py | 15 + netbox/core/forms/filtersets.py | 49 +++ netbox/core/forms/model_forms.py | 81 +++++ netbox/core/graphql/__init__.py | 0 netbox/core/graphql/schema.py | 12 + netbox/core/graphql/types.py | 21 ++ netbox/core/jobs.py | 29 ++ netbox/core/management/__init__.py | 0 netbox/core/management/commands/__init__.py | 0 .../management/commands/syncdatasource.py | 41 +++ netbox/core/migrations/0001_initial.py | 62 ++++ netbox/core/migrations/__init__.py | 0 netbox/core/models/__init__.py | 1 + netbox/core/models/data.py | 302 ++++++++++++++++++ netbox/core/search.py | 21 ++ netbox/core/signals.py | 10 + netbox/core/tables/__init__.py | 1 + netbox/core/tables/data.py | 52 +++ netbox/core/tests/__init__.py | 0 netbox/core/tests/test_api.py | 93 ++++++ netbox/core/tests/test_filtersets.py | 120 +++++++ netbox/core/tests/test_views.py | 91 ++++++ netbox/core/urls.py | 22 ++ netbox/core/views.py | 118 +++++++ netbox/extras/management/commands/nbshell.py | 2 +- netbox/extras/models/models.py | 10 +- netbox/netbox/api/views.py | 1 + netbox/netbox/graphql/schema.py | 2 + netbox/netbox/navigation/menu.py | 1 + netbox/netbox/registry.py | 3 +- netbox/netbox/settings.py | 1 + netbox/netbox/urls.py | 2 + netbox/project-static/dist/netbox.js | Bin 380899 -> 380935 bytes netbox/project-static/dist/netbox.js.map | Bin 353676 -> 353697 bytes netbox/project-static/src/htmx.ts | 4 +- netbox/templates/core/datafile.html | 81 +++++ netbox/templates/core/datasource.html | 114 +++++++ netbox/utilities/files.py | 9 + 53 files changed, 1857 insertions(+), 6 deletions(-) create mode 100644 docs/models/core/datafile.md create mode 100644 docs/models/core/datasource.md create mode 100644 netbox/core/__init__.py create mode 100644 netbox/core/api/__init__.py create mode 100644 netbox/core/api/nested_serializers.py create mode 100644 netbox/core/api/serializers.py create mode 100644 netbox/core/api/urls.py create mode 100644 netbox/core/api/views.py create mode 100644 netbox/core/apps.py create mode 100644 netbox/core/choices.py create mode 100644 netbox/core/data_backends.py create mode 100644 netbox/core/exceptions.py create mode 100644 netbox/core/filtersets.py create mode 100644 netbox/core/forms/__init__.py create mode 100644 netbox/core/forms/bulk_edit.py create mode 100644 netbox/core/forms/bulk_import.py create mode 100644 netbox/core/forms/filtersets.py create mode 100644 netbox/core/forms/model_forms.py create mode 100644 netbox/core/graphql/__init__.py create mode 100644 netbox/core/graphql/schema.py create mode 100644 netbox/core/graphql/types.py create mode 100644 netbox/core/jobs.py create mode 100644 netbox/core/management/__init__.py create mode 100644 netbox/core/management/commands/__init__.py create mode 100644 netbox/core/management/commands/syncdatasource.py create mode 100644 netbox/core/migrations/0001_initial.py create mode 100644 netbox/core/migrations/__init__.py create mode 100644 netbox/core/models/__init__.py create mode 100644 netbox/core/models/data.py create mode 100644 netbox/core/search.py create mode 100644 netbox/core/signals.py create mode 100644 netbox/core/tables/__init__.py create mode 100644 netbox/core/tables/data.py create mode 100644 netbox/core/tests/__init__.py create mode 100644 netbox/core/tests/test_api.py create mode 100644 netbox/core/tests/test_filtersets.py create mode 100644 netbox/core/tests/test_views.py create mode 100644 netbox/core/urls.py create mode 100644 netbox/core/views.py create mode 100644 netbox/templates/core/datafile.html create mode 100644 netbox/templates/core/datasource.html create mode 100644 netbox/utilities/files.py diff --git a/docs/models/core/datafile.md b/docs/models/core/datafile.md new file mode 100644 index 000000000..3e2aa2f27 --- /dev/null +++ b/docs/models/core/datafile.md @@ -0,0 +1,25 @@ +# Data Files + +A data file object is the representation in NetBox's database of some file belonging to a remote [data source](./datasource.md). Data files are synchronized automatically, and cannot be modified locally (although they can be deleted). + +## Fields + +### Source + +The [data source](./datasource.md) to which this file belongs. + +### Path + +The path to the file, relative to its source's URL. For example, a file at `/opt/config-data/routing/bgp/peer.yaml` with a source URL of `file:///opt/config-data/` would have its path set to `routing/bgp/peer.yaml`. + +### Last Updated + +The date and time at which the file most recently updated from its source. Note that this attribute is updated only when the file's contents have been modified. Re-synchronizing the data source will not update this timestamp if the upstream file's data has not changed. + +### Size + +The file's size, in bytes. + +### Hash + +A [SHA256 hash](https://en.wikipedia.org/wiki/SHA-2) of the file's data. This can be compared to a hash taken from the original file to determine whether any changes have been made. diff --git a/docs/models/core/datasource.md b/docs/models/core/datasource.md new file mode 100644 index 000000000..d16abdd10 --- /dev/null +++ b/docs/models/core/datasource.md @@ -0,0 +1,47 @@ +# Data Sources + +A data source represents some external repository of data which NetBox can consume, such as a git repository. Files within the data source are synchronized to NetBox by saving them in the database as [data file](./datafile.md) objects. + +## Fields + +### Name + +The data source's human-friendly name. + +### Type + +The type of data source. Supported options include: + +* Local directory +* git repository + +### URL + +The URL identifying the remote source. Some examples are included below. + +| Type | Example URL | +|------|-------------| +| Local | file:///var/my/data/source/ | +| git | https://https://github.com/my-organization/my-repo | + +### Status + +The source's current synchronization status. Note that this cannot be set manually: It is updated automatically when the source is synchronized. + +### Enabled + +If false, synchronization will be disabled. + +### Ignore Rules + +A set of rules (one per line) identifying filenames to ignore during synchronization. Some examples are provided below. See Python's [`fnmatch()` documentation](https://docs.python.org/3/library/fnmatch.html) for a complete reference. + +| Rule | Description | +|----------------|------------------------------------------| +| `README` | Ignore any files named `README` | +| `*.txt` | Ignore any files with a `.txt` extension | +| `data???.json` | Ignore e.g. `data123.json` | + +### Last Synced + +The date and time at which the source was most recently synchronized successfully. diff --git a/netbox/core/__init__.py b/netbox/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/core/api/__init__.py b/netbox/core/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/core/api/nested_serializers.py b/netbox/core/api/nested_serializers.py new file mode 100644 index 000000000..0a8351fec --- /dev/null +++ b/netbox/core/api/nested_serializers.py @@ -0,0 +1,25 @@ +from rest_framework import serializers + +from core.models import * +from netbox.api.serializers import WritableNestedSerializer + +__all__ = [ + 'NestedDataFileSerializer', + 'NestedDataSourceSerializer', +] + + +class NestedDataSourceSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='core-api:datasource-detail') + + class Meta: + model = DataSource + fields = ['id', 'url', 'display', 'name'] + + +class NestedDataFileSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='core-api:datafile-detail') + + class Meta: + model = DataFile + fields = ['id', 'url', 'display', 'path'] diff --git a/netbox/core/api/serializers.py b/netbox/core/api/serializers.py new file mode 100644 index 000000000..4c29fd69e --- /dev/null +++ b/netbox/core/api/serializers.py @@ -0,0 +1,51 @@ +from rest_framework import serializers + +from core.choices import * +from core.models import * +from netbox.api.fields import ChoiceField +from netbox.api.serializers import NetBoxModelSerializer +from .nested_serializers import * + +__all__ = ( + 'DataSourceSerializer', +) + + +class DataSourceSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='core-api:datasource-detail' + ) + type = ChoiceField( + choices=DataSourceTypeChoices + ) + status = ChoiceField( + choices=DataSourceStatusChoices, + read_only=True + ) + + # Related object counts + file_count = serializers.IntegerField( + read_only=True + ) + + class Meta: + model = DataSource + fields = [ + 'id', 'url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments', + 'parameters', 'ignore_rules', 'created', 'last_updated', 'file_count', + ] + + +class DataFileSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='core-api:datafile-detail' + ) + source = NestedDataSourceSerializer( + read_only=True + ) + + class Meta: + model = DataFile + fields = [ + 'id', 'url', 'display', 'source', 'path', 'last_updated', 'size', 'hash', + ] diff --git a/netbox/core/api/urls.py b/netbox/core/api/urls.py new file mode 100644 index 000000000..364e5db55 --- /dev/null +++ b/netbox/core/api/urls.py @@ -0,0 +1,13 @@ +from netbox.api.routers import NetBoxRouter +from . import views + + +router = NetBoxRouter() +router.APIRootView = views.CoreRootView + +# Data sources +router.register('data-sources', views.DataSourceViewSet) +router.register('data-files', views.DataFileViewSet) + +app_name = 'core-api' +urlpatterns = router.urls diff --git a/netbox/core/api/views.py b/netbox/core/api/views.py new file mode 100644 index 000000000..b2d8c0ed4 --- /dev/null +++ b/netbox/core/api/views.py @@ -0,0 +1,52 @@ +from django.shortcuts import get_object_or_404 + +from rest_framework.decorators import action +from rest_framework.exceptions import PermissionDenied +from rest_framework.response import Response +from rest_framework.routers import APIRootView + +from core import filtersets +from core.models import * +from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet +from utilities.utils import count_related +from . import serializers + + +class CoreRootView(APIRootView): + """ + Core API root view + """ + def get_view_name(self): + return 'Core' + + +# +# Data sources +# + +class DataSourceViewSet(NetBoxModelViewSet): + queryset = DataSource.objects.annotate( + file_count=count_related(DataFile, 'source') + ) + serializer_class = serializers.DataSourceSerializer + filterset_class = filtersets.DataSourceFilterSet + + @action(detail=True, methods=['post']) + def sync(self, request, pk): + """ + Enqueue a job to synchronize the DataSource. + """ + if not request.user.has_perm('extras.sync_datasource'): + raise PermissionDenied("Syncing data sources requires the core.sync_datasource permission.") + + datasource = get_object_or_404(DataSource, pk=pk) + datasource.enqueue_sync_job(request) + serializer = serializers.DataSourceSerializer(datasource, context={'request': request}) + + return Response(serializer.data) + + +class DataFileViewSet(NetBoxReadOnlyModelViewSet): + queryset = DataFile.objects.defer('data').prefetch_related('source') + serializer_class = serializers.DataFileSerializer + filterset_class = filtersets.DataFileFilterSet diff --git a/netbox/core/apps.py b/netbox/core/apps.py new file mode 100644 index 000000000..c4886eb41 --- /dev/null +++ b/netbox/core/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + name = "core" + + def ready(self): + from . import data_backends, search diff --git a/netbox/core/choices.py b/netbox/core/choices.py new file mode 100644 index 000000000..6927c83fb --- /dev/null +++ b/netbox/core/choices.py @@ -0,0 +1,34 @@ +from django.utils.translation import gettext as _ + +from utilities.choices import ChoiceSet + + +# +# Data sources +# + +class DataSourceTypeChoices(ChoiceSet): + LOCAL = 'local' + GIT = 'git' + + CHOICES = ( + (LOCAL, _('Local'), 'gray'), + (GIT, _('Git'), 'blue'), + ) + + +class DataSourceStatusChoices(ChoiceSet): + + NEW = 'new' + QUEUED = 'queued' + SYNCING = 'syncing' + COMPLETED = 'completed' + FAILED = 'failed' + + CHOICES = ( + (NEW, _('New'), 'blue'), + (QUEUED, _('Queued'), 'orange'), + (SYNCING, _('Syncing'), 'cyan'), + (COMPLETED, _('Completed'), 'green'), + (FAILED, _('Failed'), 'red'), + ) diff --git a/netbox/core/data_backends.py b/netbox/core/data_backends.py new file mode 100644 index 000000000..5d0e80584 --- /dev/null +++ b/netbox/core/data_backends.py @@ -0,0 +1,117 @@ +import logging +import subprocess +import tempfile +from contextlib import contextmanager +from urllib.parse import quote, urlunparse, urlparse + +from django import forms +from django.conf import settings +from django.utils.translation import gettext as _ + +from netbox.registry import registry +from .choices import DataSourceTypeChoices +from .exceptions import SyncError + +__all__ = ( + 'LocalBackend', + 'GitBackend', +) + +logger = logging.getLogger('netbox.data_backends') + + +def register_backend(name): + """ + Decorator for registering a DataBackend class. + """ + def _wrapper(cls): + registry['data_backends'][name] = cls + return cls + + return _wrapper + + +class DataBackend: + parameters = {} + + def __init__(self, url, **kwargs): + self.url = url + self.params = kwargs + + @property + def url_scheme(self): + return urlparse(self.url).scheme.lower() + + @contextmanager + def fetch(self): + raise NotImplemented() + + +@register_backend(DataSourceTypeChoices.LOCAL) +class LocalBackend(DataBackend): + + @contextmanager + def fetch(self): + logger.debug(f"Data source type is local; skipping fetch") + local_path = urlparse(self.url).path # Strip file:// scheme + + yield local_path + + +@register_backend(DataSourceTypeChoices.GIT) +class GitBackend(DataBackend): + parameters = { + 'username': forms.CharField( + required=False, + label=_('Username'), + widget=forms.TextInput(attrs={'class': 'form-control'}) + ), + 'password': forms.CharField( + required=False, + label=_('Password'), + widget=forms.TextInput(attrs={'class': 'form-control'}) + ), + 'branch': forms.CharField( + required=False, + label=_('Branch'), + widget=forms.TextInput(attrs={'class': 'form-control'}) + ) + } + + @contextmanager + def fetch(self): + local_path = tempfile.TemporaryDirectory() + + # Add authentication credentials to URL (if specified) + username = self.params.get('username') + password = self.params.get('password') + if username and password: + url_components = list(urlparse(self.url)) + # Prepend username & password to netloc + url_components[1] = quote(f'{username}@{password}:') + url_components[1] + url = urlunparse(url_components) + else: + url = self.url + + # Compile git arguments + args = ['git', 'clone', '--depth', '1'] + if branch := self.params.get('branch'): + args.extend(['--branch', branch]) + args.extend([url, local_path.name]) + + # Prep environment variables + env_vars = {} + if settings.HTTP_PROXIES and self.url_scheme in ('http', 'https'): + env_vars['http_proxy'] = settings.HTTP_PROXIES.get(self.url_scheme) + + logger.debug(f"Cloning git repo: {' '.join(args)}") + try: + subprocess.run(args, check=True, capture_output=True, env=env_vars) + except subprocess.CalledProcessError as e: + raise SyncError( + f"Fetching remote data failed: {e.stderr}" + ) + + yield local_path.name + + local_path.cleanup() diff --git a/netbox/core/exceptions.py b/netbox/core/exceptions.py new file mode 100644 index 000000000..8412b0378 --- /dev/null +++ b/netbox/core/exceptions.py @@ -0,0 +1,2 @@ +class SyncError(Exception): + pass diff --git a/netbox/core/filtersets.py b/netbox/core/filtersets.py new file mode 100644 index 000000000..3bff34158 --- /dev/null +++ b/netbox/core/filtersets.py @@ -0,0 +1,64 @@ +from django.db.models import Q +from django.utils.translation import gettext as _ + +import django_filters + +from netbox.filtersets import ChangeLoggedModelFilterSet, NetBoxModelFilterSet +from .choices import * +from .models import * + +__all__ = ( + 'DataFileFilterSet', + 'DataSourceFilterSet', +) + + +class DataSourceFilterSet(NetBoxModelFilterSet): + type = django_filters.MultipleChoiceFilter( + choices=DataSourceTypeChoices, + null_value=None + ) + status = django_filters.MultipleChoiceFilter( + choices=DataSourceStatusChoices, + null_value=None + ) + + class Meta: + model = DataSource + fields = ('id', 'name', 'enabled') + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(description__icontains=value) | + Q(comments__icontains=value) + ) + + +class DataFileFilterSet(ChangeLoggedModelFilterSet): + q = django_filters.CharFilter( + method='search' + ) + source_id = django_filters.ModelMultipleChoiceFilter( + queryset=DataSource.objects.all(), + label=_('Data source (ID)'), + ) + source = django_filters.ModelMultipleChoiceFilter( + field_name='source__name', + queryset=DataSource.objects.all(), + to_field_name='name', + label=_('Data source (name)'), + ) + + class Meta: + model = DataFile + fields = ('id', 'path', 'last_updated', 'size', 'hash') + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(path__icontains=value) + ) diff --git a/netbox/core/forms/__init__.py b/netbox/core/forms/__init__.py new file mode 100644 index 000000000..1499f98b2 --- /dev/null +++ b/netbox/core/forms/__init__.py @@ -0,0 +1,4 @@ +from .bulk_edit import * +from .bulk_import import * +from .filtersets import * +from .model_forms import * diff --git a/netbox/core/forms/bulk_edit.py b/netbox/core/forms/bulk_edit.py new file mode 100644 index 000000000..c5713b626 --- /dev/null +++ b/netbox/core/forms/bulk_edit.py @@ -0,0 +1,50 @@ +from django import forms +from django.utils.translation import gettext as _ + +from core.choices import DataSourceTypeChoices +from core.models import * +from netbox.forms import NetBoxModelBulkEditForm +from utilities.forms import ( + add_blank_choice, BulkEditNullBooleanSelect, CommentField, SmallTextarea, StaticSelect, +) + +__all__ = ( + 'DataSourceBulkEditForm', +) + + +class DataSourceBulkEditForm(NetBoxModelBulkEditForm): + type = forms.ChoiceField( + choices=add_blank_choice(DataSourceTypeChoices), + required=False, + initial='', + widget=StaticSelect() + ) + enabled = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect(), + label=_('Enforce unique space') + ) + description = forms.CharField( + max_length=200, + required=False + ) + comments = CommentField( + widget=SmallTextarea, + label=_('Comments') + ) + parameters = forms.JSONField( + required=False + ) + ignore_rules = forms.CharField( + required=False, + widget=forms.Textarea() + ) + + model = DataSource + fieldsets = ( + (None, ('type', 'enabled', 'description', 'comments', 'parameters', 'ignore_rules')), + ) + nullable_fields = ( + 'description', 'description', 'parameters', 'comments', 'parameters', 'ignore_rules', + ) diff --git a/netbox/core/forms/bulk_import.py b/netbox/core/forms/bulk_import.py new file mode 100644 index 000000000..78a859dcb --- /dev/null +++ b/netbox/core/forms/bulk_import.py @@ -0,0 +1,15 @@ +from core.models import * +from netbox.forms import NetBoxModelImportForm + +__all__ = ( + 'DataSourceImportForm', +) + + +class DataSourceImportForm(NetBoxModelImportForm): + + class Meta: + model = DataSource + fields = ( + 'name', 'type', 'source_url', 'enabled', 'description', 'comments', 'parameters', 'ignore_rules', + ) diff --git a/netbox/core/forms/filtersets.py b/netbox/core/forms/filtersets.py new file mode 100644 index 000000000..433f07067 --- /dev/null +++ b/netbox/core/forms/filtersets.py @@ -0,0 +1,49 @@ +from django import forms +from django.utils.translation import gettext as _ + +from core.choices import * +from core.models import * +from netbox.forms import NetBoxModelFilterSetForm +from utilities.forms import ( + BOOLEAN_WITH_BLANK_CHOICES, DynamicModelMultipleChoiceField, MultipleChoiceField, StaticSelect, +) + +__all__ = ( + 'DataFileFilterForm', + 'DataSourceFilterForm', +) + + +class DataSourceFilterForm(NetBoxModelFilterSetForm): + model = DataSource + fieldsets = ( + (None, ('q', 'filter_id')), + ('Data Source', ('type', 'status')), + ) + type = MultipleChoiceField( + choices=DataSourceTypeChoices, + required=False + ) + status = MultipleChoiceField( + choices=DataSourceStatusChoices, + required=False + ) + enabled = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + + +class DataFileFilterForm(NetBoxModelFilterSetForm): + model = DataFile + fieldsets = ( + (None, ('q', 'filter_id')), + ('File', ('source_id',)), + ) + source_id = DynamicModelMultipleChoiceField( + queryset=DataSource.objects.all(), + required=False, + label=_('Data source') + ) diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py new file mode 100644 index 000000000..786e71c3a --- /dev/null +++ b/netbox/core/forms/model_forms.py @@ -0,0 +1,81 @@ +import copy + +from django import forms + +from core.models import * +from netbox.forms import NetBoxModelForm, StaticSelect +from netbox.registry import registry +from utilities.forms import CommentField + +__all__ = ( + 'DataSourceForm', +) + + +class DataSourceForm(NetBoxModelForm): + comments = CommentField() + + class Meta: + model = DataSource + fields = [ + 'name', 'type', 'source_url', 'enabled', 'description', 'comments', 'ignore_rules', 'tags', + ] + widgets = { + 'type': StaticSelect( + attrs={ + 'hx-get': '.', + 'hx-include': '#form_fields input', + 'hx-target': '#form_fields', + } + ), + 'ignore_rules': forms.Textarea( + attrs={ + 'rows': 5, + 'class': 'font-monospace', + 'placeholder': '.cache\n*.txt' + } + ), + } + + @property + def fieldsets(self): + fieldsets = [ + ('Source', ('name', 'type', 'source_url', 'enabled', 'description', 'tags', 'ignore_rules')), + ] + if self.backend_fields: + fieldsets.append( + ('Backend', self.backend_fields) + ) + + return fieldsets + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + backend_classes = registry['data_backends'] + + if self.is_bound and self.data.get('type') in backend_classes: + type_ = self.data['type'] + elif self.initial and self.initial.get('type') in backend_classes: + type_ = self.initial['type'] + else: + type_ = self.fields['type'].initial + backend = backend_classes.get(type_) + + self.backend_fields = [] + for name, form_field in backend.parameters.items(): + field_name = f'backend_{name}' + self.backend_fields.append(field_name) + self.fields[field_name] = copy.copy(form_field) + if self.instance and self.instance.parameters: + self.fields[field_name].initial = self.instance.parameters.get(name) + + def save(self, *args, **kwargs): + + parameters = {} + for name in self.fields: + if name.startswith('backend_'): + parameters[name[8:]] = self.cleaned_data[name] + self.instance.parameters = parameters + + return super().save(*args, **kwargs) diff --git a/netbox/core/graphql/__init__.py b/netbox/core/graphql/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/core/graphql/schema.py b/netbox/core/graphql/schema.py new file mode 100644 index 000000000..201965430 --- /dev/null +++ b/netbox/core/graphql/schema.py @@ -0,0 +1,12 @@ +import graphene + +from netbox.graphql.fields import ObjectField, ObjectListField +from .types import * + + +class CoreQuery(graphene.ObjectType): + data_file = ObjectField(DataFileType) + data_file_list = ObjectListField(DataFileType) + + data_source = ObjectField(DataSourceType) + data_source_list = ObjectListField(DataSourceType) diff --git a/netbox/core/graphql/types.py b/netbox/core/graphql/types.py new file mode 100644 index 000000000..402e36345 --- /dev/null +++ b/netbox/core/graphql/types.py @@ -0,0 +1,21 @@ +from core import filtersets, models +from netbox.graphql.types import BaseObjectType, NetBoxObjectType + +__all__ = ( + 'DataFileType', + 'DataSourceType', +) + + +class DataFileType(BaseObjectType): + class Meta: + model = models.DataFile + exclude = ('data',) + filterset_class = filtersets.DataFileFilterSet + + +class DataSourceType(NetBoxObjectType): + class Meta: + model = models.DataSource + fields = '__all__' + filterset_class = filtersets.DataSourceFilterSet diff --git a/netbox/core/jobs.py b/netbox/core/jobs.py new file mode 100644 index 000000000..ee285fa7c --- /dev/null +++ b/netbox/core/jobs.py @@ -0,0 +1,29 @@ +import logging + +from extras.choices import JobResultStatusChoices +from netbox.search.backends import search_backend +from .choices import * +from .exceptions import SyncError +from .models import DataSource + +logger = logging.getLogger(__name__) + + +def sync_datasource(job_result, *args, **kwargs): + """ + Call sync() on a DataSource. + """ + datasource = DataSource.objects.get(name=job_result.name) + + try: + job_result.start() + datasource.sync() + + # Update the search cache for DataFiles belonging to this source + search_backend.cache(datasource.datafiles.iterator()) + + except SyncError as e: + job_result.set_status(JobResultStatusChoices.STATUS_ERRORED) + job_result.save() + DataSource.objects.filter(pk=datasource.pk).update(status=DataSourceStatusChoices.FAILED) + logging.error(e) diff --git a/netbox/core/management/__init__.py b/netbox/core/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/core/management/commands/__init__.py b/netbox/core/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/core/management/commands/syncdatasource.py b/netbox/core/management/commands/syncdatasource.py new file mode 100644 index 000000000..3d73f70ab --- /dev/null +++ b/netbox/core/management/commands/syncdatasource.py @@ -0,0 +1,41 @@ +from django.core.management.base import BaseCommand, CommandError + +from core.models import DataSource + + +class Command(BaseCommand): + help = "Synchronize a data source from its remote upstream" + + def add_arguments(self, parser): + parser.add_argument('name', nargs='*', help="Data source(s) to synchronize") + parser.add_argument( + "--all", action='store_true', dest='sync_all', + help="Synchronize all data sources" + ) + + def handle(self, *args, **options): + + # Find DataSources to sync + if options['sync_all']: + datasources = DataSource.objects.all() + elif options['name']: + datasources = DataSource.objects.filter(name__in=options['name']) + # Check for invalid names + found_names = {ds['name'] for ds in datasources.values('name')} + if invalid_names := set(options['name']) - found_names: + raise CommandError(f"Invalid data source names: {', '.join(invalid_names)}") + else: + raise CommandError(f"Must specify at least one data source, or set --all.") + + if len(options['name']) > 1: + self.stdout.write(f"Syncing {len(datasources)} data sources.") + + for i, datasource in enumerate(datasources, start=1): + self.stdout.write(f"[{i}] Syncing {datasource}... ", ending='') + self.stdout.flush() + datasource.sync() + self.stdout.write(datasource.get_status_display()) + self.stdout.flush() + + if len(options['name']) > 1: + self.stdout.write(f"Finished.") diff --git a/netbox/core/migrations/0001_initial.py b/netbox/core/migrations/0001_initial.py new file mode 100644 index 000000000..803ac3b13 --- /dev/null +++ b/netbox/core/migrations/0001_initial.py @@ -0,0 +1,62 @@ +# Generated by Django 4.1.5 on 2023-02-02 02:37 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import taggit.managers +import utilities.json + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('extras', '0084_staging'), + ] + + operations = [ + migrations.CreateModel( + name='DataSource', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), + ('description', models.CharField(blank=True, max_length=200)), + ('comments', models.TextField(blank=True)), + ('name', models.CharField(max_length=100, unique=True)), + ('type', models.CharField(default='local', max_length=50)), + ('source_url', models.CharField(max_length=200)), + ('status', models.CharField(default='new', editable=False, max_length=50)), + ('enabled', models.BooleanField(default=True)), + ('ignore_rules', models.TextField(blank=True)), + ('parameters', models.JSONField(blank=True, null=True)), + ('last_synced', models.DateTimeField(blank=True, editable=False, null=True)), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'ordering': ('name',), + }, + ), + migrations.CreateModel( + name='DataFile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('path', models.CharField(editable=False, max_length=1000)), + ('last_updated', models.DateTimeField(editable=False)), + ('size', models.PositiveIntegerField(editable=False)), + ('hash', models.CharField(editable=False, max_length=64, validators=[django.core.validators.RegexValidator(message='Length must be 64 hexadecimal characters.', regex='^[0-9a-f]{64}$')])), + ('data', models.BinaryField()), + ('source', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='datafiles', to='core.datasource')), + ], + options={ + 'ordering': ('source', 'path'), + }, + ), + migrations.AddConstraint( + model_name='datafile', + constraint=models.UniqueConstraint(fields=('source', 'path'), name='core_datafile_unique_source_path'), + ), + ] diff --git a/netbox/core/migrations/__init__.py b/netbox/core/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/core/models/__init__.py b/netbox/core/models/__init__.py new file mode 100644 index 000000000..df22d8bbb --- /dev/null +++ b/netbox/core/models/__init__.py @@ -0,0 +1 @@ +from .data import * diff --git a/netbox/core/models/data.py b/netbox/core/models/data.py new file mode 100644 index 000000000..5ad048b0f --- /dev/null +++ b/netbox/core/models/data.py @@ -0,0 +1,302 @@ +import logging +import os +from fnmatch import fnmatchcase +from urllib.parse import urlparse + +from django.conf import settings +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ValidationError +from django.core.validators import RegexValidator +from django.db import models +from django.urls import reverse +from django.utils import timezone +from django.utils.module_loading import import_string +from django.utils.translation import gettext as _ + +from extras.models import JobResult +from netbox.models import PrimaryModel +from netbox.models.features import ChangeLoggingMixin +from netbox.registry import registry +from utilities.files import sha256_hash +from utilities.querysets import RestrictedQuerySet +from ..choices import * +from ..exceptions import SyncError +from ..signals import post_sync, pre_sync + +__all__ = ( + 'DataFile', + 'DataSource', +) + +logger = logging.getLogger('netbox.core.data') + + +class DataSource(PrimaryModel): + """ + A remote source, such as a git repository, from which DataFiles are synchronized. + """ + name = models.CharField( + max_length=100, + unique=True + ) + type = models.CharField( + max_length=50, + choices=DataSourceTypeChoices, + default=DataSourceTypeChoices.LOCAL + ) + source_url = models.CharField( + max_length=200, + verbose_name=_('URL') + ) + status = models.CharField( + max_length=50, + choices=DataSourceStatusChoices, + default=DataSourceStatusChoices.NEW, + editable=False + ) + enabled = models.BooleanField( + default=True + ) + ignore_rules = models.TextField( + blank=True, + help_text=_("Patterns (one per line) matching files to ignore when syncing") + ) + parameters = models.JSONField( + blank=True, + null=True + ) + last_synced = models.DateTimeField( + blank=True, + null=True, + editable=False + ) + + class Meta: + ordering = ('name',) + + def __str__(self): + return f'{self.name}' + + def get_absolute_url(self): + return reverse('core:datasource', args=[self.pk]) + + @property + def docs_url(self): + return f'{settings.STATIC_URL}docs/models/{self._meta.app_label}/{self._meta.model_name}/' + + def get_type_color(self): + return DataSourceTypeChoices.colors.get(self.type) + + def get_status_color(self): + return DataSourceStatusChoices.colors.get(self.status) + + @property + def url_scheme(self): + return urlparse(self.source_url).scheme.lower() + + @property + def ready_for_sync(self): + return self.enabled and self.status not in ( + DataSourceStatusChoices.QUEUED, + DataSourceStatusChoices.SYNCING + ) + + def clean(self): + + # Ensure URL scheme matches selected type + if self.type == DataSourceTypeChoices.LOCAL and self.url_scheme not in ('file', ''): + raise ValidationError({ + 'url': f"URLs for local sources must start with file:// (or omit the scheme)" + }) + + def enqueue_sync_job(self, request): + """ + Enqueue a background job to synchronize the DataSource by calling sync(). + """ + # Set the status to "syncing" + self.status = DataSourceStatusChoices.QUEUED + + # Enqueue a sync job + job_result = JobResult.enqueue_job( + import_string('core.jobs.sync_datasource'), + name=self.name, + obj_type=ContentType.objects.get_for_model(DataSource), + user=request.user, + ) + + return job_result + + def get_backend(self): + backend_cls = registry['data_backends'].get(self.type) + backend_params = self.parameters or {} + + return backend_cls(self.source_url, **backend_params) + + def sync(self): + """ + Create/update/delete child DataFiles as necessary to synchronize with the remote source. + """ + if not self.ready_for_sync: + raise SyncError(f"Cannot initiate sync; data source not ready/enabled") + + # Emit the pre_sync signal + pre_sync.send(sender=self.__class__, instance=self) + + self.status = DataSourceStatusChoices.SYNCING + DataSource.objects.filter(pk=self.pk).update(status=self.status) + + # Replicate source data locally + backend = self.get_backend() + with backend.fetch() as local_path: + + logger.debug(f'Syncing files from source root {local_path}') + data_files = self.datafiles.all() + known_paths = {df.path for df in data_files} + logger.debug(f'Starting with {len(known_paths)} known files') + + # Check for any updated/deleted files + updated_files = [] + deleted_file_ids = [] + for datafile in data_files: + + try: + if datafile.refresh_from_disk(source_root=local_path): + updated_files.append(datafile) + except FileNotFoundError: + # File no longer exists + deleted_file_ids.append(datafile.pk) + continue + + # Bulk update modified files + updated_count = DataFile.objects.bulk_update(updated_files, ['hash']) + logger.debug(f"Updated {updated_count} files") + + # Bulk delete deleted files + deleted_count, _ = DataFile.objects.filter(pk__in=deleted_file_ids).delete() + logger.debug(f"Deleted {updated_count} files") + + # Walk the local replication to find new files + new_paths = self._walk(local_path) - known_paths + + # Bulk create new files + new_datafiles = [] + for path in new_paths: + datafile = DataFile(source=self, path=path) + datafile.refresh_from_disk(source_root=local_path) + datafile.full_clean() + new_datafiles.append(datafile) + created_count = len(DataFile.objects.bulk_create(new_datafiles, batch_size=100)) + logger.debug(f"Created {created_count} data files") + + # Update status & last_synced time + self.status = DataSourceStatusChoices.COMPLETED + self.last_synced = timezone.now() + DataSource.objects.filter(pk=self.pk).update(status=self.status, last_synced=self.last_synced) + + # Emit the post_sync signal + post_sync.send(sender=self.__class__, instance=self) + + def _walk(self, root): + """ + Return a set of all non-excluded files within the root path. + """ + logger.debug(f"Walking {root}...") + paths = set() + + for path, dir_names, file_names in os.walk(root): + path = path.split(root)[1].lstrip('/') # Strip root path + if path.startswith('.'): + continue + for file_name in file_names: + if not self._ignore(file_name): + paths.add(os.path.join(path, file_name)) + + logger.debug(f"Found {len(paths)} files") + return paths + + def _ignore(self, filename): + """ + Returns a boolean indicating whether the file should be ignored per the DataSource's configured + ignore rules. + """ + if filename.startswith('.'): + return True + for rule in self.ignore_rules.splitlines(): + if fnmatchcase(filename, rule): + return True + return False + + +class DataFile(ChangeLoggingMixin, models.Model): + """ + The database representation of a remote file fetched from a remote DataSource. DataFile instances should be created, + updated, or deleted only by calling DataSource.sync(). + """ + source = models.ForeignKey( + to='core.DataSource', + on_delete=models.CASCADE, + related_name='datafiles', + editable=False + ) + path = models.CharField( + max_length=1000, + editable=False, + help_text=_("File path relative to the data source's root") + ) + last_updated = models.DateTimeField( + editable=False + ) + size = models.PositiveIntegerField( + editable=False + ) + hash = models.CharField( + max_length=64, + editable=False, + validators=[ + RegexValidator(regex='^[0-9a-f]{64}$', message=_("Length must be 64 hexadecimal characters.")) + ], + help_text=_("SHA256 hash of the file data") + ) + data = models.BinaryField() + + objects = RestrictedQuerySet.as_manager() + + class Meta: + ordering = ('source', 'path') + constraints = ( + models.UniqueConstraint( + fields=('source', 'path'), + name='%(app_label)s_%(class)s_unique_source_path' + ), + ) + + def __str__(self): + return self.path + + def get_absolute_url(self): + return reverse('core:datafile', args=[self.pk]) + + @property + def data_as_string(self): + try: + return self.data.tobytes().decode('utf-8') + except UnicodeDecodeError: + return None + + def refresh_from_disk(self, source_root): + """ + Update instance attributes from the file on disk. Returns True if any attribute + has changed. + """ + file_path = os.path.join(source_root, self.path) + file_hash = sha256_hash(file_path).hexdigest() + + # Update instance file attributes & data + if is_modified := file_hash != self.hash: + self.last_updated = timezone.now() + self.size = os.path.getsize(file_path) + self.hash = file_hash + with open(file_path, 'rb') as f: + self.data = f.read() + + return is_modified diff --git a/netbox/core/search.py b/netbox/core/search.py new file mode 100644 index 000000000..e6d3005e6 --- /dev/null +++ b/netbox/core/search.py @@ -0,0 +1,21 @@ +from netbox.search import SearchIndex, register_search +from . import models + + +@register_search +class DataSourceIndex(SearchIndex): + model = models.DataSource + fields = ( + ('name', 100), + ('source_url', 300), + ('description', 500), + ('comments', 5000), + ) + + +@register_search +class DataFileIndex(SearchIndex): + model = models.DataFile + fields = ( + ('path', 200), + ) diff --git a/netbox/core/signals.py b/netbox/core/signals.py new file mode 100644 index 000000000..65ca293f5 --- /dev/null +++ b/netbox/core/signals.py @@ -0,0 +1,10 @@ +import django.dispatch + +__all__ = ( + 'post_sync', + 'pre_sync', +) + +# DataSource signals +pre_sync = django.dispatch.Signal() +post_sync = django.dispatch.Signal() diff --git a/netbox/core/tables/__init__.py b/netbox/core/tables/__init__.py new file mode 100644 index 000000000..df22d8bbb --- /dev/null +++ b/netbox/core/tables/__init__.py @@ -0,0 +1 @@ +from .data import * diff --git a/netbox/core/tables/data.py b/netbox/core/tables/data.py new file mode 100644 index 000000000..8409e3b82 --- /dev/null +++ b/netbox/core/tables/data.py @@ -0,0 +1,52 @@ +import django_tables2 as tables + +from core.models import * +from netbox.tables import NetBoxTable, columns + +__all__ = ( + 'DataFileTable', + 'DataSourceTable', +) + + +class DataSourceTable(NetBoxTable): + name = tables.Column( + linkify=True + ) + type = columns.ChoiceFieldColumn() + status = columns.ChoiceFieldColumn() + enabled = columns.BooleanColumn() + tags = columns.TagColumn( + url_name='core:datasource_list' + ) + file_count = tables.Column( + verbose_name='Files' + ) + + class Meta(NetBoxTable.Meta): + model = DataSource + fields = ( + 'pk', 'id', 'name', 'type', 'status', 'enabled', 'source_url', 'description', 'comments', 'parameters', 'created', + 'last_updated', 'file_count', + ) + default_columns = ('pk', 'name', 'type', 'status', 'enabled', 'description', 'file_count') + + +class DataFileTable(NetBoxTable): + source = tables.Column( + linkify=True + ) + path = tables.Column( + linkify=True + ) + last_updated = columns.DateTimeColumn() + actions = columns.ActionsColumn( + actions=('delete',) + ) + + class Meta(NetBoxTable.Meta): + model = DataFile + fields = ( + 'pk', 'id', 'source', 'path', 'last_updated', 'size', 'hash', + ) + default_columns = ('pk', 'source', 'path', 'size', 'last_updated') diff --git a/netbox/core/tests/__init__.py b/netbox/core/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/core/tests/test_api.py b/netbox/core/tests/test_api.py new file mode 100644 index 000000000..dc6d6a5ce --- /dev/null +++ b/netbox/core/tests/test_api.py @@ -0,0 +1,93 @@ +from django.urls import reverse +from django.utils import timezone + +from utilities.testing import APITestCase, APIViewTestCases +from ..choices import * +from ..models import * + + +class AppTest(APITestCase): + + def test_root(self): + url = reverse('core-api:api-root') + response = self.client.get('{}?format=api'.format(url), **self.header) + + self.assertEqual(response.status_code, 200) + + +class DataSourceTest(APIViewTestCases.APIViewTestCase): + model = DataSource + brief_fields = ['display', 'id', 'name', 'url'] + bulk_update_data = { + 'enabled': False, + 'description': 'foo bar baz', + } + + @classmethod + def setUpTestData(cls): + data_sources = ( + DataSource(name='Data Source 1', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source1/'), + DataSource(name='Data Source 2', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source2/'), + DataSource(name='Data Source 3', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source3/'), + ) + DataSource.objects.bulk_create(data_sources) + + cls.create_data = [ + { + 'name': 'Data Source 4', + 'type': DataSourceTypeChoices.GIT, + 'source_url': 'https://example.com/git/source4' + }, + { + 'name': 'Data Source 5', + 'type': DataSourceTypeChoices.GIT, + 'source_url': 'https://example.com/git/source5' + }, + { + 'name': 'Data Source 6', + 'type': DataSourceTypeChoices.GIT, + 'source_url': 'https://example.com/git/source6' + }, + ] + + +class DataFileTest( + APIViewTestCases.GetObjectViewTestCase, + APIViewTestCases.ListObjectsViewTestCase, + APIViewTestCases.GraphQLTestCase +): + model = DataFile + brief_fields = ['display', 'id', 'path', 'url'] + + @classmethod + def setUpTestData(cls): + datasource = DataSource.objects.create( + name='Data Source 1', + type=DataSourceTypeChoices.LOCAL, + source_url='file:///var/tmp/source1/' + ) + + data_files = ( + DataFile( + source=datasource, + path='dir1/file1.txt', + last_updated=timezone.now(), + size=1000, + hash='442da078f0111cbdf42f21903724f6597c692535f55bdfbbea758a1ae99ad9e1' + ), + DataFile( + source=datasource, + path='dir1/file2.txt', + last_updated=timezone.now(), + size=2000, + hash='a78168c7c97115bafd96450ed03ea43acec495094c5caa28f0d02e20e3a76cc2' + ), + DataFile( + source=datasource, + path='dir1/file3.txt', + last_updated=timezone.now(), + size=3000, + hash='12b8827a14c4d5a2f30b6c6e2b7983063988612391c6cbe8ee7493b59054827a' + ), + ) + DataFile.objects.bulk_create(data_files) diff --git a/netbox/core/tests/test_filtersets.py b/netbox/core/tests/test_filtersets.py new file mode 100644 index 000000000..e1e916f70 --- /dev/null +++ b/netbox/core/tests/test_filtersets.py @@ -0,0 +1,120 @@ +from datetime import datetime + +from django.test import TestCase +from django.utils import timezone + +from utilities.testing import ChangeLoggedFilterSetTests +from ..choices import * +from ..filtersets import * +from ..models import * + + +class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests): + queryset = DataSource.objects.all() + filterset = DataSourceFilterSet + + @classmethod + def setUpTestData(cls): + data_sources = ( + DataSource( + name='Data Source 1', + type=DataSourceTypeChoices.LOCAL, + source_url='file:///var/tmp/source1/', + status=DataSourceStatusChoices.NEW, + enabled=True + ), + DataSource( + name='Data Source 2', + type=DataSourceTypeChoices.LOCAL, + source_url='file:///var/tmp/source2/', + status=DataSourceStatusChoices.SYNCING, + enabled=True + ), + DataSource( + name='Data Source 3', + type=DataSourceTypeChoices.GIT, + source_url='https://example.com/git/source3', + status=DataSourceStatusChoices.COMPLETED, + enabled=False + ), + ) + DataSource.objects.bulk_create(data_sources) + + def test_name(self): + params = {'name': ['Data Source 1', 'Data Source 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_type(self): + params = {'type': [DataSourceTypeChoices.LOCAL]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_enabled(self): + params = {'enabled': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'enabled': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_status(self): + params = {'status': [DataSourceStatusChoices.NEW, DataSourceStatusChoices.SYNCING]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests): + queryset = DataFile.objects.all() + filterset = DataFileFilterSet + + @classmethod + def setUpTestData(cls): + data_sources = ( + DataSource(name='Data Source 1', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source1/'), + DataSource(name='Data Source 2', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source2/'), + DataSource(name='Data Source 3', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source3/'), + ) + DataSource.objects.bulk_create(data_sources) + + data_files = ( + DataFile( + source=data_sources[0], + path='dir1/file1.txt', + last_updated=datetime(2023, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + size=1000, + hash='442da078f0111cbdf42f21903724f6597c692535f55bdfbbea758a1ae99ad9e1' + ), + DataFile( + source=data_sources[1], + path='dir1/file2.txt', + last_updated=datetime(2023, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + size=2000, + hash='a78168c7c97115bafd96450ed03ea43acec495094c5caa28f0d02e20e3a76cc2' + ), + DataFile( + source=data_sources[2], + path='dir1/file3.txt', + last_updated=datetime(2023, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + size=3000, + hash='12b8827a14c4d5a2f30b6c6e2b7983063988612391c6cbe8ee7493b59054827a' + ), + ) + DataFile.objects.bulk_create(data_files) + + def test_source(self): + sources = DataSource.objects.all() + params = {'source_id': [sources[0].pk, sources[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'source': [sources[0].name, sources[1].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_path(self): + params = {'path': ['dir1/file1.txt', 'dir1/file2.txt']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_size(self): + params = {'size': [1000, 2000]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_hash(self): + params = {'hash': [ + '442da078f0111cbdf42f21903724f6597c692535f55bdfbbea758a1ae99ad9e1', + 'a78168c7c97115bafd96450ed03ea43acec495094c5caa28f0d02e20e3a76cc2', + ]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/core/tests/test_views.py b/netbox/core/tests/test_views.py new file mode 100644 index 000000000..fbee031ed --- /dev/null +++ b/netbox/core/tests/test_views.py @@ -0,0 +1,91 @@ +from django.utils import timezone + +from utilities.testing import ViewTestCases, create_tags +from ..choices import * +from ..models import * + + +class DataSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase): + model = DataSource + + @classmethod + def setUpTestData(cls): + data_sources = ( + DataSource(name='Data Source 1', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source1/'), + DataSource(name='Data Source 2', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source2/'), + DataSource(name='Data Source 3', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source3/'), + ) + DataSource.objects.bulk_create(data_sources) + + tags = create_tags('Alpha', 'Bravo', 'Charlie') + + cls.form_data = { + 'name': 'Data Source X', + 'type': DataSourceTypeChoices.GIT, + 'source_url': 'http:///exmaple/com/foo/bar/', + 'description': 'Something', + 'comments': 'Foo bar baz', + 'tags': [t.pk for t in tags], + } + + cls.csv_data = ( + f"name,type,source_url,enabled", + f"Data Source 4,{DataSourceTypeChoices.LOCAL},file:///var/tmp/source4/,true", + f"Data Source 5,{DataSourceTypeChoices.LOCAL},file:///var/tmp/source4/,true", + f"Data Source 6,{DataSourceTypeChoices.GIT},http:///exmaple/com/foo/bar/,false", + ) + + cls.csv_update_data = ( + "id,name,description", + f"{data_sources[0].pk},Data Source 7,New description7", + f"{data_sources[1].pk},Data Source 8,New description8", + f"{data_sources[2].pk},Data Source 9,New description9", + ) + + cls.bulk_edit_data = { + 'enabled': False, + 'description': 'New description', + } + + +class DataFileTestCase( + ViewTestCases.GetObjectViewTestCase, + ViewTestCases.GetObjectChangelogViewTestCase, + ViewTestCases.DeleteObjectViewTestCase, + ViewTestCases.ListObjectsViewTestCase, + ViewTestCases.BulkDeleteObjectsViewTestCase, +): + model = DataFile + + @classmethod + def setUpTestData(cls): + datasource = DataSource.objects.create( + name='Data Source 1', + type=DataSourceTypeChoices.LOCAL, + source_url='file:///var/tmp/source1/' + ) + + data_files = ( + DataFile( + source=datasource, + path='dir1/file1.txt', + last_updated=timezone.now(), + size=1000, + hash='442da078f0111cbdf42f21903724f6597c692535f55bdfbbea758a1ae99ad9e1' + ), + DataFile( + source=datasource, + path='dir1/file2.txt', + last_updated=timezone.now(), + size=2000, + hash='a78168c7c97115bafd96450ed03ea43acec495094c5caa28f0d02e20e3a76cc2' + ), + DataFile( + source=datasource, + path='dir1/file3.txt', + last_updated=timezone.now(), + size=3000, + hash='12b8827a14c4d5a2f30b6c6e2b7983063988612391c6cbe8ee7493b59054827a' + ), + ) + DataFile.objects.bulk_create(data_files) diff --git a/netbox/core/urls.py b/netbox/core/urls.py new file mode 100644 index 000000000..128020890 --- /dev/null +++ b/netbox/core/urls.py @@ -0,0 +1,22 @@ +from django.urls import include, path + +from utilities.urls import get_model_urls +from . import views + +app_name = 'core' +urlpatterns = ( + + # Data sources + path('data-sources/', views.DataSourceListView.as_view(), name='datasource_list'), + path('data-sources/add/', views.DataSourceEditView.as_view(), name='datasource_add'), + path('data-sources/import/', views.DataSourceBulkImportView.as_view(), name='datasource_import'), + path('data-sources/edit/', views.DataSourceBulkEditView.as_view(), name='datasource_bulk_edit'), + path('data-sources/delete/', views.DataSourceBulkDeleteView.as_view(), name='datasource_bulk_delete'), + path('data-sources//', include(get_model_urls('core', 'datasource'))), + + # Data files + path('data-files/', views.DataFileListView.as_view(), name='datafile_list'), + path('data-files/delete/', views.DataFileBulkDeleteView.as_view(), name='datafile_bulk_delete'), + path('data-files//', include(get_model_urls('core', 'datafile'))), + +) diff --git a/netbox/core/views.py b/netbox/core/views.py new file mode 100644 index 000000000..63905228e --- /dev/null +++ b/netbox/core/views.py @@ -0,0 +1,118 @@ +from django.contrib import messages +from django.shortcuts import get_object_or_404, redirect + +from netbox.views import generic +from netbox.views.generic.base import BaseObjectView +from utilities.utils import count_related +from utilities.views import register_model_view +from . import filtersets, forms, tables +from .models import * + + +# +# Data sources +# + +class DataSourceListView(generic.ObjectListView): + queryset = DataSource.objects.annotate( + file_count=count_related(DataFile, 'source') + ) + filterset = filtersets.DataSourceFilterSet + filterset_form = forms.DataSourceFilterForm + table = tables.DataSourceTable + + +@register_model_view(DataSource) +class DataSourceView(generic.ObjectView): + queryset = DataSource.objects.all() + + def get_extra_context(self, request, instance): + related_models = ( + (DataFile.objects.restrict(request.user, 'view').filter(source=instance), 'source_id'), + ) + + return { + 'related_models': related_models, + } + + +@register_model_view(DataSource, 'sync') +class DataSourceSyncView(BaseObjectView): + queryset = DataSource.objects.all() + + def get_required_permission(self): + return 'core.sync_datasource' + + def get(self, request, pk): + # Redirect GET requests to the object view + datasource = get_object_or_404(self.queryset, pk=pk) + return redirect(datasource.get_absolute_url()) + + def post(self, request, pk): + datasource = get_object_or_404(self.queryset, pk=pk) + job_result = datasource.enqueue_sync_job(request) + + messages.success(request, f"Queued job #{job_result.pk} to sync {datasource}") + return redirect(datasource.get_absolute_url()) + + +@register_model_view(DataSource, 'edit') +class DataSourceEditView(generic.ObjectEditView): + queryset = DataSource.objects.all() + form = forms.DataSourceForm + + +@register_model_view(DataSource, 'delete') +class DataSourceDeleteView(generic.ObjectDeleteView): + queryset = DataSource.objects.all() + + +class DataSourceBulkImportView(generic.BulkImportView): + queryset = DataSource.objects.all() + model_form = forms.DataSourceImportForm + table = tables.DataSourceTable + + +class DataSourceBulkEditView(generic.BulkEditView): + queryset = DataSource.objects.annotate( + count_files=count_related(DataFile, 'source') + ) + filterset = filtersets.DataSourceFilterSet + table = tables.DataSourceTable + form = forms.DataSourceBulkEditForm + + +class DataSourceBulkDeleteView(generic.BulkDeleteView): + queryset = DataSource.objects.annotate( + count_files=count_related(DataFile, 'source') + ) + filterset = filtersets.DataSourceFilterSet + table = tables.DataSourceTable + + +# +# Data files +# + +class DataFileListView(generic.ObjectListView): + queryset = DataFile.objects.defer('data') + filterset = filtersets.DataFileFilterSet + filterset_form = forms.DataFileFilterForm + table = tables.DataFileTable + actions = ('bulk_delete',) + + +@register_model_view(DataFile) +class DataFileView(generic.ObjectView): + queryset = DataFile.objects.all() + + +@register_model_view(DataFile, 'delete') +class DataFileDeleteView(generic.ObjectDeleteView): + queryset = DataFile.objects.all() + + +class DataFileBulkDeleteView(generic.BulkDeleteView): + queryset = DataFile.objects.defer('data') + filterset = filtersets.DataFileFilterSet + table = tables.DataFileTable diff --git a/netbox/extras/management/commands/nbshell.py b/netbox/extras/management/commands/nbshell.py index 07f943d15..04a67eb49 100644 --- a/netbox/extras/management/commands/nbshell.py +++ b/netbox/extras/management/commands/nbshell.py @@ -9,7 +9,7 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.core.management.base import BaseCommand -APPS = ('circuits', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'wireless') +APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'wireless') BANNER_TEXT = """### NetBox interactive shell ({node}) ### Python {python} | Django {django} | NetBox {netbox} diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index e608f81b1..df32d6ac4 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -11,6 +11,7 @@ from django.core.validators import MinValueValidator, ValidationError from django.db import models from django.http import HttpResponse, QueryDict from django.urls import reverse +from django.urls.exceptions import NoReverseMatch from django.utils import timezone from django.utils.formats import date_format from django.utils.translation import gettext as _ @@ -634,7 +635,7 @@ class JobResult(models.Model): def delete(self, *args, **kwargs): super().delete(*args, **kwargs) - rq_queue_name = get_config().QUEUE_MAPPINGS.get(self.obj_type.name, RQ_QUEUE_DEFAULT) + rq_queue_name = get_config().QUEUE_MAPPINGS.get(self.obj_type.model, RQ_QUEUE_DEFAULT) queue = django_rq.get_queue(rq_queue_name) job = queue.fetch_job(str(self.job_id)) @@ -642,7 +643,10 @@ class JobResult(models.Model): job.cancel() def get_absolute_url(self): - return reverse(f'extras:{self.obj_type.name}_result', args=[self.pk]) + try: + return reverse(f'extras:{self.obj_type.model}_result', args=[self.pk]) + except NoReverseMatch: + return None def get_status_color(self): return JobResultStatusChoices.colors.get(self.status) @@ -693,7 +697,7 @@ class JobResult(models.Model): schedule_at: Schedule the job to be executed at the passed date and time interval: Recurrence interval (in minutes) """ - rq_queue_name = get_config().QUEUE_MAPPINGS.get(obj_type.name, RQ_QUEUE_DEFAULT) + rq_queue_name = get_config().QUEUE_MAPPINGS.get(obj_type.model, RQ_QUEUE_DEFAULT) queue = django_rq.get_queue(rq_queue_name) status = JobResultStatusChoices.STATUS_SCHEDULED if schedule_at else JobResultStatusChoices.STATUS_PENDING job_result: JobResult = JobResult.objects.create( diff --git a/netbox/netbox/api/views.py b/netbox/netbox/api/views.py index 6c6083959..023843bca 100644 --- a/netbox/netbox/api/views.py +++ b/netbox/netbox/api/views.py @@ -27,6 +27,7 @@ class APIRootView(APIView): return Response({ 'circuits': reverse('circuits-api:api-root', request=request, format=format), + 'core': reverse('core-api:api-root', request=request, format=format), 'dcim': reverse('dcim-api:api-root', request=request, format=format), 'extras': reverse('extras-api:api-root', request=request, format=format), 'ipam': reverse('ipam-api:api-root', request=request, format=format), diff --git a/netbox/netbox/graphql/schema.py b/netbox/netbox/graphql/schema.py index 82abfb4d5..7224f3c38 100644 --- a/netbox/netbox/graphql/schema.py +++ b/netbox/netbox/graphql/schema.py @@ -1,6 +1,7 @@ import graphene from circuits.graphql.schema import CircuitsQuery +from core.graphql.schema import CoreQuery from dcim.graphql.schema import DCIMQuery from extras.graphql.schema import ExtrasQuery from ipam.graphql.schema import IPAMQuery @@ -14,6 +15,7 @@ from wireless.graphql.schema import WirelessQuery class Query( UsersQuery, CircuitsQuery, + CoreQuery, DCIMQuery, ExtrasQuery, IPAMQuery, diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 83a81690f..6fce7dfe6 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -287,6 +287,7 @@ OTHER_MENU = Menu( MenuGroup( label=_('Integrations'), items=( + get_model_item('core', 'datasource', _('Data Sources')), get_model_item('extras', 'webhook', _('Webhooks')), MenuItem( link='extras:report_list', diff --git a/netbox/netbox/registry.py b/netbox/netbox/registry.py index 76886e791..670bca683 100644 --- a/netbox/netbox/registry.py +++ b/netbox/netbox/registry.py @@ -25,9 +25,10 @@ class Registry(dict): # Initialize the global registry registry = Registry() +registry['data_backends'] = dict() +registry['denormalized_fields'] = collections.defaultdict(list) registry['model_features'] = { feature: collections.defaultdict(set) for feature in EXTRAS_FEATURES } -registry['denormalized_fields'] = collections.defaultdict(list) registry['search'] = dict() registry['views'] = collections.defaultdict(dict) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 7f55463df..22849e6ba 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -332,6 +332,7 @@ INSTALLED_APPS = [ 'social_django', 'taggit', 'timezone_field', + 'core', 'circuits', 'dcim', 'ipam', diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 84e899ed2..22c47f7bb 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -42,6 +42,7 @@ _patterns = [ # Apps path('circuits/', include('circuits.urls')), + path('core/', include('core.urls')), path('dcim/', include('dcim.urls')), path('extras/', include('extras.urls')), path('ipam/', include('ipam.urls')), @@ -53,6 +54,7 @@ _patterns = [ # API path('api/', APIRootView.as_view(), name='api-root'), path('api/circuits/', include('circuits.api.urls')), + path('api/core/', include('core.api.urls')), path('api/dcim/', include('dcim.api.urls')), path('api/extras/', include('extras.api.urls')), path('api/ipam/', include('ipam.api.urls')), diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 19cdae0bd318381cbc0701ff151e7f87ad3ae9ee..e5793c1282d63d16d4b6e1eb8cf2ea7e3be55c7a 100644 GIT binary patch delta 16968 zcmZ{L2Y6f6+3<7Dy^5XK*|DAFr6`V+T+4C>NvzyBvMk%OCChta9BnDqUbZ|)fKvLk zrHl(4O3MiIBMA_aG&gJlgoHvUl$JdbAfe1M{z6*{{O`F}R-pg$KMPD_iEu>g9*cxyi8x#o9Ddy)d6sgcWy3b`B% zg@&dw=yF6PblI!9RCcAFF5YbiNK!)U|JLjr%l4;bG;)C??})&OO%XmF(MDonr#l|! zhNV`$I~3+RBfKe6QDKT`Q|?&Y9}dMQCx@Y9INQOEM)*$X>4ct5oW;*21NzZOt=i(} zMypF1}lt( z<(7y#VdoMd&E#acx?Xr>Z>gdlps@(2_cozcVfDTWl{vysOzF*$LAPy)SB@+7t>@0s znTYvAUbR_B>{Bir$%2%y$tNBW?%o&9X_NZfGP}+aw%$|I<&qERt>$)QXN7DTF^KR1 zdH_$7581S?urnEShZ0=O#&<;2o+L=RRBD|uw>{xD1>E$rM!zGo!Av%0tMv$<-BVKO zlXpo~bJ6TBejA?ve&@n6j9lEtyC_rXjlX==y%{634Mw0R>4Y0}}$kWjNjqrQz{zmwHYySo6Zh6jdHP@4! zGoCqrZ-ntD<224QJVD%`|fwnhE_~!(Ng$yf1}_$ux@2g zK7402*PmV5XXCr62t{nt()xbsa4#IFThkW-#YUA@FoPd241u$8jle#X8Yvqer0tf- zRpo`V^nDSnhY#9nTZEy5i&r&9KnrU?_wp&58k8~2q9Aczn?}4~-P0WUy`3+&~s=uh9O76=mL})*p&T+|ER2 zI2m)g%Xy_I8FD5-(3K#kUg4TQto03?W6u8`KdpM?4Gh&>B1pA4J#=S=^+s)ciXPG( z!K}Vdq$6&yBJQA|J*?X54r-<)HYn?VV>K7ct~v%RLrQaPNm}#S zwDn)$$L7`gs|ZMo?rwYF_x za@A{KP{V@oKrKoOFFmlPZ2HSIYGPs77WU)pK^U(O2qh1i7kaV>Ys=u?DU3h3beV+y zWRUNmGTH?46_lV=K~1>t!Aj%-tQ826f6pKTn^C^NKJ*LZ6fS@05d79Xe92N*cG)@` zs4Z-@TFv={Rge4*`GhAQc_Tjkg-ue6W%;OfU_;Q3>6_YN>-^7gP-thh_=jaGAxEKmbBevB^X z$ebeg;HO9H5a3Ps=3VtbbR6VsBue;416o(j zb!L~g>v&fjmhKcjd8)q5CXX1e=DM;Y0yY|Gh?Ivc%%xR*J3D+Fk z3O4SgW6oluyhbnBwCoxl9d8T5E(YlUyPi>y{U5NoLh@`osDlv&XH(UaF9&uFRU+JqbaIJm6sobZn6GAh6#?c5~H zd$vHaBM5vV?0T{Qb5X(Y?8>5Ou%e=TM^N@HC#q}KPI1*5FauN1E<;h_x6f8+cLX!* z_~H`7G*@$~?BQ)XJ`7$AXVA%hl}bW#>8*-fA^*AgP4&{&b-|3{G3sP%WYO^!dZ>CG zAE7G4s^fh?P0p;7wQehvR11$lKONTqpq9_l*9Acf8+Ek}!j;c$L=D1;=PY@>G6Dvw zxpATNqy~*=keM&+IH}Y2%U|75t~(2IG_zTgj*n8YQ|b6#O1YjOnXg3^``GBFZ&px3gvxSVLypIvw8+ z+ktYIR^Y~Ue2n^mjak6A{%J+NHK`ugadGPCTZQ1CG~mXNKQ-mFNXxbeXaDrG*?rSk z?bY#SVf2M6MIXfvxLQkvV=t_l*D3+iOh+|mK0s`dvmisw0U0&nDAz8mesTGnR*L98 zon-#>?Lya!vGWpMiQw@l=k`w9j^+$$+I9Q@#oB<5uctHJ20la`YqwW;5oCGG^4UV! zpIvz)GDhrCE_CpUKhH!cB>eWJ;=)*Vy|$$62H6dKz3|*i<#Q8WSj}bNM_^TbOj?bc zzXg0fHf+LATlP^~@0wIM7g+d;^pP2LpbxY0q;msmM9aR7juQSS2}9r z-mGby01FgL)a*#AtEng8qP4Xe4m`vlt**=RL{eVa73G}5=~tG|vBUYabQl-#@*P6n ztE-g(?;53UlTuU8^P46oM+0i9W32i-scY?8h=zJ!b*_xckVm4ND|^5$18=72w;6a7 zND?UicV69+@0IrqM!BF@Qtc|ASC%}3LDD%~Q<9I;c}4>-5#D0pEyDKKR^o^k93HF> z3OB!2Hs9y1sEEYX6ONkdDGn-N7hYQhzT7`wdkQrPkGw9xZ~Ges@aug;2WI8^H#X&^ z<-Jo;&LO<<##-zE)E_PuxHos^4a=X>QO+bBep5ArLZCMZr`{yRqw*(Hl2 zsgk@B!l*=;dNoR}0h)1V%pXZ8IfIvPHmH?m({Njdt;+@xQC*Us_O{fTi7C$LmD;56 zR>l0K-Zc%$HC^5{iO?DwkgiH8x2HIZ@SC?6BcpKF+hI^oMej^O{n~eK;w@hFSU^+L zZcuA#jVJj5 zIBPN#cZMT&N5Bo<8P}WS4Y8O#P2)@%LHge0;5cl;)72^}5=N~@-J9gblfr+_EEe{i z&cjworh4RY)TILerxjsPHWi~$u2mvu>yHFydQ6)P*jSVs$&x%|oDQW{zI$>HcI3U;mvnfq8$Q$uQ9j7tEo+d20MiI_jcvC$fIphu0dw{{D8a~7xnTz z!uRi~mUIJEjcTPo;f~o8;TRme$qPvhaEm6-F5$VRFxQ{t8k73LAuV-V!Od<=@|gx~ ztqi%x5=wultT9=u?gt6-s^R~}q(;N_3s=3rbbdcnYv__XIZdJx9)4eI>P`;H<`se% z5b;C^G_VRJOWhO(V+^^Ba+h&(Qso6*U!F8VFV{`OK4+%N`HUKUgHUy5V?mR58hHlU z0XIa!>`k0mQB>#Uy>PN=2e8ZT1s7qQYR~1ui)Tvm>*(qcBXwfygm2HR%OgHp~CZRM!uJtdV`Vgq2z4=T2aN+=jHn(1!d$7RF<2) z{5TzFG4ea8)vkx}kY?bTlX7CH-OC553o+p3Tj;!YU~}QZ4~v)quU4>~DHQJgaN$aK zikIUN&Jy62DSIFZ+|MPT7h>v|pEg1%Ve0G>6ubJPtA(#W;)KsXnjt*((OR5H2_;V~ z6;6FLPf&jRSV4H@%rIO{g@DAEGrKl?G@$&BH@*G5kc6Er^)r%jDUj--_hN}7r;2cR<@ zCMA&nssFQK-hWRMX8xlTR@D4s^Ac04qC#EQE$AfUz+rPHtjmkJ_0_Q>a_Uo;bt51RBw&LF#u~lZ%v47uz>JOHDdn*Eu>F9s#3zG6Cy;06AZDOY5N`=nv zhPUpJ&(IU)IhN z6CDli0O#=xwx)(ar!E%SzR!n*#?bc_xos)v*LO%L6~YfFa)4Wa?jypz-&a5$_OIVx zP@$4x>yC0wS=h$l%#^32DakhIo8%)Wf0z%U>SaF+mPV<30m6ZCvq&D77wV64b)e%a zIvP?^Xu%mQvLP{A_@izmJVm_OA{(Su3$KEw(adQ$dydO@ZoWG^R9DTqDuVKqWZn5l-K=nD}NO9cm=Moq<-OAbD&C+62Wv zXP{M3l;r@lE#k=>q(-Qhe4Ps)Bcy63szco*HWSrDOPq;n=Xa;55lV)%eizkVTrbI) zg&-eHYG7RpU zi|W}Z2cZ)~8>F6H4Io=o)Zl#kyW=+P=>|UT)#M&T?A#)a?7ch)|xBzH! zb1_OF4~1qm*;<0;td>5j&q2-%-LvZb8IRn+p3~Wx5(L0-40K zfhbjI5E;mYD%1t1dr5_6DUgZ0twD!Tnq0dY9aE$n6%{EmyauGwL9SebOtXxV1*R?p zDVvWLlW*3b&leayS|cO}-IJ4bVe0Ko8Ogsm)B}sPRigFSn1|1y**MLQbuyZh zDy|{Rk7=uvzU53-euTQ*!&W)h*J;fp&cu0I;Jl3u=hmADxIKv)l|%BhVt%hrZk%(p zdgXmQDsG%yRfU$Iak94xt1+bdWEq(ZbvohgxY@a!N8;4L;)5YBYBNjIHai zwU}CmjfM_W*Yr@-LgLkETVA^iPEf@)lSivj1@QBkYUIjurzNfBRdEBPYc2E-kcqYE z=Qx&@(kzRb07X(x(aQHb0A-0ZC@pRsS~07gHhZkRJ5Bo6p{^X2hwl$?4dl=|6sicN z`9%7sz)wy>&yPXm>PXXiRKB#q1LTE&p)_2GStloZtonW{Xfc(Cyl??iK(1ep97sjJ zU5{!}NYrmYr3lrL!Sg^jHIPfsLw7+@xe;xKPVYu^svwrmYDv{JH;j|>H-TdHlUb31FGHtm7;$Zmy+?AN1$zHv|ll7ALX%T~3;$~RKm6i>?pv!`XZqR%Ra zam`lVo>nXA{T2pPzP>+fihtuj_54ZpjH4Rn|X{KV}SoB%dz2r(gTAbf4Lp`kGoHE7cw_EuT z0I!w90=+X$oqe*ahRGq{>5;OcU!Kva;*#lUMLl4ZE+06zSvsUXNhcMF;2UaCUPNY* zE*0k^SMtbqVC=Mor?<2+BkA5-!SZPWai8hM%ao zA&S!Q-DP0%{LX|@-$!n{2_^GGCJDk+1D7yK zOx@X@=9Mc4?Uf!~<>sM@wNqe0(^ASQqX65}oYLzD8oEtC?pCUmYueLml$tdldi1+~ z+C3}_kze18?#m6iq}$UFG2ViDL6m-X3o1q-^1v-1{{!UJThImAZ6cd?qX;ec>_%6i z1Zlq=ZmN2Y&L`j9i*iWlbtaD#b7&U1@K$t7iC+fZ(ZIz_lv+PIV2o_J4SiD*m&c?V zxbXiwCQR3VDmNmHN%)2N&n~L=xAM&tso_>WWP;Le;{CLYxAJkR-3mD!ek=Twl4V>> zblib1orwb`(t0aeM%eq%yU0mCxDU*?ldRkiYCcKE_oJKhJSNK26EHJLRQH3&HFy9{ zlDr?CrQYEIv=ar$&|#z`-ycA?gXzERAY48!bD7lf(4aju1W$h;JPrU2A=?**oPdk` zEj(E8aPlRn|sM(nY;oA;47|A2O(fC$Si!L#giV@|n3sUa7A4E%Q85md(d zOll8#@CXtX#au%*;Jr)hLoN~?69Its2f*2g5#5963KSC`e-KP6?DX10Xzd)Qi|3VL z$@D5~o#K**QROO?!Nu7IhZ_>2?(ERc z>7H|AjpV)ekP7T%?)zxlk|q}?KZs}xN5DQ?rdI@pe;;{@Y%VSscKJPicdQNUINaM% z04(I~_tE5nQ5VE39(T+ga>`&BN#G1RA6dvVXOJCsSaTMPwUG>*MSs}Y>Ee~=fKzg# z(1w>7E+sbxwd?_vTvv9W)$R{L)*0GCsb-iSD1R_!2TF%iau(ueJsuyqm5-|u_#oN9@dYo@{QLfUvLG#s z?kCT;#%tqowKenX3=*A5KlKo6TZW+kZ$nc9pUp&=i(kgAPSU;^6_8!?a2Sn{&*$OG z(TI5Ad~Ae}hdi+ezXbTbt^ltGSe`AwOEDgClCnj(Ml4^9KWAX8o!6ZJJPMeN7oEH`TE!zhriNbuWSr+MmueqoY3Sf zc0Lt`d|z!OtWNp4VR1Q!>j6c?UWtzYSWC6I5%jb}i#5>twHDt9YI=1Qei-c#Kd8b8 zgP;3hHC~1-PIBWnsGO`?i%sO)wRkp3uf-Rm2Jt^@aV}N$iuKrlqGIEE%prD%t0qi# ze#|Z>!UjA9Ykjc+w;{I4scsNW8(~CQ)TJH{aifrXnec?PG4OojJmi$7c{?gzwF#d> zjLNAd7p=t$$z?UT50*bwgRes+O)h?z-q=ZR_d5AOKfH~IbFny=h!1fp@tQ5TkO3!Q zZ!LC%%P@Z%?niav$TmC^%^P=08V|g^L>cf#9r>975K<>TW58<>xOD%i!%AqD8u2F> z>~nqt{tFbR8}KI7LzXsT2ehUdu@;Jbjd;uAUZ*y5u??)mZw-ai^rZ>cdtDRmh8aCg z_$WApi_Q2#0En3J3Md{jq!0LbxtN@U6??=NtXM!~X&cTX zAsgNYHfmWLK5uTqC&^pJpKl@C+wdyv_6c_XBJsvH%%Zge6ig5iiEEu9Ufz-NFL)^d z>G7gmq4xoljoZmf?Z63rq@V*o07w5*2VR2O$%h^IX=v^5gl`G*WhXYlK4fzjUO~$X zx^UO>pKKL?urUNz4zM}hl|0{tTVZ~2H%NUq`KTL@m-JA_Aspi25P(VQUxnxlkSMrN zZ4oc-!Jn|GTMQ54{Rs7wvLRfr>~~IW=Zu~Fc4r_Qce_**QinzA@HVkd5Z;u$A? z3r5`K!W*G@$Aycb^E($l25#w-ZoC>CzR%pa2E|2<2ZJfHlR7VcsW9vV*kYp;HVKrt ze@ZnG7T5TIFUPCE?Rf5T zT(5BXG_@|V;5YaL!Yxkn_CFK@wnV9bWy_)iqEiaVee z;^#~6#P`#30rASaK|BHCYxm&oF!t*` zcqOEEmhZ({h1bUofK7rbxoIyhCO7QG<&ge*crS=hisbFX=OKq!w+~DhPWi~Td!+V# z_uy@qwvXNi+RY)B?Z*)eQT$a0@EVl~5Eb`@N0r<#pdlCr=bIuz3nW)fq$!QvNpk7{ z{t`^mO9$aJCb9DnC2AV{IXr=eNzq|gZrDdoKZOg)&4*!&Zu0zLybg5|EaG*uM`4F) z3XPEqp2N$?kcg8Dx?n$_-{o=x*6n`S$VT25v2L{i`eW|6f83qvPb4!$)4Kfepg$hZ z^c%_6Be0Ey#ExLy%6ixa+$(#a5^^Kp>Nv;XZ6Z9KQ*zeq5kunfBlv2B$1?KONq+x4 z7`=xc#I@MwBjv@+Dzf4soFIQZif57C58-yuXkR^q*C2~n`Y=9=uoccZK-I#dxQMm* zG#uIeXS|5CK8nv^K-7lEa0K#DKatN)@zcjZYk(8;>`|((jAZgjyh<_Zt5t`{?f|op z+ZCR{!kY}I6b>IlhISH1uRWzK$dW6kBauEUjHzEQK-$f8M?tB5y zr{z^I;ymg%-1ukwTkx}ff($dc@FiRcs1oma2`^<}6o6DpK6n`~1TW>=mjQhpV&D~g zhyj%WS}faW1or_;Q)00DUVIel{`id9?HUe=0=dk`}3Lh(l|UU(T41)%78yKqU8KtnI<^+K_xgT<+B-T-+Z%~ z7Zma26jw${=P*kY{k}}Khy``fO;U51$8k?adcvf0F4G9Jem583ZX|EbWqt!XKQNE! zK(IZ~{R+0hr4Eyh1xz*Mf`7Jvxem-WSyseco-1tvMw#p{Vmfd2~qOrOXM$w)<+D#T%D1Z=#}hpXBfN!VS33 zA88eIuM^$mI<VspoR;*Y zXCzn#??XCu8c1~;ll-u5a3nay+3ma$6>7h1F+ktU0~jj zoT5It5uOm+xsjmcptl4lXdSSf4BD7Mem3FpLS)WsnH3t37qWCxux}0}YM><#G-n2; z;DHEzhG8}cLtmGZJz8eo>=c~>)bM*lH7Rmj%T$4>`9{lJ532aeDrU*bA7#e4Cb-q7 zsm)~OmPwU)3*aHoS20D4lk$yIosoxH_QnYk3g1-$M|wz=j(L1%0+OIISdA7rfg5u0 z5-xpqJ^^VsF71)?gE5DkFEn}h7-VljcBWtK_#INubRu4Q>B0?rc%NM>KN*nd&=H_? z2stPnIz4=&Ww0YKw3Dm%^PQ0Y@ARC@GD4qpi(T&@GUuwC^1#|Z zrjtAj22H`r(C|Cp6{0-W@8CKD8ZP9ZdqG47w@9im1;b~jVAT7`r`5~?l)i2)qXjTv zV@?%@DH4I3J(R#siXg6Cs{5q6H%c9l`uVcEQb>BLMTCeZcG4F!uKDC}1hFrn64b0mZG6JQWn3KSYpKfA42lMy# zX67Rp^Gpp>mSb`7oqqKMIlYZpLT2h2eqOytt>dKsXjo?U2-_c6M0V(z4u!=5H>c#N zp1Bql>D|JVBcqt!!tBFfJS%EJY|SK8%RB-W5Vl`7OEW<^8`4Q;v3(oUf=H^KDIoV7 zm? z0|u2B8u_nztpqnPSHc{jfw>EJIE3AI6_Q`nfWP5xWaj2}c<9TDl$D%b&nza_G%}>C z!=ss?-SVpjc*55qUAO5+9Pl?k@?UwtyF+?vh|MzB?ohMBfu`X87XA=0O`(-nhL|}P zs_YPVHZdZ?Z4UDJhmg~2ZDy|dabp%?TL9%dNLvfD9VmZ)3!}q3926*(m}6nu5su0& za-o&E6gN49%H_+2)xHH}T`MyRDUH3YOaN@|VjE*yw8KGFKFzMjz=I8s0o(2%6E>iD z6M53c{0jE5wK0E&S(NZ1-_E>&SgQy8uq$7K`>H@YGfUjs33mIxI?4B)jE3CN#hfjv zgTunhNKb+@Sb6CYd=KEADrBRT?iEpYcs z{?*6C!A=MJnT;jVmYrrkLUYQaX1 zbbxsXdMgK+CWwbpgG?KPg5vQD81TSS_6w$KDcm)lyp>rF zi+pq|Q#BW-)1>VHQ$*GtU}~``EmYrAApYV2Gmh|phc?KPLyRVGm|oO?=L8Gn$=)N(O!Ba1XxoaYjXIj>6s#KF;jI2DrO9z!s49qYUH`#g$JmM;K6EZy#gou-iqHPct`z z`g!7Mz?FsM9cL;)H?2L+RMU^{76dKcmvCxp#<@VT&)Ngl3cL#a$`)y~5lytCy}NtwYM5V3(2pR~aXKd*oH75{i#sWnKhkd*n4{J?>7ETlOhd zkiyr&xOa;UuQOL;`1abH%t|PJc#}B-q4SZqm`fCp+}ZLT$Ya0Q{T|bbf!%LE!}Op= z@!K=t_Cjg+fGL9ngXs7hsJfi6i`u1#_^%I{-(EvLypUCq!xysaNZU>}ha?WM#bowQ z)(A}5zLV`FkzH&D`FbZi4h}|i7yAN4xOEq?O7QjF7qJOMn7d)Z*B7%V7Yv*DL1ozK zOh)`}mvV@UyJ$#UBz|%UI|o6Q=G&jKF9I8#yp&xzJ7}UgD;X1Za{p!QVzKDw>;?r& ziPm4S0)kym|C;RwM$%u-o@D@bE2n?SE)&1Klg*_;{_7XRsIt4+ z?K4m(x#&K&YJObGES}?mQOFChwP3T+ezp?$x@tfB8W?C8TS!XpXKT=?IB-8Zm%gvq zae%ewjL`$aHX9DHrKIX{Rv~_UfVH5d0l4V> zE6Ch?6f?!h5%zp$u2V|h!=WWcvy(R-VtEvR`x^EiG=VnvQPu;gsnnzFt$C0_TJlGB zQ4XY#v@f#tIgn|Jzsjy=Sf3NFnRdScq)d|KZ?Zqf5hpcw;)8Fp%?Na*;%#;VOwzy2 z=3#jM)Od2;;MiKIatNd&qZQ z_3#&h;IDrYRw%`j->}sP4U0eglYN)6$GhLN+gQfyR1-U+C?GYAq8ai&mof@F5cmT| z@!||P-2<}}Wq^j)XDRlukc+u=uHr+CI>qn;MO`jX>cCQkA0Fw5%a5fv#eH*u$AWE&I?!eN+Z5*6u=T%(6nD-*DYDz8xEMyM+=`tnpzC(Oq93wV-}@D5 zioLNBMJY6|A5qk@1wQ$H^rr_B;Xo6L{4OWo3Xi^|Cp9FVRO|&lDorW=2blftlme`S zNBnnM(Lw=im{7o#ibLGEQ}HC-(7I7smw3MCk%ZFeX-6b=AKOPcOb=*2(Zq43a+H}6rnsSMt{Ptk=VlFlfQ*k1(p zADsbTE;|U=kctxW#X-gGC_=6~1mH!;2Cl??e j|55QoVO^RVH*vitZY0h1rn!KV8!&N6@yTZuW$gb2f*KTk delta 16934 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 diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index d0563b9fc670f99db1fe7ed2c7fb22555367e360..1ef66b9dbd29ebfc3129c12d135db9c8bbc1916c 100644 GIT binary patch delta 89 zcmeApAUn{}F;K_T(a}?<(8pAlu*3FIncXZbA Vbac*{9+=G9#OSvDN;2zNMgZCk6w?3z diff --git a/netbox/project-static/src/htmx.ts b/netbox/project-static/src/htmx.ts index 70ed4f534..bd80a0c49 100644 --- a/netbox/project-static/src/htmx.ts +++ b/netbox/project-static/src/htmx.ts @@ -1,8 +1,10 @@ import { getElements, isTruthy } from './util'; import { initButtons } from './buttons'; +import { initSelect } from './select'; function initDepedencies(): void { - for (const init of [initButtons]) { + console.log('initDepedencies()'); + for (const init of [initButtons, initSelect]) { init(); } } diff --git a/netbox/templates/core/datafile.html b/netbox/templates/core/datafile.html new file mode 100644 index 000000000..a10f5039d --- /dev/null +++ b/netbox/templates/core/datafile.html @@ -0,0 +1,81 @@ +{% extends 'generic/object.html' %} +{% load buttons %} +{% load custom_links %} +{% load helpers %} +{% load perms %} +{% load plugins %} + +{% block breadcrumbs %} + {{ block.super }} + +{% endblock %} + +{% block controls %} +
    +
    + {% plugin_buttons object %} +
    + {% if request.user|can_delete:object %} + {% delete_button object %} + {% endif %} +
    + {% custom_links object %} +
    +
    +{% endblock controls %} + +{% block content %} +
    +
    +
    +
    Data File
    +
    + + + + + + + + + + + + + + + + + + + + + +
    Source{{ object.source }}
    Path + {{ object.path }} + + + +
    Last Updated{{ object.last_updated }}
    Size{{ object.size }} byte{{ object.size|pluralize }}
    SHA256 Hash + {{ object.hash }} + + + +
    +
    +
    +
    +
    Content
    +
    +
    {{ object.data_as_string }}
    +
    +
    + {% plugin_left_page object %} +
    +
    +
    +
    + {% plugin_full_width_page object %} +
    +
    +{% endblock %} diff --git a/netbox/templates/core/datasource.html b/netbox/templates/core/datasource.html new file mode 100644 index 000000000..168ced700 --- /dev/null +++ b/netbox/templates/core/datasource.html @@ -0,0 +1,114 @@ +{% extends 'generic/object.html' %} +{% load static %} +{% load helpers %} +{% load plugins %} +{% load render_table from django_tables2 %} + +{% block extra_controls %} + {% if perms.core.sync_datasource %} + {% if object.ready_for_sync %} + + {% csrf_token %} + + + {% else %} + + {% endif %} + {% endif %} +{% endblock %} + +{% block content %} +
    +
    +
    +
    Data Source
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Name{{ object.name }}
    Type{{ object.get_type_display }}
    Enabled{% checkmark object.enabled %}
    Status{% badge object.get_status_display bg_color=object.get_status_color %}
    Last synced{{ object.last_synced|placeholder }}
    Description{{ object.description|placeholder }}
    URL + {{ object.url }} +
    Ignore rules + {% if object.ignore_rules %} +
    {{ object.ignore_rules }}
    + {% else %} + {{ ''|placeholder }} + {% endif %}
    +
    +
    + {% include 'inc/panels/tags.html' %} + {% include 'inc/panels/comments.html' %} + {% plugin_left_page object %} +
    +
    +
    +
    Backend
    +
    + + {% for name, field in object.get_backend.parameters.items %} + + + + + {% empty %} + + + + {% endfor %} +
    {{ field.label }}{{ object.parameters|get_key:name|placeholder }}
    + No parameters defined +
    +
    +
    + {% include 'inc/panels/related_objects.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% plugin_right_page object %} +
    +
    +
    +
    +
    +
    Files
    +
    +
    + {% plugin_full_width_page object %} +
    +
    +{% endblock %} diff --git a/netbox/utilities/files.py b/netbox/utilities/files.py new file mode 100644 index 000000000..68afe2962 --- /dev/null +++ b/netbox/utilities/files.py @@ -0,0 +1,9 @@ +import hashlib + + +def sha256_hash(filepath): + """ + Return the SHA256 hash of the file at the specified path. + """ + with open(filepath, 'rb') as f: + return hashlib.sha256(f.read()) From 0be633d62400a8a91ebc11eda7667708df884b24 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 2 Feb 2023 12:46:49 -0500 Subject: [PATCH 022/174] #11558: Fix URL display under data source view --- netbox/templates/core/datasource.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/core/datasource.html b/netbox/templates/core/datasource.html index 168ced700..061017ad7 100644 --- a/netbox/templates/core/datasource.html +++ b/netbox/templates/core/datasource.html @@ -55,7 +55,7 @@ URL - {{ object.url }} + {{ object.source_url }} From 664132281e6d41b58b1d52d36573ccdc9dee140b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 3 Feb 2023 14:57:24 -0500 Subject: [PATCH 023/174] Fixes #11659: Include all relevant DataFile attributes during bulk update --- netbox/core/models/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/core/models/data.py b/netbox/core/models/data.py index 5ad048b0f..54e1dca04 100644 --- a/netbox/core/models/data.py +++ b/netbox/core/models/data.py @@ -168,7 +168,7 @@ class DataSource(PrimaryModel): continue # Bulk update modified files - updated_count = DataFile.objects.bulk_update(updated_files, ['hash']) + updated_count = DataFile.objects.bulk_update(updated_files, ('last_updated', 'size', 'hash', 'data')) logger.debug(f"Updated {updated_count} files") # Bulk delete deleted files From 678a7d17df00a7bd8538d7d6788b3169311446b6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 7 Feb 2023 16:44:05 -0500 Subject: [PATCH 024/174] Closes #9073: Remote data support for config contexts (#11692) * WIP * Add bulk sync view for config contexts * Introduce 'sync' permission for synced data models * Docs & cleanup * Remove unused method * Add a REST API endpoint to synchronize config context data --- docs/models/extras/configcontext.md | 4 + docs/release-notes/version-3.5.md | 1 + netbox/core/models/data.py | 8 + netbox/extras/api/serializers.py | 10 +- netbox/extras/api/views.py | 6 +- netbox/extras/constants.py | 1 + netbox/extras/filtersets.py | 11 +- netbox/extras/forms/filtersets.py | 15 ++ netbox/extras/forms/mixins.py | 20 +- netbox/extras/forms/model_forms.py | 18 +- .../0085_configcontext_synced_data.py | 35 ++++ netbox/extras/models/configcontexts.py | 12 +- netbox/extras/tables/tables.py | 17 +- netbox/extras/urls.py | 1 + netbox/extras/views.py | 7 +- netbox/netbox/api/features.py | 30 +++ netbox/netbox/models/features.py | 84 ++++++++- netbox/netbox/views/generic/feature_views.py | 56 +++++- netbox/templates/extras/configcontext.html | 174 ++++++++++-------- .../templates/extras/configcontext_list.html | 10 + 20 files changed, 426 insertions(+), 94 deletions(-) create mode 100644 netbox/extras/migrations/0085_configcontext_synced_data.py create mode 100644 netbox/netbox/api/features.py create mode 100644 netbox/templates/extras/configcontext_list.html diff --git a/docs/models/extras/configcontext.md b/docs/models/extras/configcontext.md index 156b2d784..1e58b9e01 100644 --- a/docs/models/extras/configcontext.md +++ b/docs/models/extras/configcontext.md @@ -18,6 +18,10 @@ A numeric value which influences the order in which context data is merged. Cont The context data expressed in JSON format. +### Data File + +Config context data may optionally be sourced from a remote [data file](../core/datafile.md), which is synchronized from a remote data source. When designating a data file, there is no need to specify local data for the config context: It will be populated automatically from the data file. + ### Is Active If not selected, this config context will be excluded from rendering. This can be convenient to temporarily disable a config context. diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index ae2d319b3..985953d47 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -4,6 +4,7 @@ ### Enhancements +* [#9073](https://github.com/netbox-community/netbox/issues/9073) - Enable syncing config context data from remote sources * [#11254](https://github.com/netbox-community/netbox/issues/11254) - Introduce the `X-Request-ID` HTTP header to annotate the unique ID of each request for change logging * [#11440](https://github.com/netbox-community/netbox/issues/11440) - Add an `enabled` field for device type interfaces * [#11517](https://github.com/netbox-community/netbox/issues/11517) - Standardize the inclusion of related objects across the entire UI diff --git a/netbox/core/models/data.py b/netbox/core/models/data.py index 54e1dca04..4228c599c 100644 --- a/netbox/core/models/data.py +++ b/netbox/core/models/data.py @@ -1,5 +1,6 @@ import logging import os +import yaml from fnmatch import fnmatchcase from urllib.parse import urlparse @@ -283,6 +284,13 @@ class DataFile(ChangeLoggingMixin, models.Model): except UnicodeDecodeError: return None + def get_data(self): + """ + Attempt to read the file data as JSON/YAML and return a native Python object. + """ + # TODO: Something more robust + return yaml.safe_load(self.data_as_string) + def refresh_from_disk(self, source_root): """ Update instance attributes from the file on disk. Returns True if any attribute diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 8b9c6dcb1..54627fbb3 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -4,6 +4,7 @@ from django.core.exceptions import ObjectDoesNotExist from drf_yasg.utils import swagger_serializer_method from rest_framework import serializers +from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer from dcim.api.nested_serializers import ( NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer, NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer, @@ -358,13 +359,20 @@ class ConfigContextSerializer(ValidatedModelSerializer): required=False, many=True ) + data_source = NestedDataSourceSerializer( + required=False + ) + data_file = NestedDataFileSerializer( + read_only=True + ) class Meta: model = ConfigContext fields = [ 'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', - 'tenant_groups', 'tenants', 'tags', 'data', 'created', 'last_updated', + 'tenant_groups', 'tenants', 'tags', 'data_source', 'data_path', 'data_file', 'data_synced', 'data', + 'created', 'last_updated', ] diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 1423824cd..8b97491b1 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -17,6 +17,7 @@ from extras.models import CustomField from extras.reports import get_report, get_reports, run_report from extras.scripts import get_script, get_scripts, run_script from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired +from netbox.api.features import SyncedDataMixin from netbox.api.metadata import ContentTypeMetadata from netbox.api.viewsets import NetBoxModelViewSet from utilities.exceptions import RQWorkerNotRunningException @@ -147,9 +148,10 @@ class JournalEntryViewSet(NetBoxModelViewSet): # Config contexts # -class ConfigContextViewSet(NetBoxModelViewSet): +class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet): queryset = ConfigContext.objects.prefetch_related( - 'regions', 'site_groups', 'sites', 'locations', 'roles', 'platforms', 'tenant_groups', 'tenants', + 'regions', 'site_groups', 'sites', 'locations', 'roles', 'platforms', 'tenant_groups', 'tenants', 'data_source', + 'data_file', ) serializer_class = serializers.ConfigContextSerializer filterset_class = filtersets.ConfigContextFilterSet diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index 123eb0a45..7c7fe331e 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -8,6 +8,7 @@ EXTRAS_FEATURES = [ 'export_templates', 'job_results', 'journaling', + 'synced_data', 'tags', 'webhooks' ] diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 74b98ccf6..799e79123 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -4,6 +4,7 @@ from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.utils.translation import gettext as _ +from core.models import DataFile, DataSource from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet from tenancy.models import Tenant, TenantGroup @@ -422,10 +423,18 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet): to_field_name='slug', label=_('Tag (slug)'), ) + data_source_id = django_filters.ModelMultipleChoiceFilter( + queryset=DataSource.objects.all(), + label=_('Data source (ID)'), + ) + data_file_id = django_filters.ModelMultipleChoiceFilter( + queryset=DataSource.objects.all(), + label=_('Data file (ID)'), + ) class Meta: model = ConfigContext - fields = ['id', 'name', 'is_active'] + fields = ['id', 'name', 'is_active', 'data_synced'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 22c7364db..46b7aa8f6 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -3,6 +3,7 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ +from core.models import DataFile, DataSource from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from extras.choices import * from extras.models import * @@ -257,11 +258,25 @@ class TagFilterForm(SavedFiltersMixin, FilterForm): class ConfigContextFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id', 'tag_id')), + ('Data', ('data_source_id', 'data_file_id')), ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')), ('Device', ('device_type_id', 'platform_id', 'role_id')), ('Cluster', ('cluster_type_id', 'cluster_group_id', 'cluster_id')), ('Tenant', ('tenant_group_id', 'tenant_id')) ) + data_source_id = DynamicModelMultipleChoiceField( + queryset=DataSource.objects.all(), + required=False, + label=_('Data source') + ) + data_file_id = DynamicModelMultipleChoiceField( + queryset=DataFile.objects.all(), + required=False, + label=_('Data file'), + query_params={ + 'source_id': '$data_source_id' + } + ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, diff --git a/netbox/extras/forms/mixins.py b/netbox/extras/forms/mixins.py index 640bcc3dc..4e05e3a1e 100644 --- a/netbox/extras/forms/mixins.py +++ b/netbox/extras/forms/mixins.py @@ -2,13 +2,15 @@ from django.contrib.contenttypes.models import ContentType from django import forms from django.utils.translation import gettext as _ +from core.models import DataFile, DataSource from extras.models import * from extras.choices import CustomFieldVisibilityChoices -from utilities.forms.fields import DynamicModelMultipleChoiceField +from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField __all__ = ( 'CustomFieldsMixin', 'SavedFiltersMixin', + 'SyncedDataMixin', ) @@ -72,3 +74,19 @@ class SavedFiltersMixin(forms.Form): 'usable': True, } ) + + +class SyncedDataMixin(forms.Form): + data_source = DynamicModelChoiceField( + queryset=DataSource.objects.all(), + required=False, + label=_('Data source') + ) + data_file = DynamicModelChoiceField( + queryset=DataFile.objects.all(), + required=False, + label=_('File'), + query_params={ + 'source_id': '$data_source', + } + ) diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index a21cf21e2..429c4140a 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -5,6 +5,7 @@ from django.utils.translation import gettext as _ from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from extras.choices import * +from extras.forms.mixins import SyncedDataMixin from extras.models import * from extras.utils import FeatureQuery from netbox.forms import NetBoxModelForm @@ -183,7 +184,7 @@ class TagForm(BootstrapMixin, forms.ModelForm): ] -class ConfigContextForm(BootstrapMixin, forms.ModelForm): +class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm): regions = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False @@ -236,10 +237,13 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm): queryset=Tag.objects.all(), required=False ) - data = JSONField() + data = JSONField( + required=False + ) fieldsets = ( ('Config Context', ('name', 'weight', 'description', 'data', 'is_active')), + ('Data Source', ('data_source', 'data_file')), ('Assignment', ( 'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', @@ -251,9 +255,17 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm): fields = ( 'name', 'weight', 'description', 'data', 'is_active', 'regions', 'site_groups', 'sites', 'locations', 'roles', 'device_types', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups', - 'tenants', 'tags', + 'tenants', 'tags', 'data_source', 'data_file', ) + def clean(self): + super().clean() + + if not self.cleaned_data.get('data') and not self.cleaned_data.get('data_source'): + raise forms.ValidationError("Must specify either local data or a data source") + + return self.cleaned_data + class ImageAttachmentForm(BootstrapMixin, forms.ModelForm): diff --git a/netbox/extras/migrations/0085_configcontext_synced_data.py b/netbox/extras/migrations/0085_configcontext_synced_data.py new file mode 100644 index 000000000..f3022665b --- /dev/null +++ b/netbox/extras/migrations/0085_configcontext_synced_data.py @@ -0,0 +1,35 @@ +# Generated by Django 4.1.6 on 2023-02-06 15:34 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ('extras', '0084_staging'), + ] + + operations = [ + migrations.AddField( + model_name='configcontext', + name='data_file', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.datafile'), + ), + migrations.AddField( + model_name='configcontext', + name='data_path', + field=models.CharField(blank=True, editable=False, max_length=1000), + ), + migrations.AddField( + model_name='configcontext', + name='data_source', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource'), + ), + migrations.AddField( + model_name='configcontext', + name='data_synced', + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + ] diff --git a/netbox/extras/models/configcontexts.py b/netbox/extras/models/configcontexts.py index d8d3510d7..7b6088324 100644 --- a/netbox/extras/models/configcontexts.py +++ b/netbox/extras/models/configcontexts.py @@ -2,10 +2,11 @@ from django.conf import settings from django.core.validators import ValidationError from django.db import models from django.urls import reverse +from django.utils import timezone from extras.querysets import ConfigContextQuerySet from netbox.models import ChangeLoggedModel -from netbox.models.features import WebhooksMixin +from netbox.models.features import SyncedDataMixin, WebhooksMixin from utilities.utils import deepmerge @@ -19,7 +20,7 @@ __all__ = ( # Config contexts # -class ConfigContext(WebhooksMixin, ChangeLoggedModel): +class ConfigContext(SyncedDataMixin, WebhooksMixin, ChangeLoggedModel): """ A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B @@ -130,6 +131,13 @@ class ConfigContext(WebhooksMixin, ChangeLoggedModel): {'data': 'JSON data must be in object form. Example: {"foo": 123}'} ) + def sync_data(self): + """ + Synchronize context data from the designated DataFile (if any). + """ + self.data = self.data_file.get_data() + self.data_synced = timezone.now() + class ConfigContextModel(models.Model): """ diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index c2b8c9424..51443ad87 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -188,21 +188,30 @@ class TaggedItemTable(NetBoxTable): class ConfigContextTable(NetBoxTable): + data_source = tables.Column( + linkify=True + ) + data_file = tables.Column( + linkify=True + ) name = tables.Column( linkify=True ) is_active = columns.BooleanColumn( verbose_name='Active' ) + is_synced = columns.BooleanColumn( + verbose_name='Synced' + ) class Meta(NetBoxTable.Meta): model = ConfigContext fields = ( - 'pk', 'id', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'locations', 'roles', - 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'created', - 'last_updated', + 'pk', 'id', 'name', 'weight', 'is_active', 'is_synced', 'description', 'regions', 'sites', 'locations', + 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', + 'data_source', 'data_file', 'data_synced', 'created', 'last_updated', ) - default_columns = ('pk', 'name', 'weight', 'is_active', 'description') + default_columns = ('pk', 'name', 'weight', 'is_active', 'is_synced', 'description') class ObjectChangeTable(NetBoxTable): diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index f41a45f5a..6fd178284 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -60,6 +60,7 @@ urlpatterns = [ path('config-contexts/add/', views.ConfigContextEditView.as_view(), name='configcontext_add'), path('config-contexts/edit/', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'), path('config-contexts/delete/', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'), + path('config-contexts/sync/', views.ConfigContextBulkSyncDataView.as_view(), name='configcontext_bulk_sync'), path('config-contexts//', include(get_model_urls('extras', 'configcontext'))), # Image attachments diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 2d2608ae8..c46890c19 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -352,7 +352,8 @@ class ConfigContextListView(generic.ObjectListView): filterset = filtersets.ConfigContextFilterSet filterset_form = forms.ConfigContextFilterForm table = tables.ConfigContextTable - actions = ('add', 'bulk_edit', 'bulk_delete') + template_name = 'extras/configcontext_list.html' + actions = ('add', 'bulk_edit', 'bulk_delete', 'bulk_sync') @register_model_view(ConfigContext) @@ -416,6 +417,10 @@ class ConfigContextBulkDeleteView(generic.BulkDeleteView): table = tables.ConfigContextTable +class ConfigContextBulkSyncDataView(generic.BulkSyncDataView): + queryset = ConfigContext.objects.all() + + class ObjectConfigContextView(generic.ObjectView): base_template = None template_name = 'extras/object_configcontext.html' diff --git a/netbox/netbox/api/features.py b/netbox/netbox/api/features.py new file mode 100644 index 000000000..db018ca12 --- /dev/null +++ b/netbox/netbox/api/features.py @@ -0,0 +1,30 @@ +from django.shortcuts import get_object_or_404 +from rest_framework.decorators import action +from rest_framework.exceptions import PermissionDenied +from rest_framework.response import Response + +from utilities.permissions import get_permission_for_model + +__all__ = ( + 'SyncedDataMixin', +) + + +class SyncedDataMixin: + + @action(detail=True, methods=['post']) + def sync(self, request, pk): + """ + Provide a /sync API endpoint to synchronize an object's data from its associated DataFile (if any). + """ + permission = get_permission_for_model(self.queryset.model, 'sync') + if not request.user.has_perm(permission): + raise PermissionDenied(f"Missing permission: {permission}") + + obj = get_object_or_404(self.queryset, pk=pk) + if obj.data_file: + obj.sync_data() + obj.save() + serializer = self.serializer_class(obj, context={'request': request}) + + return Response(serializer.data) diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index f041d016d..2bd0a93d2 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -2,11 +2,12 @@ from collections import defaultdict from functools import cached_property from django.contrib.contenttypes.fields import GenericRelation -from django.db.models.signals import class_prepared -from django.dispatch import receiver - from django.core.validators import ValidationError from django.db import models +from django.db.models.signals import class_prepared +from django.dispatch import receiver +from django.utils.translation import gettext as _ + from taggit.managers import TaggableManager from extras.choices import CustomFieldVisibilityChoices, ObjectChangeActionChoices @@ -25,6 +26,7 @@ __all__ = ( 'ExportTemplatesMixin', 'JobResultsMixin', 'JournalingMixin', + 'SyncedDataMixin', 'TagsMixin', 'WebhooksMixin', ) @@ -317,12 +319,82 @@ class WebhooksMixin(models.Model): abstract = True +class SyncedDataMixin(models.Model): + """ + Enables population of local data from a DataFile object, synchronized from a remote DatSource. + """ + data_source = models.ForeignKey( + to='core.DataSource', + on_delete=models.PROTECT, + blank=True, + null=True, + related_name='+', + help_text=_("Remote data source") + ) + data_file = models.ForeignKey( + to='core.DataFile', + on_delete=models.SET_NULL, + blank=True, + null=True, + related_name='+' + ) + data_path = models.CharField( + max_length=1000, + blank=True, + editable=False, + help_text=_("Path to remote file (relative to data source root)") + ) + data_synced = models.DateTimeField( + blank=True, + null=True, + editable=False + ) + + class Meta: + abstract = True + + @property + def is_synced(self): + return self.data_file and self.data_synced >= self.data_file.last_updated + + def clean(self): + if self.data_file: + self.sync_data() + self.data_path = self.data_file.path + + if self.data_source and not self.data_file: + raise ValidationError({ + 'data_file': _(f"Must specify a data file when designating a data source.") + }) + if self.data_file and not self.data_source: + self.data_source = self.data_file.source + + super().clean() + + def resolve_data_file(self): + """ + Determine the designated DataFile object identified by its parent DataSource and its path. Returns None if + either attribute is unset, or if no matching DataFile is found. + """ + from core.models import DataFile + + if self.data_source and self.data_path: + try: + return DataFile.objects.get(source=self.data_source, path=self.data_path) + except DataFile.DoesNotExist: + pass + + def sync_data(self): + raise NotImplementedError(f"{self.__class__} must implement a sync_data() method.") + + FEATURES_MAP = ( ('custom_fields', CustomFieldsMixin), ('custom_links', CustomLinksMixin), ('export_templates', ExportTemplatesMixin), ('job_results', JobResultsMixin), ('journaling', JournalingMixin), + ('synced_data', SyncedDataMixin), ('tags', TagsMixin), ('webhooks', WebhooksMixin), ) @@ -348,3 +420,9 @@ def _register_features(sender, **kwargs): 'changelog', kwargs={'model': sender} )('netbox.views.generic.ObjectChangeLogView') + if issubclass(sender, SyncedDataMixin): + register_model_view( + sender, + 'sync', + kwargs={'model': sender} + )('netbox.views.generic.ObjectSyncDataView') diff --git a/netbox/netbox/views/generic/feature_views.py b/netbox/netbox/views/generic/feature_views.py index d4d02ee4e..6e310c97a 100644 --- a/netbox/netbox/views/generic/feature_views.py +++ b/netbox/netbox/views/generic/feature_views.py @@ -1,16 +1,22 @@ from django.contrib.contenttypes.models import ContentType +from django.contrib import messages +from django.db import transaction from django.db.models import Q -from django.shortcuts import get_object_or_404, render +from django.shortcuts import get_object_or_404, redirect, render from django.utils.translation import gettext as _ from django.views.generic import View from extras import forms, tables from extras.models import * -from utilities.views import ViewTab +from utilities.permissions import get_permission_for_model +from utilities.views import GetReturnURLMixin, ViewTab +from .base import BaseMultiObjectView __all__ = ( + 'BulkSyncDataView', 'ObjectChangeLogView', 'ObjectJournalView', + 'ObjectSyncDataView', ) @@ -126,3 +132,49 @@ class ObjectJournalView(View): 'base_template': self.base_template, 'tab': self.tab, }) + + +class ObjectSyncDataView(View): + + def post(self, request, model, **kwargs): + """ + Synchronize data from the DataFile associated with this object. + """ + qs = model.objects.all() + if hasattr(model.objects, 'restrict'): + qs = qs.restrict(request.user, 'sync') + obj = get_object_or_404(qs, **kwargs) + + if not obj.data_file: + messages.error(request, f"Unable to synchronize data: No data file set.") + return redirect(obj.get_absolute_url()) + + obj.sync_data() + obj.save() + messages.success(request, f"Synchronized data for {model._meta.verbose_name} {obj}.") + + return redirect(obj.get_absolute_url()) + + +class BulkSyncDataView(GetReturnURLMixin, BaseMultiObjectView): + """ + Synchronize multiple instances of a model inheriting from SyncedDataMixin. + """ + def get_required_permission(self): + return get_permission_for_model(self.queryset.model, 'sync') + + def post(self, request): + selected_objects = self.queryset.filter( + pk__in=request.POST.getlist('pk'), + data_file__isnull=False + ) + + with transaction.atomic(): + for obj in selected_objects: + obj.sync_data() + obj.save() + + model_name = self.queryset.model._meta.verbose_name_plural + messages.success(request, f"Synced {len(selected_objects)} {model_name}") + + return redirect(self.get_return_url(request)) diff --git a/netbox/templates/extras/configcontext.html b/netbox/templates/extras/configcontext.html index 56ec52c07..3714b3f1c 100644 --- a/netbox/templates/extras/configcontext.html +++ b/netbox/templates/extras/configcontext.html @@ -3,81 +3,107 @@ {% load static %} {% block content %} -
    -
    -
    -
    - Config Context -
    -
    - - - - - - - - - - - - - - - - - -
    Name - {{ object.name }} -
    Weight - {{ object.weight }} -
    Description{{ object.description|placeholder }}
    Active - {% if object.is_active %} - - - - {% else %} - - - - {% endif %} -
    -
    -
    -
    -
    - Assignment -
    -
    - - {% for title, objects in assigned_objects %} - - - - - {% endfor %} -
    {{ title }} -
      - {% for object in objects %} -
    • {{ object|linkify }}
    • - {% empty %} -
    • None
    • - {% endfor %} -
    -
    -
    -
    +
    +
    +
    +
    Config Context
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Name{{ object.name }}
    Weight{{ object.weight }}
    Description{{ object.description|placeholder }}
    Active{% checkmark object.is_active %}
    Data Source + {% if object.data_source %} + {{ object.data_source }} + {% else %} + {{ ''|placeholder }} + {% endif %} +
    Data File + {% if object.data_file %} + {{ object.data_file }} + {% elif object.data_path %} +
    + +
    + {{ object.data_path }} + {% else %} + {{ ''|placeholder }} + {% endif %} +
    Data Synced{{ object.data_synced|placeholder }}
    -
    -
    -
    -
    Data
    - {% include 'extras/inc/configcontext_format.html' %} -
    -
    - {% include 'extras/inc/configcontext_data.html' with data=object.data format=format %} -
    -
    +
    +
    +
    Assignment
    +
    + + {% for title, objects in assigned_objects %} + + + + + {% endfor %} +
    {{ title }} +
      + {% for object in objects %} +
    • {{ object|linkify }}
    • + {% empty %} +
    • None
    • + {% endfor %} +
    +
    +
    +
    +
    +
    +
    Data
    + {% include 'extras/inc/configcontext_format.html' %} +
    +
    + {% if object.data_file and object.data_file.last_updated > object.data_synced %} + + {% endif %} + {% include 'extras/inc/configcontext_data.html' with data=object.data format=format %} +
    +
    +
    +
    {% endblock %} diff --git a/netbox/templates/extras/configcontext_list.html b/netbox/templates/extras/configcontext_list.html new file mode 100644 index 000000000..31e7087ad --- /dev/null +++ b/netbox/templates/extras/configcontext_list.html @@ -0,0 +1,10 @@ +{% extends 'generic/object_list.html' %} + +{% block bulk_buttons %} + {% if perms.extras.sync_configcontext %} + + {% endif %} + {{ block.super }} +{% endblock %} From ac87ce733deb366605a5aeefa68f1b8cebddea28 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 8 Feb 2023 18:24:18 -0500 Subject: [PATCH 025/174] Closes #11693: Enable remote data synchronization for export templates --- docs/models/extras/exporttemplate.md | 4 + netbox/extras/api/serializers.py | 9 +- netbox/extras/api/views.py | 4 +- netbox/extras/filtersets.py | 10 +- netbox/extras/forms/filtersets.py | 16 ++- netbox/extras/forms/model_forms.py | 21 ++- .../0086_exporttemplate_synced_data.py | 35 +++++ netbox/extras/models/models.py | 12 +- netbox/extras/tables/tables.py | 13 +- netbox/extras/urls.py | 1 + netbox/extras/views.py | 6 + netbox/templates/extras/configcontext.html | 26 +--- netbox/templates/extras/exporttemplate.html | 134 +++++++++++------- .../templates/extras/exporttemplate_list.html | 10 ++ netbox/templates/inc/sync_warning.html | 13 ++ netbox/utilities/templates/buttons/sync.html | 6 + netbox/utilities/templatetags/buttons.py | 10 ++ netbox/utilities/templatetags/perms.py | 5 + 18 files changed, 246 insertions(+), 89 deletions(-) create mode 100644 netbox/extras/migrations/0086_exporttemplate_synced_data.py create mode 100644 netbox/templates/extras/exporttemplate_list.html create mode 100644 netbox/templates/inc/sync_warning.html create mode 100644 netbox/utilities/templates/buttons/sync.html diff --git a/docs/models/extras/exporttemplate.md b/docs/models/extras/exporttemplate.md index 3215201b3..d2f9292c6 100644 --- a/docs/models/extras/exporttemplate.md +++ b/docs/models/extras/exporttemplate.md @@ -12,6 +12,10 @@ The name of the export template. This will appear in the "export" dropdown list The type of NetBox object to which the export template applies. +### Data File + +Template code may optionally be sourced from a remote [data file](../core/datafile.md), which is synchronized from a remote data source. When designating a data file, there is no need to specify local content for the template: It will be populated automatically from the data file. + ### Template Code Jinja2 template code for rendering the exported data. diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 54627fbb3..6a8248548 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -142,12 +142,19 @@ class ExportTemplateSerializer(ValidatedModelSerializer): queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()), many=True ) + data_source = NestedDataSourceSerializer( + required=False + ) + data_file = NestedDataFileSerializer( + read_only=True + ) class Meta: model = ExportTemplate fields = [ 'id', 'url', 'display', 'content_types', 'name', 'description', 'template_code', 'mime_type', - 'file_extension', 'as_attachment', 'created', 'last_updated', + 'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created', + 'last_updated', ] diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 8b97491b1..190b32f53 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -92,9 +92,9 @@ class CustomLinkViewSet(NetBoxModelViewSet): # Export templates # -class ExportTemplateViewSet(NetBoxModelViewSet): +class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet): metadata_class = ContentTypeMetadata - queryset = ExportTemplate.objects.all() + queryset = ExportTemplate.objects.prefetch_related('data_source', 'data_file') serializer_class = serializers.ExportTemplateSerializer filterset_class = filtersets.ExportTemplateFilterSet diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 799e79123..f7f34e17a 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -127,10 +127,18 @@ class ExportTemplateFilterSet(BaseFilterSet): field_name='content_types__id' ) content_types = ContentTypeFilter() + data_source_id = django_filters.ModelMultipleChoiceFilter( + queryset=DataSource.objects.all(), + label=_('Data source (ID)'), + ) + data_file_id = django_filters.ModelMultipleChoiceFilter( + queryset=DataSource.objects.all(), + label=_('Data file (ID)'), + ) class Meta: model = ExportTemplate - fields = ['id', 'content_types', 'name', 'description'] + fields = ['id', 'content_types', 'name', 'description', 'data_synced'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 46b7aa8f6..5c7a10ac8 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -21,9 +21,9 @@ from .mixins import SavedFiltersMixin __all__ = ( 'ConfigContextFilterForm', 'CustomFieldFilterForm', - 'JobResultFilterForm', 'CustomLinkFilterForm', 'ExportTemplateFilterForm', + 'JobResultFilterForm', 'JournalEntryFilterForm', 'LocalConfigContextFilterForm', 'ObjectChangeFilterForm', @@ -157,8 +157,22 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm): class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), + ('Data', ('data_source_id', 'data_file_id')), ('Attributes', ('content_types', 'mime_type', 'file_extension', 'as_attachment')), ) + data_source_id = DynamicModelMultipleChoiceField( + queryset=DataSource.objects.all(), + required=False, + label=_('Data source') + ) + data_file_id = DynamicModelMultipleChoiceField( + queryset=DataFile.objects.all(), + required=False, + label=_('Data file'), + query_params={ + 'source_id': '$data_source_id' + } + ) content_types = ContentTypeMultipleChoiceField( queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()), required=False diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 429c4140a..0ffc5117c 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -96,19 +96,28 @@ class ExportTemplateForm(BootstrapMixin, forms.ModelForm): queryset=ContentType.objects.all(), limit_choices_to=FeatureQuery('export_templates') ) + template_code = forms.CharField( + required=False, + widget=forms.Textarea(attrs={'class': 'font-monospace'}) + ) fieldsets = ( ('Export Template', ('name', 'content_types', 'description')), - ('Template', ('template_code',)), + ('Content', ('data_source', 'data_file', 'template_code',)), ('Rendering', ('mime_type', 'file_extension', 'as_attachment')), ) class Meta: model = ExportTemplate fields = '__all__' - widgets = { - 'template_code': forms.Textarea(attrs={'class': 'font-monospace'}), - } + + def clean(self): + super().clean() + + if not self.cleaned_data.get('template_code') and not self.cleaned_data.get('data_file'): + raise forms.ValidationError("Must specify either local content or a data file") + + return self.cleaned_data class SavedFilterForm(BootstrapMixin, forms.ModelForm): @@ -261,8 +270,8 @@ class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm): def clean(self): super().clean() - if not self.cleaned_data.get('data') and not self.cleaned_data.get('data_source'): - raise forms.ValidationError("Must specify either local data or a data source") + if not self.cleaned_data.get('data') and not self.cleaned_data.get('data_file'): + raise forms.ValidationError("Must specify either local data or a data file") return self.cleaned_data diff --git a/netbox/extras/migrations/0086_exporttemplate_synced_data.py b/netbox/extras/migrations/0086_exporttemplate_synced_data.py new file mode 100644 index 000000000..87de6b71c --- /dev/null +++ b/netbox/extras/migrations/0086_exporttemplate_synced_data.py @@ -0,0 +1,35 @@ +# Generated by Django 4.1.6 on 2023-02-08 22:16 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ('extras', '0085_configcontext_synced_data'), + ] + + operations = [ + migrations.AddField( + model_name='exporttemplate', + name='data_file', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.datafile'), + ), + migrations.AddField( + model_name='exporttemplate', + name='data_path', + field=models.CharField(blank=True, editable=False, max_length=1000), + ), + migrations.AddField( + model_name='exporttemplate', + name='data_source', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource'), + ), + migrations.AddField( + model_name='exporttemplate', + name='data_synced', + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + ] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index df32d6ac4..63a1e199e 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -26,7 +26,8 @@ from netbox.config import get_config from netbox.constants import RQ_QUEUE_DEFAULT from netbox.models import ChangeLoggedModel from netbox.models.features import ( - CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, JobResultsMixin, TagsMixin, WebhooksMixin, + CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, JobResultsMixin, SyncedDataMixin, + TagsMixin, WebhooksMixin, ) from utilities.querysets import RestrictedQuerySet from utilities.utils import render_jinja2 @@ -281,7 +282,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogged } -class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel): +class ExportTemplate(SyncedDataMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel): content_types = models.ManyToManyField( to=ContentType, related_name='export_templates', @@ -335,6 +336,13 @@ class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel): 'name': f'"{self.name}" is a reserved name. Please choose a different name.' }) + def sync_data(self): + """ + Synchronize template content from the designated DataFile (if any). + """ + self.template_code = self.data_file.data_as_string + self.data_synced = timezone.now() + def render(self, queryset): """ Render the contents of the template. diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index 51443ad87..6b2f34de4 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -90,15 +90,24 @@ class ExportTemplateTable(NetBoxTable): ) content_types = columns.ContentTypesColumn() as_attachment = columns.BooleanColumn() + data_source = tables.Column( + linkify=True + ) + data_file = tables.Column( + linkify=True + ) + is_synced = columns.BooleanColumn( + verbose_name='Synced' + ) class Meta(NetBoxTable.Meta): model = ExportTemplate fields = ( 'pk', 'id', 'name', 'content_types', 'description', 'mime_type', 'file_extension', 'as_attachment', - 'created', 'last_updated', + 'data_source', 'data_file', 'data_synced', 'created', 'last_updated', ) default_columns = ( - 'pk', 'name', 'content_types', 'description', 'mime_type', 'file_extension', 'as_attachment', + 'pk', 'name', 'content_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'is_synced', ) diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index 6fd178284..dabb9f977 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -29,6 +29,7 @@ urlpatterns = [ path('export-templates/import/', views.ExportTemplateBulkImportView.as_view(), name='exporttemplate_import'), path('export-templates/edit/', views.ExportTemplateBulkEditView.as_view(), name='exporttemplate_bulk_edit'), path('export-templates/delete/', views.ExportTemplateBulkDeleteView.as_view(), name='exporttemplate_bulk_delete'), + path('export-templates/sync/', views.ExportTemplateBulkSyncDataView.as_view(), name='exporttemplate_bulk_sync'), path('export-templates//', include(get_model_urls('extras', 'exporttemplate'))), # Saved filters diff --git a/netbox/extras/views.py b/netbox/extras/views.py index c46890c19..de06b5739 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -121,6 +121,8 @@ class ExportTemplateListView(generic.ObjectListView): filterset = filtersets.ExportTemplateFilterSet filterset_form = forms.ExportTemplateFilterForm table = tables.ExportTemplateTable + template_name = 'extras/exporttemplate_list.html' + actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_sync') @register_model_view(ExportTemplate) @@ -158,6 +160,10 @@ class ExportTemplateBulkDeleteView(generic.BulkDeleteView): table = tables.ExportTemplateTable +class ExportTemplateBulkSyncDataView(generic.BulkSyncDataView): + queryset = ExportTemplate.objects.all() + + # # Saved filters # diff --git a/netbox/templates/extras/configcontext.html b/netbox/templates/extras/configcontext.html index 3714b3f1c..e9513a3a8 100644 --- a/netbox/templates/extras/configcontext.html +++ b/netbox/templates/extras/configcontext.html @@ -50,10 +50,10 @@ {% endif %} - - Data Synced - {{ object.data_synced|placeholder }} - + + Data Synced + {{ object.data_synced|placeholder }} +
    @@ -86,22 +86,8 @@ {% include 'extras/inc/configcontext_format.html' %}
    - {% if object.data_file and object.data_file.last_updated > object.data_synced %} - - {% endif %} - {% include 'extras/inc/configcontext_data.html' with data=object.data format=format %} + {% include 'inc/sync_warning.html' %} + {% include 'extras/inc/configcontext_data.html' with data=object.data format=format %}
    diff --git a/netbox/templates/extras/exporttemplate.html b/netbox/templates/extras/exporttemplate.html index d14294355..a80db8fca 100644 --- a/netbox/templates/extras/exporttemplate.html +++ b/netbox/templates/extras/exporttemplate.html @@ -10,66 +10,92 @@ {% endblock %} {% block content %} -
    -
    -
    -
    - Export Template -
    -
    - - - - - - - - - - - - - - - - - - - - - -
    Name{{ object.name }}
    Description{{ object.description|placeholder }}
    MIME Type{{ object.mime_type|placeholder }}
    File Extension{{ object.file_extension|placeholder }}
    Attachment{% checkmark object.as_attachment %}
    -
    -
    -
    -
    Assigned Models
    -
    - - {% for ct in object.content_types.all %} +
    +
    +
    +
    Export Template
    +
    +
    - + + - {% endfor %} -
    {{ ct }}Name{{ object.name }}
    + + Description + {{ object.description|placeholder }} + + + MIME Type + {{ object.mime_type|placeholder }} + + + File Extension + {{ object.file_extension|placeholder }} + + + Attachment + {% checkmark object.as_attachment %} + + + Data Source + + {% if object.data_source %} + {{ object.data_source }} + {% else %} + {{ ''|placeholder }} + {% endif %} + + + + Data File + + {% if object.data_file %} + {{ object.data_file }} + {% elif object.data_path %} +
    + +
    + {{ object.data_path }} + {% else %} + {{ ''|placeholder }} + {% endif %} + + + + Data Synced + {{ object.data_synced|placeholder }} + + +
    -
    - {% plugin_left_page object %} -
    -
    -
    -
    - Template -
    -
    -
    {{ object.template_code }}
    +
    +
    Assigned Models
    +
    + + {% for ct in object.content_types.all %} + + + + {% endfor %} +
    {{ ct }}
    +
    + {% plugin_left_page object %} +
    +
    +
    +
    Template
    +
    + {% include 'inc/sync_warning.html' %} +
    {{ object.template_code }}
    +
    +
    + {% plugin_right_page object %}
    - {% plugin_right_page object %}
    -
    -
    +
    - {% plugin_full_width_page object %} + {% plugin_full_width_page object %}
    -
    +
    {% endblock %} diff --git a/netbox/templates/extras/exporttemplate_list.html b/netbox/templates/extras/exporttemplate_list.html new file mode 100644 index 000000000..c79f9259a --- /dev/null +++ b/netbox/templates/extras/exporttemplate_list.html @@ -0,0 +1,10 @@ +{% extends 'generic/object_list.html' %} + +{% block bulk_buttons %} + {% if perms.extras.sync_configcontext %} + + {% endif %} + {{ block.super }} +{% endblock %} diff --git a/netbox/templates/inc/sync_warning.html b/netbox/templates/inc/sync_warning.html new file mode 100644 index 000000000..1ffc77e15 --- /dev/null +++ b/netbox/templates/inc/sync_warning.html @@ -0,0 +1,13 @@ +{% load buttons %} +{% load perms %} + +{% if object.data_file and object.data_file.last_updated > object.data_synced %} + +{% endif %} diff --git a/netbox/utilities/templates/buttons/sync.html b/netbox/utilities/templates/buttons/sync.html new file mode 100644 index 000000000..58f2b95cc --- /dev/null +++ b/netbox/utilities/templates/buttons/sync.html @@ -0,0 +1,6 @@ +
    + {% csrf_token %} + +
    diff --git a/netbox/utilities/templatetags/buttons.py b/netbox/utilities/templatetags/buttons.py index bcdb099d8..8a706ebeb 100644 --- a/netbox/utilities/templatetags/buttons.py +++ b/netbox/utilities/templatetags/buttons.py @@ -46,6 +46,16 @@ def delete_button(instance): } +@register.inclusion_tag('buttons/sync.html') +def sync_button(instance): + viewname = get_viewname(instance, 'sync') + url = reverse(viewname, kwargs={'pk': instance.pk}) + + return { + 'url': url, + } + + # # List buttons # diff --git a/netbox/utilities/templatetags/perms.py b/netbox/utilities/templatetags/perms.py index f1bbf7549..809c74ad1 100644 --- a/netbox/utilities/templatetags/perms.py +++ b/netbox/utilities/templatetags/perms.py @@ -28,3 +28,8 @@ def can_change(user, instance): @register.filter() def can_delete(user, instance): return _check_permission(user, instance, 'delete') + + +@register.filter() +def can_sync(user, instance): + return _check_permission(user, instance, 'sync') From b267cbae36edc2a975504f58a2fce74bc1372575 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 9 Feb 2023 08:53:21 -0500 Subject: [PATCH 026/174] Merge migrations --- ...ext_synced_data.py => 0085_synced_data.py} | 24 +++++++++++-- .../0086_exporttemplate_synced_data.py | 35 ------------------- 2 files changed, 22 insertions(+), 37 deletions(-) rename netbox/extras/migrations/{0085_configcontext_synced_data.py => 0085_synced_data.py} (54%) delete mode 100644 netbox/extras/migrations/0086_exporttemplate_synced_data.py diff --git a/netbox/extras/migrations/0085_configcontext_synced_data.py b/netbox/extras/migrations/0085_synced_data.py similarity index 54% rename from netbox/extras/migrations/0085_configcontext_synced_data.py rename to netbox/extras/migrations/0085_synced_data.py index f3022665b..4790cd51a 100644 --- a/netbox/extras/migrations/0085_configcontext_synced_data.py +++ b/netbox/extras/migrations/0085_synced_data.py @@ -1,5 +1,3 @@ -# Generated by Django 4.1.6 on 2023-02-06 15:34 - from django.db import migrations, models import django.db.models.deletion @@ -12,6 +10,7 @@ class Migration(migrations.Migration): ] operations = [ + # ConfigContexts migrations.AddField( model_name='configcontext', name='data_file', @@ -32,4 +31,25 @@ class Migration(migrations.Migration): name='data_synced', field=models.DateTimeField(blank=True, editable=False, null=True), ), + # ExportTemplates + migrations.AddField( + model_name='exporttemplate', + name='data_file', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.datafile'), + ), + migrations.AddField( + model_name='exporttemplate', + name='data_path', + field=models.CharField(blank=True, editable=False, max_length=1000), + ), + migrations.AddField( + model_name='exporttemplate', + name='data_source', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource'), + ), + migrations.AddField( + model_name='exporttemplate', + name='data_synced', + field=models.DateTimeField(blank=True, editable=False, null=True), + ), ] diff --git a/netbox/extras/migrations/0086_exporttemplate_synced_data.py b/netbox/extras/migrations/0086_exporttemplate_synced_data.py deleted file mode 100644 index 87de6b71c..000000000 --- a/netbox/extras/migrations/0086_exporttemplate_synced_data.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 4.1.6 on 2023-02-08 22:16 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0001_initial'), - ('extras', '0085_configcontext_synced_data'), - ] - - operations = [ - migrations.AddField( - model_name='exporttemplate', - name='data_file', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.datafile'), - ), - migrations.AddField( - model_name='exporttemplate', - name='data_path', - field=models.CharField(blank=True, editable=False, max_length=1000), - ), - migrations.AddField( - model_name='exporttemplate', - name='data_source', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource'), - ), - migrations.AddField( - model_name='exporttemplate', - name='data_synced', - field=models.DateTimeField(blank=True, editable=False, null=True), - ), - ] From c8faca01f10179f1adbb17bca8956fbdc58866da Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 9 Feb 2023 09:22:43 -0500 Subject: [PATCH 027/174] Changelog for #11693 --- docs/release-notes/version-3.5.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 985953d47..6df759f6c 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -10,6 +10,7 @@ * [#11517](https://github.com/netbox-community/netbox/issues/11517) - Standardize the inclusion of related objects across the entire UI * [#11584](https://github.com/netbox-community/netbox/issues/11584) - Add a list view for contact assignments * [#11625](https://github.com/netbox-community/netbox/issues/11625) - Add HTMX support to ObjectEditView +* [#11693](https://github.com/netbox-community/netbox/issues/11693) - Enable syncing export template content from remote sources ### Other Changes From 8d68b6a2e60aa42afae7ffa4852f420356bcea1d Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Fri, 10 Feb 2023 22:29:34 +0100 Subject: [PATCH 028/174] Fixes #11694 - Remove obsolete SmallTextarea widget --- netbox/circuits/forms/bulk_edit.py | 9 ++++----- netbox/core/forms/bulk_edit.py | 4 ++-- netbox/dcim/forms/bulk_edit.py | 24 ++++++++++++------------ netbox/dcim/forms/model_forms.py | 8 ++++---- netbox/ipam/forms/bulk_edit.py | 24 ++++++++++++------------ netbox/tenancy/forms/bulk_edit.py | 4 ++-- netbox/tenancy/forms/model_forms.py | 4 ++-- netbox/utilities/forms/widgets.py | 8 -------- netbox/virtualization/forms/bulk_edit.py | 6 +++--- netbox/wireless/forms/bulk_edit.py | 6 +++--- 10 files changed, 44 insertions(+), 53 deletions(-) diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index e1fe6338d..dd6e103e4 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -7,8 +7,7 @@ from ipam.models import ASN from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import ( - add_blank_choice, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, - StaticSelect, + add_blank_choice, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect, ) __all__ = ( @@ -35,7 +34,7 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label=_('Comments') ) @@ -63,7 +62,7 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label=_('Comments') ) @@ -125,7 +124,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label=_('Comments') ) diff --git a/netbox/core/forms/bulk_edit.py b/netbox/core/forms/bulk_edit.py index c5713b626..6fb562db6 100644 --- a/netbox/core/forms/bulk_edit.py +++ b/netbox/core/forms/bulk_edit.py @@ -5,7 +5,7 @@ from core.choices import DataSourceTypeChoices from core.models import * from netbox.forms import NetBoxModelBulkEditForm from utilities.forms import ( - add_blank_choice, BulkEditNullBooleanSelect, CommentField, SmallTextarea, StaticSelect, + add_blank_choice, BulkEditNullBooleanSelect, CommentField, StaticSelect, ) __all__ = ( @@ -30,7 +30,7 @@ class DataSourceBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label=_('Comments') ) parameters = forms.JSONField( diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 8969b1e69..d9770db40 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -11,7 +11,7 @@ from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField, - DynamicModelMultipleChoiceField, form_from_model, SmallTextarea, StaticSelect, SelectSpeedWidget, + DynamicModelMultipleChoiceField, form_from_model, StaticSelect, SelectSpeedWidget, ) __all__ = ( @@ -138,7 +138,7 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -309,7 +309,7 @@ class RackBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -345,7 +345,7 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -406,7 +406,7 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -441,7 +441,7 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -551,7 +551,7 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -594,7 +594,7 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -644,7 +644,7 @@ class CableBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -668,7 +668,7 @@ class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -714,7 +714,7 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -776,7 +776,7 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label=_('Comments') ) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 703a7a6b4..44e2e3526 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -12,7 +12,7 @@ from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms import ( APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField, - DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK, SmallTextarea, + DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK, SlugField, StaticSelect, SelectSpeedWidget, ) from virtualization.models import Cluster, ClusterGroup @@ -149,12 +149,12 @@ class SiteForm(TenancyForm, NetBoxModelForm): 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags', ) widgets = { - 'physical_address': SmallTextarea( + 'physical_address': forms.Textarea( attrs={ 'rows': 3, } ), - 'shipping_address': SmallTextarea( + 'shipping_address': forms.Textarea( attrs={ 'rows': 3, } @@ -470,7 +470,7 @@ class PlatformForm(NetBoxModelForm): 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags', ] widgets = { - 'napalm_args': SmallTextarea(), + 'napalm_args': forms.Textarea(), } diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index d0af43975..ed5ca53f5 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -10,7 +10,7 @@ from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, BulkEditNullBooleanSelect, CommentField, DynamicModelChoiceField, NumericArrayField, - SmallTextarea, StaticSelect, DynamicModelMultipleChoiceField, + StaticSelect, DynamicModelMultipleChoiceField, ) __all__ = ( @@ -48,7 +48,7 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -69,7 +69,7 @@ class RouteTargetBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -116,7 +116,7 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -145,7 +145,7 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -227,7 +227,7 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -266,7 +266,7 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -314,7 +314,7 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -359,7 +359,7 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -442,7 +442,7 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -474,7 +474,7 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -504,7 +504,7 @@ class L2VPNBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) diff --git a/netbox/tenancy/forms/bulk_edit.py b/netbox/tenancy/forms/bulk_edit.py index 183a8e851..eda256a57 100644 --- a/netbox/tenancy/forms/bulk_edit.py +++ b/netbox/tenancy/forms/bulk_edit.py @@ -2,7 +2,7 @@ from django import forms from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import * -from utilities.forms import CommentField, DynamicModelChoiceField, SmallTextarea +from utilities.forms import CommentField, DynamicModelChoiceField __all__ = ( 'ContactBulkEditForm', @@ -106,7 +106,7 @@ class ContactBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) diff --git a/netbox/tenancy/forms/model_forms.py b/netbox/tenancy/forms/model_forms.py index b466c94b2..e835194ff 100644 --- a/netbox/tenancy/forms/model_forms.py +++ b/netbox/tenancy/forms/model_forms.py @@ -3,7 +3,7 @@ from django import forms from netbox.forms import NetBoxModelForm from tenancy.models import * from utilities.forms import ( - BootstrapMixin, CommentField, DynamicModelChoiceField, SlugField, SmallTextarea, StaticSelect, + BootstrapMixin, CommentField, DynamicModelChoiceField, SlugField, StaticSelect, ) __all__ = ( @@ -112,7 +112,7 @@ class ContactForm(NetBoxModelForm): 'group', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments', 'tags', ) widgets = { - 'address': SmallTextarea(attrs={'rows': 3}), + 'address': forms.Textarea(attrs={'rows': 3}), } diff --git a/netbox/utilities/forms/widgets.py b/netbox/utilities/forms/widgets.py index 1802306f1..16ec72ecf 100644 --- a/netbox/utilities/forms/widgets.py +++ b/netbox/utilities/forms/widgets.py @@ -21,7 +21,6 @@ __all__ = ( 'SelectSpeedWidget', 'SelectWithPK', 'SlugWidget', - 'SmallTextarea', 'StaticSelect', 'StaticSelectMultiple', 'TimePicker', @@ -33,13 +32,6 @@ QueryParam = Dict[str, QueryParamValue] ProcessedParams = Sequence[Dict[str, Sequence[JSONPrimitive]]] -class SmallTextarea(forms.Textarea): - """ - Subclass used for rendering a smaller textarea element. - """ - pass - - class SlugWidget(forms.TextInput): """ Subclass TextInput and add a slug regeneration button next to the form field. diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py index 14ae89c37..bce04ffc7 100644 --- a/netbox/virtualization/forms/bulk_edit.py +++ b/netbox/virtualization/forms/bulk_edit.py @@ -9,7 +9,7 @@ from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, BulkEditNullBooleanSelect, BulkRenameForm, CommentField, DynamicModelChoiceField, - DynamicModelMultipleChoiceField, SmallTextarea, StaticSelect + DynamicModelMultipleChoiceField, StaticSelect ) from virtualization.choices import * from virtualization.models import * @@ -90,7 +90,7 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label=_('Comments') ) @@ -163,7 +163,7 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label=_('Comments') ) diff --git a/netbox/wireless/forms/bulk_edit.py b/netbox/wireless/forms/bulk_edit.py index be54faf9e..c0e265270 100644 --- a/netbox/wireless/forms/bulk_edit.py +++ b/netbox/wireless/forms/bulk_edit.py @@ -5,7 +5,7 @@ from dcim.choices import LinkStatusChoices from ipam.models import VLAN from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant -from utilities.forms import add_blank_choice, CommentField, DynamicModelChoiceField, SmallTextarea +from utilities.forms import add_blank_choice, CommentField, DynamicModelChoiceField from wireless.choices import * from wireless.constants import SSID_MAX_LENGTH from wireless.models import * @@ -74,7 +74,7 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) @@ -119,7 +119,7 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, + widget=forms.Textarea, label='Comments' ) From 96a79c212688a31ffe174a9f0e2cddc616908598 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Sat, 11 Feb 2023 16:16:06 -0500 Subject: [PATCH 029/174] Closes #11737: ChangeLoggedModel should inherit WebhooksMixin --- netbox/circuits/models/circuits.py | 3 --- netbox/dcim/models/device_component_templates.py | 3 +-- netbox/extras/models/configcontexts.py | 5 ++--- netbox/extras/models/customfields.py | 4 ++-- netbox/extras/models/models.py | 14 +++++++------- netbox/extras/models/tags.py | 4 ++-- netbox/ipam/models/fhrp.py | 3 +-- netbox/netbox/models/__init__.py | 2 +- netbox/tenancy/models/contacts.py | 3 +-- 9 files changed, 17 insertions(+), 24 deletions(-) diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py index eba7f4de0..a04d78d9f 100644 --- a/netbox/circuits/models/circuits.py +++ b/netbox/circuits/models/circuits.py @@ -1,4 +1,3 @@ -from django.apps import apps from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from django.db import models @@ -10,7 +9,6 @@ from dcim.models import CabledObjectModel from netbox.models import ( ChangeLoggedModel, CustomFieldsMixin, CustomLinksMixin, OrganizationalModel, PrimaryModel, TagsMixin, ) -from netbox.models.features import WebhooksMixin __all__ = ( 'Circuit', @@ -132,7 +130,6 @@ class CircuitTermination( CustomFieldsMixin, CustomLinksMixin, TagsMixin, - WebhooksMixin, ChangeLoggedModel, CabledObjectModel ): diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 3d2d32509..be17627fb 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -9,7 +9,6 @@ from mptt.models import MPTTModel, TreeForeignKey from dcim.choices import * from dcim.constants import * from netbox.models import ChangeLoggedModel -from netbox.models.features import WebhooksMixin from utilities.fields import ColorField, NaturalOrderingField from utilities.mptt import TreeManager from utilities.ordering import naturalize_interface @@ -33,7 +32,7 @@ __all__ = ( ) -class ComponentTemplateModel(WebhooksMixin, ChangeLoggedModel): +class ComponentTemplateModel(ChangeLoggedModel): device_type = models.ForeignKey( to='dcim.DeviceType', on_delete=models.CASCADE, diff --git a/netbox/extras/models/configcontexts.py b/netbox/extras/models/configcontexts.py index 7b6088324..eed8babcd 100644 --- a/netbox/extras/models/configcontexts.py +++ b/netbox/extras/models/configcontexts.py @@ -6,10 +6,9 @@ from django.utils import timezone from extras.querysets import ConfigContextQuerySet from netbox.models import ChangeLoggedModel -from netbox.models.features import SyncedDataMixin, WebhooksMixin +from netbox.models.features import SyncedDataMixin from utilities.utils import deepmerge - __all__ = ( 'ConfigContext', 'ConfigContextModel', @@ -20,7 +19,7 @@ __all__ = ( # Config contexts # -class ConfigContext(SyncedDataMixin, WebhooksMixin, ChangeLoggedModel): +class ConfigContext(SyncedDataMixin, ChangeLoggedModel): """ A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index fa16b8501..021a2005a 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -17,7 +17,7 @@ from django.utils.translation import gettext as _ from extras.choices import * from extras.utils import FeatureQuery from netbox.models import ChangeLoggedModel -from netbox.models.features import CloningMixin, ExportTemplatesMixin, WebhooksMixin +from netbox.models.features import CloningMixin, ExportTemplatesMixin from netbox.search import FieldTypes from utilities import filters from utilities.forms.fields import ( @@ -56,7 +56,7 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)): return self.get_queryset().filter(content_types=content_type) -class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel): +class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): content_types = models.ManyToManyField( to=ContentType, related_name='custom_fields', diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 63a1e199e..1360904dc 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -27,7 +27,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT from netbox.models import ChangeLoggedModel from netbox.models.features import ( CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, JobResultsMixin, SyncedDataMixin, - TagsMixin, WebhooksMixin, + TagsMixin, ) from utilities.querysets import RestrictedQuerySet from utilities.utils import render_jinja2 @@ -46,7 +46,7 @@ __all__ = ( ) -class Webhook(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel): +class Webhook(ExportTemplatesMixin, ChangeLoggedModel): """ A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or delete in NetBox. The request will contain a representation of the object, which the remote application can act on. @@ -203,7 +203,7 @@ class Webhook(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel): return render_jinja2(self.payload_url, context) -class CustomLink(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel): +class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): """ A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template code to be rendered with an object as context. @@ -282,7 +282,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogged } -class ExportTemplate(SyncedDataMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel): +class ExportTemplate(SyncedDataMixin, ExportTemplatesMixin, ChangeLoggedModel): content_types = models.ManyToManyField( to=ContentType, related_name='export_templates', @@ -376,7 +376,7 @@ class ExportTemplate(SyncedDataMixin, ExportTemplatesMixin, WebhooksMixin, Chang return response -class SavedFilter(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel): +class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): """ A set of predefined keyword parameters that can be reused to filter for specific objects. """ @@ -447,7 +447,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge return qd.urlencode() -class ImageAttachment(WebhooksMixin, ChangeLoggedModel): +class ImageAttachment(ChangeLoggedModel): """ An uploaded image which is associated with an object. """ @@ -523,7 +523,7 @@ class ImageAttachment(WebhooksMixin, ChangeLoggedModel): return objectchange -class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, WebhooksMixin, ExportTemplatesMixin, ChangeLoggedModel): +class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplatesMixin, ChangeLoggedModel): """ A historical remark concerning an object; collectively, these form an object's journal. The journal is used to preserve historical context around an object, and complements NetBox's built-in change logging. For example, you diff --git a/netbox/extras/models/tags.py b/netbox/extras/models/tags.py index 827d969e3..b980f0709 100644 --- a/netbox/extras/models/tags.py +++ b/netbox/extras/models/tags.py @@ -5,7 +5,7 @@ from django.utils.text import slugify from taggit.models import TagBase, GenericTaggedItemBase from netbox.models import ChangeLoggedModel -from netbox.models.features import ExportTemplatesMixin, WebhooksMixin +from netbox.models.features import ExportTemplatesMixin from utilities.choices import ColorChoices from utilities.fields import ColorField @@ -14,7 +14,7 @@ from utilities.fields import ColorField # Tags # -class Tag(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel, TagBase): +class Tag(ExportTemplatesMixin, ChangeLoggedModel, TagBase): id = models.BigAutoField( primary_key=True ) diff --git a/netbox/ipam/models/fhrp.py b/netbox/ipam/models/fhrp.py index 759a6e1d3..1044a5cde 100644 --- a/netbox/ipam/models/fhrp.py +++ b/netbox/ipam/models/fhrp.py @@ -5,7 +5,6 @@ from django.db import models from django.urls import reverse from netbox.models import ChangeLoggedModel, PrimaryModel -from netbox.models.features import WebhooksMixin from ipam.choices import * from ipam.constants import * @@ -73,7 +72,7 @@ class FHRPGroup(PrimaryModel): return reverse('ipam:fhrpgroup', args=[self.pk]) -class FHRPGroupAssignment(WebhooksMixin, ChangeLoggedModel): +class FHRPGroupAssignment(ChangeLoggedModel): interface_type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index a4c8e0ec2..db8179fdc 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -38,7 +38,7 @@ class NetBoxFeatureSet( # Base model classes # -class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, models.Model): +class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, WebhooksMixin, models.Model): """ Base model for ancillary models; provides limited functionality for models which don't support NetBox's full feature set. diff --git a/netbox/tenancy/models/contacts.py b/netbox/tenancy/models/contacts.py index 4fa8d87cb..440541b5f 100644 --- a/netbox/tenancy/models/contacts.py +++ b/netbox/tenancy/models/contacts.py @@ -4,7 +4,6 @@ from django.db import models from django.urls import reverse from netbox.models import ChangeLoggedModel, NestedGroupModel, OrganizationalModel, PrimaryModel -from netbox.models.features import WebhooksMixin from tenancy.choices import * __all__ = ( @@ -93,7 +92,7 @@ class Contact(PrimaryModel): return reverse('tenancy:contact', args=[self.pk]) -class ContactAssignment(WebhooksMixin, ChangeLoggedModel): +class ContactAssignment(ChangeLoggedModel): content_type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE From 62509c20dab6cc24dca6cdc66fd4ead78a272172 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Sat, 11 Feb 2023 16:30:17 -0500 Subject: [PATCH 030/174] Check for change records only if objects being deleted support change logging --- netbox/utilities/testing/views.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/netbox/utilities/testing/views.py b/netbox/utilities/testing/views.py index 0a16c4b3b..4a1b2207d 100644 --- a/netbox/utilities/testing/views.py +++ b/netbox/utilities/testing/views.py @@ -8,6 +8,7 @@ from django.urls import reverse from extras.choices import ObjectChangeActionChoices from extras.models import ObjectChange +from netbox.models.features import ChangeLoggingMixin from users.models import ObjectPermission from utilities.choices import ImportFormatChoices from .base import ModelTestCase @@ -350,12 +351,13 @@ class ViewTestCases: self._get_queryset().get(pk=instance.pk) # Verify ObjectChange creation - objectchanges = ObjectChange.objects.filter( - changed_object_type=ContentType.objects.get_for_model(instance), - changed_object_id=instance.pk - ) - self.assertEqual(len(objectchanges), 1) - self.assertEqual(objectchanges[0].action, ObjectChangeActionChoices.ACTION_DELETE) + if issubclass(instance.__class__, ChangeLoggingMixin): + objectchanges = ObjectChange.objects.filter( + changed_object_type=ContentType.objects.get_for_model(instance), + changed_object_id=instance.pk + ) + self.assertEqual(len(objectchanges), 1) + self.assertEqual(objectchanges[0].action, ObjectChangeActionChoices.ACTION_DELETE) @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_delete_object_with_constrained_permission(self): From a1c9f7a2c64f93a19356c1bfe3a0069964ca9f25 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Sat, 11 Feb 2023 16:31:00 -0500 Subject: [PATCH 031/174] DataFile should not inherit from ChangeLoggingMixin --- netbox/core/migrations/0001_initial.py | 4 ++-- netbox/core/models/data.py | 11 +++++++---- netbox/core/tests/test_views.py | 1 - 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/netbox/core/migrations/0001_initial.py b/netbox/core/migrations/0001_initial.py index 803ac3b13..37c3b617e 100644 --- a/netbox/core/migrations/0001_initial.py +++ b/netbox/core/migrations/0001_initial.py @@ -43,9 +43,9 @@ class Migration(migrations.Migration): name='DataFile', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), - ('created', models.DateTimeField(auto_now_add=True, null=True)), - ('path', models.CharField(editable=False, max_length=1000)), + ('created', models.DateTimeField(auto_now_add=True)), ('last_updated', models.DateTimeField(editable=False)), + ('path', models.CharField(editable=False, max_length=1000)), ('size', models.PositiveIntegerField(editable=False)), ('hash', models.CharField(editable=False, max_length=64, validators=[django.core.validators.RegexValidator(message='Length must be 64 hexadecimal characters.', regex='^[0-9a-f]{64}$')])), ('data', models.BinaryField()), diff --git a/netbox/core/models/data.py b/netbox/core/models/data.py index 4228c599c..67ab4a6c7 100644 --- a/netbox/core/models/data.py +++ b/netbox/core/models/data.py @@ -228,11 +228,17 @@ class DataSource(PrimaryModel): return False -class DataFile(ChangeLoggingMixin, models.Model): +class DataFile(models.Model): """ The database representation of a remote file fetched from a remote DataSource. DataFile instances should be created, updated, or deleted only by calling DataSource.sync(). """ + created = models.DateTimeField( + auto_now_add=True + ) + last_updated = models.DateTimeField( + editable=False + ) source = models.ForeignKey( to='core.DataSource', on_delete=models.CASCADE, @@ -244,9 +250,6 @@ class DataFile(ChangeLoggingMixin, models.Model): editable=False, help_text=_("File path relative to the data source's root") ) - last_updated = models.DateTimeField( - editable=False - ) size = models.PositiveIntegerField( editable=False ) diff --git a/netbox/core/tests/test_views.py b/netbox/core/tests/test_views.py index fbee031ed..4a50a8d05 100644 --- a/netbox/core/tests/test_views.py +++ b/netbox/core/tests/test_views.py @@ -50,7 +50,6 @@ class DataSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase): class DataFileTestCase( ViewTestCases.GetObjectViewTestCase, - ViewTestCases.GetObjectChangelogViewTestCase, ViewTestCases.DeleteObjectViewTestCase, ViewTestCases.ListObjectsViewTestCase, ViewTestCases.BulkDeleteObjectsViewTestCase, From 81b8046d1d416399f2229f0dd9fcb831af74ee3d Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Fri, 10 Feb 2023 21:57:23 +0100 Subject: [PATCH 032/174] Fixes #9653 - Add default_platform to DeviceType --- netbox/dcim/api/serializers.py | 3 ++- netbox/dcim/filtersets.py | 10 ++++++++++ netbox/dcim/forms/bulk_edit.py | 6 +++++- netbox/dcim/forms/bulk_import.py | 9 +++++++-- netbox/dcim/forms/filtersets.py | 7 ++++++- netbox/dcim/forms/model_forms.py | 8 ++++++-- .../0169_devicetype_default_platform.py | 19 +++++++++++++++++++ netbox/dcim/models/devices.py | 15 ++++++++++++++- netbox/dcim/tables/devicetypes.py | 5 ++++- netbox/dcim/tests/test_filtersets.py | 18 ++++++++++++++++-- netbox/dcim/tests/test_views.py | 17 +++++++++++++++-- netbox/templates/dcim/devicetype.html | 4 ++++ 12 files changed, 108 insertions(+), 13 deletions(-) create mode 100644 netbox/dcim/migrations/0169_devicetype_default_platform.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 1f4f3ff5f..379d71b0d 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -309,6 +309,7 @@ class ManufacturerSerializer(NetBoxModelSerializer): class DeviceTypeSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail') manufacturer = NestedManufacturerSerializer() + default_platform = NestedPlatformSerializer(required=False, allow_null=True) u_height = serializers.DecimalField( max_digits=4, decimal_places=1, @@ -324,7 +325,7 @@ class DeviceTypeSerializer(NetBoxModelSerializer): class Meta: model = DeviceType fields = [ - 'id', 'url', 'display', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', + 'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', ] diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 83ae8bcc9..774f8a41f 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -436,6 +436,16 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet): to_field_name='slug', label=_('Manufacturer (slug)'), ) + default_platform_id = django_filters.ModelMultipleChoiceFilter( + queryset=Platform.objects.all(), + label=_('Default platform (ID)'), + ) + default_platform = django_filters.ModelMultipleChoiceFilter( + field_name='default_platform__slug', + queryset=Platform.objects.all(), + to_field_name='slug', + label=_('Default platform (slug)'), + ) has_front_image = django_filters.BooleanFilter( label=_('Has a front image'), method='_has_front_image' diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index d9770db40..e5b896f9f 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -374,6 +374,10 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm): queryset=Manufacturer.objects.all(), required=False ) + default_platform = DynamicModelChoiceField( + queryset=Platform.objects.all(), + required=False + ) part_number = forms.CharField( required=False ) @@ -412,7 +416,7 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm): model = DeviceType fieldsets = ( - ('Device Type', ('manufacturer', 'part_number', 'u_height', 'is_full_depth', 'airflow', 'description')), + ('Device Type', ('manufacturer', 'default_platform', 'part_number', 'u_height', 'is_full_depth', 'airflow', 'description')), ('Weight', ('weight', 'weight_unit')), ) nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments') diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 3f016899e..1e8abcac6 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -281,12 +281,17 @@ class DeviceTypeImportForm(NetBoxModelImportForm): queryset=Manufacturer.objects.all(), to_field_name='name' ) + default_platform = forms.ModelChoiceField( + queryset=Platform.objects.all(), + to_field_name='name', + required=False, + ) class Meta: model = DeviceType fields = [ - 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', - 'description', 'comments', + 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', + 'subdevice_role', 'airflow', 'description', 'comments', ] diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 4dd2f73eb..7e2b4d2d8 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -378,7 +378,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm): model = DeviceType fieldsets = ( (None, ('q', 'filter_id', 'tag')), - ('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow')), + ('Hardware', ('manufacturer_id', 'default_platform_id', 'part_number', 'subdevice_role', 'airflow')), ('Images', ('has_front_image', 'has_rear_image')), ('Components', ( 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', @@ -391,6 +391,11 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm): required=False, label=_('Manufacturer') ) + default_platform_id = DynamicModelMultipleChoiceField( + queryset=Platform.objects.all(), + required=False, + label=_('Default platform') + ) part_number = forms.CharField( required=False ) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 44e2e3526..14217d2d6 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -378,13 +378,17 @@ class DeviceTypeForm(NetBoxModelForm): manufacturer = DynamicModelChoiceField( queryset=Manufacturer.objects.all() ) + default_platform = DynamicModelChoiceField( + queryset=Platform.objects.all(), + required=False + ) slug = SlugField( slug_source='model' ) comments = CommentField() fieldsets = ( - ('Device Type', ('manufacturer', 'model', 'slug', 'description', 'tags')), + ('Device Type', ('manufacturer', 'model', 'slug', 'description', 'tags', 'default_platform')), ('Chassis', ( 'u_height', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow', 'weight', 'weight_unit', )), @@ -395,7 +399,7 @@ class DeviceTypeForm(NetBoxModelForm): model = DeviceType fields = [ 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', - 'weight', 'weight_unit', 'front_image', 'rear_image', 'description', 'comments', 'tags', + 'weight', 'weight_unit', 'front_image', 'rear_image', 'description', 'comments', 'tags', 'default_platform' ] widgets = { 'airflow': StaticSelect(), diff --git a/netbox/dcim/migrations/0169_devicetype_default_platform.py b/netbox/dcim/migrations/0169_devicetype_default_platform.py new file mode 100644 index 000000000..a143f2c62 --- /dev/null +++ b/netbox/dcim/migrations/0169_devicetype_default_platform.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1.6 on 2023-02-10 18:06 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0168_interface_template_enabled'), + ] + + operations = [ + migrations.AddField( + model_name='devicetype', + name='default_platform', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.platform'), + ), + ] diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 603129228..94f61aba7 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -82,6 +82,14 @@ class DeviceType(PrimaryModel, WeightMixin): slug = models.SlugField( max_length=100 ) + default_platform = models.ForeignKey( + to='dcim.Platform', + on_delete=models.SET_NULL, + related_name='+', + blank=True, + null=True, + verbose_name='Default platform' + ) part_number = models.CharField( max_length=50, blank=True, @@ -121,7 +129,7 @@ class DeviceType(PrimaryModel, WeightMixin): ) clone_fields = ( - 'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit' + 'manufacturer', 'default_platform', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit' ) prerequisite_models = ( 'dcim.Manufacturer', @@ -165,6 +173,7 @@ class DeviceType(PrimaryModel, WeightMixin): 'manufacturer': self.manufacturer.name, 'model': self.model, 'slug': self.slug, + 'default_platform': self.default_platform.name if self.default_platform else None, 'part_number': self.part_number, 'u_height': float(self.u_height), 'is_full_depth': self.is_full_depth, @@ -801,6 +810,10 @@ class Device(PrimaryModel, ConfigContextModel): if is_new and not self.airflow: self.airflow = self.device_type.airflow + # Inherit default_platform from DeviceType if not set + if is_new and not self.platform: + self.platform = self.device_type.default_platform + super().save(*args, **kwargs) # If this is a new Device, instantiate all the related components per the DeviceType definition diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index dff697588..91a37fab3 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/netbox/dcim/tables/devicetypes.py @@ -77,6 +77,9 @@ class DeviceTypeTable(NetBoxTable): manufacturer = tables.Column( linkify=True ) + default_platform = tables.Column( + linkify=True + ) is_full_depth = columns.BooleanColumn( verbose_name='Full Depth' ) @@ -100,7 +103,7 @@ class DeviceTypeTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = models.DeviceType fields = ( - 'pk', 'id', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', + 'pk', 'id', 'model', 'manufacturer', 'default_platform', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'description', 'comments', 'instance_count', 'tags', 'created', 'last_updated', ) default_columns = ( diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 45d5797bd..c78b592d3 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -699,9 +699,16 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests): ) Manufacturer.objects.bulk_create(manufacturers) + platforms = ( + Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0]), + Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1]), + Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2]), + ) + Platform.objects.bulk_create(platforms) + device_types = ( - DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', part_number='Part Number 1', u_height=1, is_full_depth=True, front_image='front.png', rear_image='rear.png', weight=10, weight_unit=WeightUnitChoices.UNIT_POUND), - DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', part_number='Part Number 2', u_height=2, is_full_depth=True, subdevice_role=SubdeviceRoleChoices.ROLE_PARENT, airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR, weight=20, weight_unit=WeightUnitChoices.UNIT_POUND), + DeviceType(manufacturer=manufacturers[0], default_platform=platforms[0], model='Model 1', slug='model-1', part_number='Part Number 1', u_height=1, is_full_depth=True, front_image='front.png', rear_image='rear.png', weight=10, weight_unit=WeightUnitChoices.UNIT_POUND), + DeviceType(manufacturer=manufacturers[1], default_platform=platforms[1], model='Model 2', slug='model-2', part_number='Part Number 2', u_height=2, is_full_depth=True, subdevice_role=SubdeviceRoleChoices.ROLE_PARENT, airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR, weight=20, weight_unit=WeightUnitChoices.UNIT_POUND), DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', part_number='Part Number 3', u_height=3, is_full_depth=False, subdevice_role=SubdeviceRoleChoices.ROLE_CHILD, airflow=DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT, weight=30, weight_unit=WeightUnitChoices.UNIT_KILOGRAM), ) DeviceType.objects.bulk_create(device_types) @@ -785,6 +792,13 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_default_platform(self): + platforms = Platform.objects.all()[:2] + params = {'default_platform_id': [platforms[0].pk, platforms[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'default_platform': [platforms[0].slug, platforms[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_has_front_image(self): params = {'has_front_image': True} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 6ea935bc8..bba91412d 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -503,6 +503,12 @@ class DeviceTypeTestCase( ) Manufacturer.objects.bulk_create(manufacturers) + platforms = ( + Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0]), + Platform(name='Platform 2', slug='platform-3', manufacturer=manufacturers[1]), + ) + Platform.objects.bulk_create(platforms) + DeviceType.objects.bulk_create([ DeviceType(model='Device Type 1', slug='device-type-1', manufacturer=manufacturers[0]), DeviceType(model='Device Type 2', slug='device-type-2', manufacturer=manufacturers[0]), @@ -513,6 +519,7 @@ class DeviceTypeTestCase( cls.form_data = { 'manufacturer': manufacturers[1].pk, + 'default_platform': platforms[0].pk, 'model': 'Device Type X', 'slug': 'device-type-x', 'part_number': '123ABC', @@ -525,6 +532,7 @@ class DeviceTypeTestCase( cls.bulk_edit_data = { 'manufacturer': manufacturers[1].pk, + 'default_platform': platforms[1].pk, 'u_height': 3, 'is_full_depth': False, } @@ -673,6 +681,7 @@ class DeviceTypeTestCase( """ IMPORT_DATA = """ manufacturer: Generic +default_platform: Platform model: TEST-1000 slug: test-1000 u_height: 2 @@ -755,8 +764,11 @@ inventory-items: manufacturer: Generic """ - # Create the manufacturer - Manufacturer(name='Generic', slug='generic').save() + # Create the manufacturer and platform + manufacturer = Manufacturer(name='Generic', slug='generic') + manufacturer.save() + platform = Platform(name='Platform', slug='test-platform', manufacturer=manufacturer) + platform.save() # Add all required permissions to the test user self.add_permissions( @@ -783,6 +795,7 @@ inventory-items: device_type = DeviceType.objects.get(model='TEST-1000') self.assertEqual(device_type.comments, 'Test comment') + self.assertEqual(device_type.default_platform.pk, platform.pk) # Verify all of the components were created self.assertEqual(device_type.consoleporttemplates.count(), 3) diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index 73c82ddae..984898caa 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -27,6 +27,10 @@ Part Number {{ object.part_number|placeholder }} + + Default Platform + {{ object.default_platform|linkify }} + Description {{ object.description|placeholder }} From c73829fe920eab0e0e347b3deba5a430db1a8b88 Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Wed, 15 Feb 2023 12:55:25 +0100 Subject: [PATCH 033/174] Fix issues with the ContactAssignmentListView --- netbox/tenancy/filtersets.py | 12 ++++++++++++ netbox/tenancy/urls.py | 1 + netbox/tenancy/views.py | 7 +++++++ 3 files changed, 20 insertions(+) diff --git a/netbox/tenancy/filtersets.py b/netbox/tenancy/filtersets.py index ab74949ff..1edc8fdc8 100644 --- a/netbox/tenancy/filtersets.py +++ b/netbox/tenancy/filtersets.py @@ -81,6 +81,10 @@ class ContactFilterSet(NetBoxModelFilterSet): class ContactAssignmentFilterSet(ChangeLoggedModelFilterSet): + q = django_filters.CharFilter( + method='search', + label=_('Search'), + ) content_type = ContentTypeFilter() contact_id = django_filters.ModelMultipleChoiceFilter( queryset=Contact.objects.all(), @@ -101,6 +105,14 @@ class ContactAssignmentFilterSet(ChangeLoggedModelFilterSet): model = ContactAssignment fields = ['id', 'content_type_id', 'object_id', 'priority'] + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(contact__name__icontains=value) | + Q(role__name__icontains=value) + ) + class ContactModelFilterSet(django_filters.FilterSet): contact = django_filters.ModelMultipleChoiceFilter( diff --git a/netbox/tenancy/urls.py b/netbox/tenancy/urls.py index cb8715f70..6563eff4b 100644 --- a/netbox/tenancy/urls.py +++ b/netbox/tenancy/urls.py @@ -49,6 +49,7 @@ urlpatterns = [ # Contact assignments path('contact-assignments/', views.ContactAssignmentListView.as_view(), name='contactassignment_list'), path('contact-assignments/add/', views.ContactAssignmentEditView.as_view(), name='contactassignment_add'), + path('contact-assignments/delete/', views.ContactAssignmentBulkDeleteView.as_view(), name='contactassignment_bulk_delete'), path('contact-assignments//', include(get_model_urls('tenancy', 'contactassignment'))), ] diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index b7585b8d7..b71702d65 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -354,6 +354,7 @@ class ContactAssignmentListView(generic.ObjectListView): filterset = filtersets.ContactAssignmentFilterSet filterset_form = forms.ContactAssignmentFilterForm table = tables.ContactAssignmentTable + actions = ('export', 'bulk_delete') @register_model_view(ContactAssignment, 'edit') @@ -376,6 +377,12 @@ class ContactAssignmentEditView(generic.ObjectEditView): } +class ContactAssignmentBulkDeleteView(generic.BulkDeleteView): + queryset = ContactAssignment.objects.all() + filterset = filtersets.ContactAssignmentFilterSet + table = tables.ContactAssignmentTable + + @register_model_view(ContactAssignment, 'delete') class ContactAssignmentDeleteView(generic.ObjectDeleteView): queryset = ContactAssignment.objects.all() From b9bd96f0c7b3d1e4e03d9246fcb6d1868df3d1ac Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 16 Feb 2023 10:25:51 -0500 Subject: [PATCH 034/174] Closes #11765: Remove StaticSelect & StaticSelectMultiple (#11767) * Remove StaticSelect, StaticSelectMultiple form widgets * Tag custom ChoiceField, MultipleChoiceField classes for removal in v3.6 --- docs/plugins/development/forms.md | 3 + netbox/circuits/forms/bulk_edit.py | 5 +- netbox/circuits/forms/filtersets.py | 4 +- netbox/circuits/forms/model_forms.py | 3 - netbox/core/forms/bulk_edit.py | 7 +- netbox/core/forms/filtersets.py | 10 +- netbox/core/forms/model_forms.py | 4 +- netbox/dcim/forms/bulk_edit.py | 100 +++++-------- netbox/dcim/forms/filtersets.py | 135 +++++++++--------- netbox/dcim/forms/model_forms.py | 105 +------------- netbox/extras/forms/bulk_edit.py | 8 +- netbox/extras/forms/filtersets.py | 41 +++--- netbox/extras/forms/model_forms.py | 12 +- netbox/extras/models/customfields.py | 15 +- netbox/ipam/forms/bulk_edit.py | 29 ++-- netbox/ipam/forms/filtersets.py | 47 +++--- netbox/ipam/forms/model_forms.py | 38 +---- netbox/netbox/forms/__init__.py | 8 +- netbox/tenancy/forms/filtersets.py | 5 +- netbox/tenancy/forms/model_forms.py | 3 +- netbox/users/forms.py | 4 +- .../utilities/forms/fields/content_types.py | 4 +- netbox/utilities/forms/fields/fields.py | 12 +- netbox/utilities/forms/forms.py | 17 ++- netbox/utilities/forms/widgets.py | 23 +-- netbox/virtualization/forms/bulk_edit.py | 9 +- netbox/virtualization/forms/filtersets.py | 10 +- netbox/virtualization/forms/model_forms.py | 13 +- netbox/wireless/forms/filtersets.py | 20 +-- netbox/wireless/forms/model_forms.py | 12 +- 30 files changed, 221 insertions(+), 485 deletions(-) diff --git a/docs/plugins/development/forms.md b/docs/plugins/development/forms.md index d819b76cb..db7adff23 100644 --- a/docs/plugins/development/forms.md +++ b/docs/plugins/development/forms.md @@ -170,6 +170,9 @@ In addition to the [form fields provided by Django](https://docs.djangoproject.c ## Choice Fields +!!! warning "Obsolete Fields" + NetBox's custom `ChoiceField` and `MultipleChoiceField` classes are no longer necessary thanks to improvements made to the user interface. Django's native form fields can be used instead. These custom field classes will be removed in NetBox v3.6. + ::: utilities.forms.ChoiceField options: members: false diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index dd6e103e4..a3e91c8ae 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -7,7 +7,7 @@ from ipam.models import ASN from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import ( - add_blank_choice, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect, + add_blank_choice, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ) __all__ = ( @@ -100,8 +100,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm): status = forms.ChoiceField( choices=add_blank_choice(CircuitStatusChoices), required=False, - initial='', - widget=StaticSelect() + initial='' ) tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index d7cfc494d..05dacfd38 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -7,7 +7,7 @@ from dcim.models import Region, Site, SiteGroup from ipam.models import ASN from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm, ContactModelFilterForm -from utilities.forms import DatePicker, DynamicModelMultipleChoiceField, MultipleChoiceField, TagFilterField +from utilities.forms import DatePicker, DynamicModelMultipleChoiceField, TagFilterField __all__ = ( 'CircuitFilterForm', @@ -107,7 +107,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi }, label=_('Provider network') ) - status = MultipleChoiceField( + status = forms.MultipleChoiceField( choices=CircuitStatusChoices, required=False ) diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index cd73780fa..be0d39835 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -7,7 +7,6 @@ from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms import ( CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SelectSpeedWidget, SlugField, - StaticSelect, ) __all__ = ( @@ -102,7 +101,6 @@ class CircuitForm(TenancyForm, NetBoxModelForm): 'commit_rate': _("Committed rate"), } widgets = { - 'status': StaticSelect(), 'install_date': DatePicker(), 'termination_date': DatePicker(), 'commit_rate': SelectSpeedWidget(), @@ -174,7 +172,6 @@ class CircuitTerminationForm(NetBoxModelForm): 'pp_info': _("Patch panel ID and port number(s)") } widgets = { - 'term_side': StaticSelect(), 'port_speed': SelectSpeedWidget(), 'upstream_speed': SelectSpeedWidget(), } diff --git a/netbox/core/forms/bulk_edit.py b/netbox/core/forms/bulk_edit.py index 6fb562db6..f613785c5 100644 --- a/netbox/core/forms/bulk_edit.py +++ b/netbox/core/forms/bulk_edit.py @@ -4,9 +4,7 @@ from django.utils.translation import gettext as _ from core.choices import DataSourceTypeChoices from core.models import * from netbox.forms import NetBoxModelBulkEditForm -from utilities.forms import ( - add_blank_choice, BulkEditNullBooleanSelect, CommentField, StaticSelect, -) +from utilities.forms import add_blank_choice, BulkEditNullBooleanSelect, CommentField __all__ = ( 'DataSourceBulkEditForm', @@ -17,8 +15,7 @@ class DataSourceBulkEditForm(NetBoxModelBulkEditForm): type = forms.ChoiceField( choices=add_blank_choice(DataSourceTypeChoices), required=False, - initial='', - widget=StaticSelect() + initial='' ) enabled = forms.NullBooleanField( required=False, diff --git a/netbox/core/forms/filtersets.py b/netbox/core/forms/filtersets.py index 433f07067..a54941537 100644 --- a/netbox/core/forms/filtersets.py +++ b/netbox/core/forms/filtersets.py @@ -4,9 +4,7 @@ from django.utils.translation import gettext as _ from core.choices import * from core.models import * from netbox.forms import NetBoxModelFilterSetForm -from utilities.forms import ( - BOOLEAN_WITH_BLANK_CHOICES, DynamicModelMultipleChoiceField, MultipleChoiceField, StaticSelect, -) +from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, DynamicModelMultipleChoiceField __all__ = ( 'DataFileFilterForm', @@ -20,17 +18,17 @@ class DataSourceFilterForm(NetBoxModelFilterSetForm): (None, ('q', 'filter_id')), ('Data Source', ('type', 'status')), ) - type = MultipleChoiceField( + type = forms.MultipleChoiceField( choices=DataSourceTypeChoices, required=False ) - status = MultipleChoiceField( + status = forms.MultipleChoiceField( choices=DataSourceStatusChoices, required=False ) enabled = forms.NullBooleanField( required=False, - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py index 786e71c3a..e9cc962cd 100644 --- a/netbox/core/forms/model_forms.py +++ b/netbox/core/forms/model_forms.py @@ -3,7 +3,7 @@ import copy from django import forms from core.models import * -from netbox.forms import NetBoxModelForm, StaticSelect +from netbox.forms import NetBoxModelForm from netbox.registry import registry from utilities.forms import CommentField @@ -21,7 +21,7 @@ class DataSourceForm(NetBoxModelForm): 'name', 'type', 'source_url', 'enabled', 'description', 'comments', 'ignore_rules', 'tags', ] widgets = { - 'type': StaticSelect( + 'type': forms.Select( attrs={ 'hx-get': '.', 'hx-include': '#form_fields input', diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index e5b896f9f..c00359d4c 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -11,7 +11,7 @@ from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField, - DynamicModelMultipleChoiceField, form_from_model, StaticSelect, SelectSpeedWidget, + DynamicModelMultipleChoiceField, form_from_model, SelectSpeedWidget, ) __all__ = ( @@ -96,8 +96,7 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm): status = forms.ChoiceField( choices=add_blank_choice(SiteStatusChoices), required=False, - initial='', - widget=StaticSelect() + initial='' ) region = DynamicModelChoiceField( queryset=Region.objects.all(), @@ -130,8 +129,7 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm): ) time_zone = TimeZoneFormField( choices=add_blank_choice(TimeZoneFormField().choices), - required=False, - widget=StaticSelect() + required=False ) description = forms.CharField( max_length=200, @@ -166,8 +164,7 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm): status = forms.ChoiceField( choices=add_blank_choice(LocationStatusChoices), required=False, - initial='', - widget=StaticSelect() + initial='' ) tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), @@ -238,8 +235,7 @@ class RackBulkEditForm(NetBoxModelBulkEditForm): status = forms.ChoiceField( choices=add_blank_choice(RackStatusChoices), required=False, - initial='', - widget=StaticSelect() + initial='' ) role = DynamicModelChoiceField( queryset=RackRole.objects.all(), @@ -256,13 +252,11 @@ class RackBulkEditForm(NetBoxModelBulkEditForm): ) type = forms.ChoiceField( choices=add_blank_choice(RackTypeChoices), - required=False, - widget=StaticSelect() + required=False ) width = forms.ChoiceField( choices=add_blank_choice(RackWidthChoices), - required=False, - widget=StaticSelect() + required=False ) u_height = forms.IntegerField( required=False, @@ -283,8 +277,7 @@ class RackBulkEditForm(NetBoxModelBulkEditForm): ) outer_unit = forms.ChoiceField( choices=add_blank_choice(RackDimensionUnitChoices), - required=False, - widget=StaticSelect() + required=False ) mounting_depth = forms.IntegerField( required=False, @@ -301,8 +294,7 @@ class RackBulkEditForm(NetBoxModelBulkEditForm): weight_unit = forms.ChoiceField( choices=add_blank_choice(WeightUnitChoices), required=False, - initial='', - widget=StaticSelect() + initial='' ) description = forms.CharField( max_length=200, @@ -333,8 +325,7 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm): queryset=User.objects.order_by( 'username' ), - required=False, - widget=StaticSelect() + required=False ) tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), @@ -392,8 +383,7 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm): ) airflow = forms.ChoiceField( choices=add_blank_choice(DeviceAirflowChoices), - required=False, - widget=StaticSelect() + required=False ) weight = forms.DecimalField( min_value=0, @@ -402,8 +392,7 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm): weight_unit = forms.ChoiceField( choices=add_blank_choice(WeightUnitChoices), required=False, - initial='', - widget=StaticSelect() + initial='' ) description = forms.CharField( max_length=200, @@ -437,8 +426,7 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm): weight_unit = forms.ChoiceField( choices=add_blank_choice(WeightUnitChoices), required=False, - initial='', - widget=StaticSelect() + initial='' ) description = forms.CharField( max_length=200, @@ -537,13 +525,11 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm): ) status = forms.ChoiceField( choices=add_blank_choice(DeviceStatusChoices), - required=False, - widget=StaticSelect() + required=False ) airflow = forms.ChoiceField( choices=add_blank_choice(DeviceAirflowChoices), - required=False, - widget=StaticSelect() + required=False ) serial = forms.CharField( max_length=50, @@ -585,8 +571,7 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm): status = forms.ChoiceField( choices=add_blank_choice(ModuleStatusChoices), required=False, - initial='', - widget=StaticSelect() + initial='' ) serial = forms.CharField( max_length=50, @@ -613,13 +598,11 @@ class CableBulkEditForm(NetBoxModelBulkEditForm): type = forms.ChoiceField( choices=add_blank_choice(CableTypeChoices), required=False, - initial='', - widget=StaticSelect() + initial='' ) status = forms.ChoiceField( choices=add_blank_choice(LinkStatusChoices), required=False, - widget=StaticSelect(), initial='' ) tenant = DynamicModelChoiceField( @@ -640,8 +623,7 @@ class CableBulkEditForm(NetBoxModelBulkEditForm): length_unit = forms.ChoiceField( choices=add_blank_choice(CableLengthUnitChoices), required=False, - initial='', - widget=StaticSelect() + initial='' ) description = forms.CharField( max_length=200, @@ -741,26 +723,22 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm): status = forms.ChoiceField( choices=add_blank_choice(PowerFeedStatusChoices), required=False, - initial='', - widget=StaticSelect() + initial='' ) type = forms.ChoiceField( choices=add_blank_choice(PowerFeedTypeChoices), required=False, - initial='', - widget=StaticSelect() + initial='' ) supply = forms.ChoiceField( choices=add_blank_choice(PowerFeedSupplyChoices), required=False, - initial='', - widget=StaticSelect() + initial='' ) phase = forms.ChoiceField( choices=add_blank_choice(PowerFeedPhaseChoices), required=False, - initial='', - widget=StaticSelect() + initial='' ) voltage = forms.IntegerField( required=False @@ -807,8 +785,7 @@ class ConsolePortTemplateBulkEditForm(BulkEditForm): ) type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), - required=False, - widget=StaticSelect() + required=False ) nullable_fields = ('label', 'type', 'description') @@ -825,8 +802,7 @@ class ConsoleServerPortTemplateBulkEditForm(BulkEditForm): ) type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), - required=False, - widget=StaticSelect() + required=False ) description = forms.CharField( required=False @@ -846,8 +822,7 @@ class PowerPortTemplateBulkEditForm(BulkEditForm): ) type = forms.ChoiceField( choices=add_blank_choice(PowerPortTypeChoices), - required=False, - widget=StaticSelect() + required=False ) maximum_draw = forms.IntegerField( min_value=1, @@ -883,8 +858,7 @@ class PowerOutletTemplateBulkEditForm(BulkEditForm): ) type = forms.ChoiceField( choices=add_blank_choice(PowerOutletTypeChoices), - required=False, - widget=StaticSelect() + required=False ) power_port = forms.ModelChoiceField( queryset=PowerPortTemplate.objects.all(), @@ -892,8 +866,7 @@ class PowerOutletTemplateBulkEditForm(BulkEditForm): ) feed_leg = forms.ChoiceField( choices=add_blank_choice(PowerOutletFeedLegChoices), - required=False, - widget=StaticSelect() + required=False ) description = forms.CharField( required=False @@ -924,8 +897,7 @@ class InterfaceTemplateBulkEditForm(BulkEditForm): ) type = forms.ChoiceField( choices=add_blank_choice(InterfaceTypeChoices), - required=False, - widget=StaticSelect() + required=False ) enabled = forms.NullBooleanField( required=False, @@ -943,14 +915,12 @@ class InterfaceTemplateBulkEditForm(BulkEditForm): choices=add_blank_choice(InterfacePoEModeChoices), required=False, initial='', - widget=StaticSelect(), label=_('PoE mode') ) poe_type = forms.ChoiceField( choices=add_blank_choice(InterfacePoETypeChoices), required=False, initial='', - widget=StaticSelect(), label=_('PoE type') ) @@ -968,8 +938,7 @@ class FrontPortTemplateBulkEditForm(BulkEditForm): ) type = forms.ChoiceField( choices=add_blank_choice(PortTypeChoices), - required=False, - widget=StaticSelect() + required=False ) color = ColorField( required=False @@ -992,8 +961,7 @@ class RearPortTemplateBulkEditForm(BulkEditForm): ) type = forms.ChoiceField( choices=add_blank_choice(PortTypeChoices), - required=False, - widget=StaticSelect() + required=False ) color = ColorField( required=False @@ -1208,14 +1176,12 @@ class InterfaceBulkEditForm( choices=add_blank_choice(InterfacePoEModeChoices), required=False, initial='', - widget=StaticSelect(), label=_('PoE mode') ) poe_type = forms.ChoiceField( choices=add_blank_choice(InterfacePoETypeChoices), required=False, initial='', - widget=StaticSelect(), label=_('PoE type') ) mark_connected = forms.NullBooleanField( @@ -1225,8 +1191,7 @@ class InterfaceBulkEditForm( mode = forms.ChoiceField( choices=add_blank_choice(InterfaceModeChoices), required=False, - initial='', - widget=StaticSelect() + initial='' ) vlan_group = DynamicModelChoiceField( queryset=VLANGroup.objects.all(), @@ -1426,8 +1391,7 @@ class VirtualDeviceContextBulkEditForm(NetBoxModelBulkEditForm): ) status = forms.ChoiceField( required=False, - choices=add_blank_choice(VirtualDeviceContextStatusChoices), - widget=StaticSelect() + choices=add_blank_choice(VirtualDeviceContextStatusChoices) ) tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 7e2b4d2d8..b5a6cd53b 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -10,8 +10,8 @@ from ipam.models import ASN, L2VPN, VRF from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from utilities.forms import ( - APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, MultipleChoiceField, - StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, SelectSpeedWidget, + APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, + TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, SelectSpeedWidget, ) from wireless.choices import * @@ -150,7 +150,7 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte ('Tenant', ('tenant_group_id', 'tenant_id')), ('Contacts', ('contact', 'contact_role', 'contact_group')), ) - status = MultipleChoiceField( + status = forms.MultipleChoiceField( choices=SiteStatusChoices, required=False ) @@ -208,7 +208,7 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF }, label=_('Parent') ) - status = MultipleChoiceField( + status = forms.MultipleChoiceField( choices=LocationStatusChoices, required=False ) @@ -258,15 +258,15 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte }, label=_('Location') ) - status = MultipleChoiceField( + status = forms.MultipleChoiceField( choices=RackStatusChoices, required=False ) - type = MultipleChoiceField( + type = forms.MultipleChoiceField( choices=RackTypeChoices, required=False ) - width = MultipleChoiceField( + width = forms.MultipleChoiceField( choices=RackWidthChoices, required=False ) @@ -399,88 +399,88 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm): part_number = forms.CharField( required=False ) - subdevice_role = MultipleChoiceField( + subdevice_role = forms.MultipleChoiceField( choices=add_blank_choice(SubdeviceRoleChoices), required=False ) - airflow = MultipleChoiceField( + airflow = forms.MultipleChoiceField( choices=add_blank_choice(DeviceAirflowChoices), required=False ) has_front_image = forms.NullBooleanField( required=False, label='Has a front image', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) has_rear_image = forms.NullBooleanField( required=False, label='Has a rear image', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) console_ports = forms.NullBooleanField( required=False, label='Has console ports', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) console_server_ports = forms.NullBooleanField( required=False, label='Has console server ports', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) power_ports = forms.NullBooleanField( required=False, label='Has power ports', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) power_outlets = forms.NullBooleanField( required=False, label='Has power outlets', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) interfaces = forms.NullBooleanField( required=False, label='Has interfaces', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) pass_through_ports = forms.NullBooleanField( required=False, label='Has pass-through ports', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) device_bays = forms.NullBooleanField( required=False, label='Has device bays', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) module_bays = forms.NullBooleanField( required=False, label='Has module bays', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) inventory_items = forms.NullBooleanField( required=False, label='Has inventory items', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) @@ -517,42 +517,42 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm): console_ports = forms.NullBooleanField( required=False, label='Has console ports', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) console_server_ports = forms.NullBooleanField( required=False, label='Has console server ports', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) power_ports = forms.NullBooleanField( required=False, label='Has power ports', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) power_outlets = forms.NullBooleanField( required=False, label='Has power outlets', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) interfaces = forms.NullBooleanField( required=False, label='Has interfaces', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) pass_through_ports = forms.NullBooleanField( required=False, label='Has pass-through ports', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) @@ -662,11 +662,11 @@ class DeviceFilterForm( null_option='None', label=_('Platform') ) - status = MultipleChoiceField( + status = forms.MultipleChoiceField( choices=DeviceStatusChoices, required=False ) - airflow = MultipleChoiceField( + airflow = forms.MultipleChoiceField( choices=add_blank_choice(DeviceAirflowChoices), required=False ) @@ -683,56 +683,56 @@ class DeviceFilterForm( has_primary_ip = forms.NullBooleanField( required=False, label='Has a primary IP', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) virtual_chassis_member = forms.NullBooleanField( required=False, label='Virtual chassis member', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) console_ports = forms.NullBooleanField( required=False, label='Has console ports', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) console_server_ports = forms.NullBooleanField( required=False, label='Has console server ports', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) power_ports = forms.NullBooleanField( required=False, label='Has power ports', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) power_outlets = forms.NullBooleanField( required=False, label='Has power outlets', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) interfaces = forms.NullBooleanField( required=False, label='Has interfaces', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) pass_through_ports = forms.NullBooleanField( required=False, label='Has pass-through ports', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) @@ -755,14 +755,14 @@ class VirtualDeviceContextFilterForm( label=_('Device'), fetch_trigger='open' ) - status = MultipleChoiceField( + status = forms.MultipleChoiceField( required=False, choices=add_blank_choice(VirtualDeviceContextStatusChoices) ) has_primary_ip = forms.NullBooleanField( required=False, label='Has a primary IP', - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) @@ -790,7 +790,7 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo label=_('Type'), fetch_trigger='open' ) - status = MultipleChoiceField( + status = forms.MultipleChoiceField( choices=ModuleStatusChoices, required=False ) @@ -883,11 +883,11 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): }, label=_('Device') ) - type = MultipleChoiceField( + type = forms.MultipleChoiceField( choices=add_blank_choice(CableTypeChoices), required=False ) - status = MultipleChoiceField( + status = forms.MultipleChoiceField( required=False, choices=add_blank_choice(LinkStatusChoices) ) @@ -985,24 +985,21 @@ class PowerFeedFilterForm(NetBoxModelFilterSetForm): }, label=_('Rack') ) - status = MultipleChoiceField( + status = forms.MultipleChoiceField( choices=PowerFeedStatusChoices, required=False ) type = forms.ChoiceField( choices=add_blank_choice(PowerFeedTypeChoices), - required=False, - widget=StaticSelect() + required=False ) supply = forms.ChoiceField( choices=add_blank_choice(PowerFeedSupplyChoices), - required=False, - widget=StaticSelect() + required=False ) phase = forms.ChoiceField( choices=add_blank_choice(PowerFeedPhaseChoices), - required=False, - widget=StaticSelect() + required=False ) voltage = forms.IntegerField( required=False @@ -1023,13 +1020,13 @@ class PowerFeedFilterForm(NetBoxModelFilterSetForm): class CabledFilterForm(forms.Form): cabled = forms.NullBooleanField( required=False, - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) occupied = forms.NullBooleanField( required=False, - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) @@ -1038,7 +1035,7 @@ class CabledFilterForm(forms.Form): class PathEndpointFilterForm(CabledFilterForm): connected = forms.NullBooleanField( required=False, - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) @@ -1052,11 +1049,11 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')), ('Connection', ('cabled', 'connected', 'occupied')), ) - type = MultipleChoiceField( + type = forms.MultipleChoiceField( choices=ConsolePortTypeChoices, required=False ) - speed = MultipleChoiceField( + speed = forms.MultipleChoiceField( choices=ConsolePortSpeedChoices, required=False ) @@ -1071,11 +1068,11 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')), ('Connection', ('cabled', 'connected', 'occupied')), ) - type = MultipleChoiceField( + type = forms.MultipleChoiceField( choices=ConsolePortTypeChoices, required=False ) - speed = MultipleChoiceField( + speed = forms.MultipleChoiceField( choices=ConsolePortSpeedChoices, required=False ) @@ -1090,7 +1087,7 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')), ('Connection', ('cabled', 'connected', 'occupied')), ) - type = MultipleChoiceField( + type = forms.MultipleChoiceField( choices=PowerPortTypeChoices, required=False ) @@ -1105,7 +1102,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')), ('Connection', ('cabled', 'connected', 'occupied')), ) - type = MultipleChoiceField( + type = forms.MultipleChoiceField( choices=PowerOutletTypeChoices, required=False ) @@ -1132,11 +1129,11 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): }, label=_('Virtual Device Context') ) - kind = MultipleChoiceField( + kind = forms.MultipleChoiceField( choices=InterfaceKindChoices, required=False ) - type = MultipleChoiceField( + type = forms.MultipleChoiceField( choices=InterfaceTypeChoices, required=False ) @@ -1145,19 +1142,19 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): label='Speed', widget=SelectSpeedWidget() ) - duplex = MultipleChoiceField( + duplex = forms.MultipleChoiceField( choices=InterfaceDuplexChoices, required=False ) enabled = forms.NullBooleanField( required=False, - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) mgmt_only = forms.NullBooleanField( required=False, - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) @@ -1169,22 +1166,22 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): required=False, label='WWN' ) - poe_mode = MultipleChoiceField( + poe_mode = forms.MultipleChoiceField( choices=InterfacePoEModeChoices, required=False, label='PoE mode' ) - poe_type = MultipleChoiceField( + poe_type = forms.MultipleChoiceField( choices=InterfacePoETypeChoices, required=False, label='PoE type' ) - rf_role = MultipleChoiceField( + rf_role = forms.MultipleChoiceField( choices=WirelessRoleChoices, required=False, label='Wireless role' ) - rf_channel = MultipleChoiceField( + rf_channel = forms.MultipleChoiceField( choices=WirelessChannelChoices, required=False, label='Wireless channel' @@ -1224,7 +1221,7 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): ('Cable', ('cabled', 'occupied')), ) model = FrontPort - type = MultipleChoiceField( + type = forms.MultipleChoiceField( choices=PortTypeChoices, required=False ) @@ -1242,7 +1239,7 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')), ('Cable', ('cabled', 'occupied')), ) - type = MultipleChoiceField( + type = forms.MultipleChoiceField( choices=PortTypeChoices, required=False ) @@ -1301,7 +1298,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm): ) discovered = forms.NullBooleanField( required=False, - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 14217d2d6..8bac5d342 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -13,7 +13,7 @@ from tenancy.forms import TenancyForm from utilities.forms import ( APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK, - SlugField, StaticSelect, SelectSpeedWidget, + SlugField, SelectSpeedWidget, ) from virtualization.models import Cluster, ClusterGroup from wireless.models import WirelessLAN, WirelessLANGroup @@ -129,8 +129,7 @@ class SiteForm(TenancyForm, NetBoxModelForm): slug = SlugField() time_zone = TimeZoneFormField( choices=add_blank_choice(TimeZoneFormField().choices), - required=False, - widget=StaticSelect() + required=False ) comments = CommentField() @@ -159,8 +158,6 @@ class SiteForm(TenancyForm, NetBoxModelForm): 'rows': 3, } ), - 'status': StaticSelect(), - 'time_zone': StaticSelect(), } help_texts = { 'name': _("Full name of the site"), @@ -218,9 +215,6 @@ class LocationForm(TenancyForm, NetBoxModelForm): 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'tags', ) - widgets = { - 'status': StaticSelect(), - } class RackRoleForm(NetBoxModelForm): @@ -287,13 +281,6 @@ class RackForm(TenancyForm, NetBoxModelForm): 'facility_id': _("The unique rack ID assigned by the facility"), 'u_height': _("Height in rack units"), } - widgets = { - 'status': StaticSelect(), - 'type': StaticSelect(), - 'width': StaticSelect(), - 'outer_unit': StaticSelect(), - 'weight_unit': StaticSelect(), - } class RackReservationForm(TenancyForm, NetBoxModelForm): @@ -340,8 +327,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm): user = forms.ModelChoiceField( queryset=User.objects.order_by( 'username' - ), - widget=StaticSelect() + ) ) comments = CommentField() @@ -402,15 +388,12 @@ class DeviceTypeForm(NetBoxModelForm): 'weight', 'weight_unit', 'front_image', 'rear_image', 'description', 'comments', 'tags', 'default_platform' ] widgets = { - 'airflow': StaticSelect(), - 'subdevice_role': StaticSelect(), 'front_image': ClearableFileInput(attrs={ 'accept': DEVICETYPE_IMAGE_FORMATS }), 'rear_image': ClearableFileInput(attrs={ 'accept': DEVICETYPE_IMAGE_FORMATS }), - 'weight_unit': StaticSelect(), } @@ -431,10 +414,6 @@ class ModuleTypeForm(NetBoxModelForm): 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description', 'comments', 'tags', ] - widgets = { - 'weight_unit': StaticSelect(), - } - class DeviceRoleForm(NetBoxModelForm): slug = SlugField() @@ -601,13 +580,6 @@ class DeviceForm(TenancyForm, NetBoxModelForm): 'local_context_data': _("Local config context data overwrites all source contexts in the final rendered " "config context"), } - widgets = { - 'face': StaticSelect(), - 'status': StaticSelect(), - 'airflow': StaticSelect(), - 'primary_ip4': StaticSelect(), - 'primary_ip6': StaticSelect(), - } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -741,11 +713,6 @@ class CableForm(TenancyForm, NetBoxModelForm): 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags', ] - widgets = { - 'status': StaticSelect, - 'type': StaticSelect, - 'length_unit': StaticSelect, - } error_messages = { 'length': { 'max_value': 'Maximum length is 32767 (any unit)' @@ -860,12 +827,6 @@ class PowerFeedForm(NetBoxModelForm): 'mark_connected', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'description', 'comments', 'tags', ] - widgets = { - 'status': StaticSelect(), - 'type': StaticSelect(), - 'supply': StaticSelect(), - 'phase': StaticSelect(), - } # @@ -1029,9 +990,6 @@ class ConsolePortTemplateForm(ModularComponentTemplateForm): fields = [ 'device_type', 'module_type', 'name', 'label', 'type', 'description', ] - widgets = { - 'type': StaticSelect, - } class ConsoleServerPortTemplateForm(ModularComponentTemplateForm): @@ -1044,9 +1002,6 @@ class ConsoleServerPortTemplateForm(ModularComponentTemplateForm): fields = [ 'device_type', 'module_type', 'name', 'label', 'type', 'description', ] - widgets = { - 'type': StaticSelect, - } class PowerPortTemplateForm(ModularComponentTemplateForm): @@ -1061,9 +1016,6 @@ class PowerPortTemplateForm(ModularComponentTemplateForm): fields = [ 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', ] - widgets = { - 'type': StaticSelect(), - } class PowerOutletTemplateForm(ModularComponentTemplateForm): @@ -1084,10 +1036,6 @@ class PowerOutletTemplateForm(ModularComponentTemplateForm): fields = [ 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', ] - widgets = { - 'type': StaticSelect(), - 'feed_leg': StaticSelect(), - } class InterfaceTemplateForm(ModularComponentTemplateForm): @@ -1101,11 +1049,6 @@ class InterfaceTemplateForm(ModularComponentTemplateForm): fields = [ 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'enabled', 'description', 'poe_mode', 'poe_type', ] - widgets = { - 'type': StaticSelect(), - 'poe_mode': StaticSelect(), - 'poe_type': StaticSelect(), - } class FrontPortTemplateForm(ModularComponentTemplateForm): @@ -1131,9 +1074,6 @@ class FrontPortTemplateForm(ModularComponentTemplateForm): 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', ] - widgets = { - 'type': StaticSelect(), - } class RearPortTemplateForm(ModularComponentTemplateForm): @@ -1146,9 +1086,6 @@ class RearPortTemplateForm(ModularComponentTemplateForm): fields = [ 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description', ] - widgets = { - 'type': StaticSelect(), - } class ModuleBayTemplateForm(ComponentTemplateForm): @@ -1256,10 +1193,6 @@ class ConsolePortForm(ModularDeviceComponentForm): fields = [ 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', ] - widgets = { - 'type': StaticSelect(), - 'speed': StaticSelect(), - } class ConsoleServerPortForm(ModularDeviceComponentForm): @@ -1275,10 +1208,6 @@ class ConsoleServerPortForm(ModularDeviceComponentForm): fields = [ 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', ] - widgets = { - 'type': StaticSelect(), - 'speed': StaticSelect(), - } class PowerPortForm(ModularDeviceComponentForm): @@ -1296,9 +1225,6 @@ class PowerPortForm(ModularDeviceComponentForm): 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description', 'tags', ] - widgets = { - 'type': StaticSelect(), - } class PowerOutletForm(ModularDeviceComponentForm): @@ -1323,10 +1249,6 @@ class PowerOutletForm(ModularDeviceComponentForm): 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description', 'tags', ] - widgets = { - 'type': StaticSelect(), - 'feed_leg': StaticSelect(), - } class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm): @@ -1431,14 +1353,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm): 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags', ] widgets = { - 'type': StaticSelect(), 'speed': SelectSpeedWidget(), - 'poe_mode': StaticSelect(), - 'poe_type': StaticSelect(), - 'duplex': StaticSelect(), - 'mode': StaticSelect(), - 'rf_role': StaticSelect(), - 'rf_channel': StaticSelect(), } labels = { 'mode': '802.1Q Mode', @@ -1471,9 +1386,6 @@ class FrontPortForm(ModularDeviceComponentForm): 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected', 'description', 'tags', ] - widgets = { - 'type': StaticSelect(), - } class RearPortForm(ModularDeviceComponentForm): @@ -1488,9 +1400,6 @@ class RearPortForm(ModularDeviceComponentForm): fields = [ 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags', ] - widgets = { - 'type': StaticSelect(), - } class ModuleBayForm(DeviceComponentForm): @@ -1521,8 +1430,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form): installed_device = forms.ModelChoiceField( queryset=Device.objects.all(), label=_('Child Device'), - help_text=_("Child devices must first be created and assigned to the site/rack of the parent device."), - widget=StaticSelect(), + help_text=_("Child devices must first be created and assigned to the site/rack of the parent device.") ) def __init__(self, device_bay, *args, **kwargs): @@ -1771,8 +1679,3 @@ class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm): 'region', 'site_group', 'site', 'location', 'rack', 'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments', 'tags' ] - widgets = { - 'status': StaticSelect(), - 'primary_ip4': StaticSelect(), - 'primary_ip6': StaticSelect(), - } diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py index 6e245bcaf..47a529772 100644 --- a/netbox/extras/forms/bulk_edit.py +++ b/netbox/extras/forms/bulk_edit.py @@ -4,7 +4,7 @@ from django.utils.translation import gettext as _ from extras.choices import * from extras.models import * from utilities.forms import ( - add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, StaticSelect, + add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, ) __all__ = ( @@ -41,8 +41,7 @@ class CustomFieldBulkEditForm(BulkEditForm): label=_("UI visibility"), choices=add_blank_choice(CustomFieldVisibilityChoices), required=False, - initial='', - widget=StaticSelect() + initial='' ) nullable_fields = ('group_name', 'description',) @@ -66,8 +65,7 @@ class CustomLinkBulkEditForm(BulkEditForm): ) button_class = forms.ChoiceField( choices=add_blank_choice(CustomLinkButtonClassChoices), - required=False, - widget=StaticSelect() + required=False ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 5c7a10ac8..4a92ff606 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -12,8 +12,8 @@ from netbox.forms.base import NetBoxModelFilterSetForm from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelectMultiple, BOOLEAN_WITH_BLANK_CHOICES, ContentTypeChoiceField, - ContentTypeMultipleChoiceField, DateTimePicker, DynamicModelMultipleChoiceField, FilterForm, MultipleChoiceField, - StaticSelect, TagFilterField, + ContentTypeMultipleChoiceField, DateTimePicker, DynamicModelMultipleChoiceField, FilterForm, + TagFilterField, ) from virtualization.models import Cluster, ClusterGroup, ClusterType from .mixins import SavedFiltersMixin @@ -43,7 +43,7 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): required=False, label=_('Object type') ) - type = MultipleChoiceField( + type = forms.MultipleChoiceField( choices=CustomFieldTypeChoices, required=False, label=_('Field type') @@ -56,15 +56,14 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): ) required = forms.NullBooleanField( required=False, - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) ui_visibility = forms.ChoiceField( choices=add_blank_choice(CustomFieldVisibilityChoices), required=False, - label=_('UI visibility'), - widget=StaticSelect() + label=_('UI visibility') ) @@ -82,7 +81,7 @@ class JobResultFilterForm(SavedFiltersMixin, FilterForm): queryset=ContentType.objects.filter(FeatureQuery('job_results').get_query()), required=False, ) - status = MultipleChoiceField( + status = forms.MultipleChoiceField( choices=JobResultStatusChoices, required=False ) @@ -139,13 +138,13 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm): ) enabled = forms.NullBooleanField( required=False, - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) new_window = forms.NullBooleanField( required=False, - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) @@ -186,7 +185,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): ) as_attachment = forms.NullBooleanField( required=False, - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) @@ -203,13 +202,13 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm): ) enabled = forms.NullBooleanField( required=False, - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) shared = forms.NullBooleanField( required=False, - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) @@ -229,32 +228,32 @@ class WebhookFilterForm(SavedFiltersMixin, FilterForm): required=False, label=_('Object type') ) - http_method = MultipleChoiceField( + http_method = forms.MultipleChoiceField( choices=WebhookHttpMethodChoices, required=False, label=_('HTTP method') ) enabled = forms.NullBooleanField( required=False, - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) type_create = forms.NullBooleanField( required=False, - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) type_update = forms.NullBooleanField( required=False, - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) type_delete = forms.NullBooleanField( required=False, - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) @@ -363,7 +362,7 @@ class LocalConfigContextFilterForm(forms.Form): local_context_data = forms.NullBooleanField( required=False, label=_('Has local config context data'), - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) @@ -404,8 +403,7 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm): ) kind = forms.ChoiceField( choices=add_blank_choice(JournalEntryKindChoices), - required=False, - widget=StaticSelect() + required=False ) tag = TagFilterField(model) @@ -429,8 +427,7 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm): ) action = forms.ChoiceField( choices=add_blank_choice(ObjectChangeActionChoices), - required=False, - widget=StaticSelect() + required=False ) user_id = DynamicModelMultipleChoiceField( queryset=User.objects.all(), diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 0ffc5117c..69c124ee2 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -12,7 +12,7 @@ from netbox.forms import NetBoxModelForm from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, - DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect, + DynamicModelMultipleChoiceField, JSONField, SlugField, ) from virtualization.models import Cluster, ClusterGroup, ClusterType @@ -58,11 +58,6 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm): 'type': _("The type of data stored in this field. For object/multi-object fields, select the related object " "type below.") } - widgets = { - 'type': StaticSelect(), - 'filter_logic': StaticSelect(), - 'ui_visibility': StaticSelect(), - } class CustomLinkForm(BootstrapMixin, forms.ModelForm): @@ -80,7 +75,6 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm): model = CustomLink fields = '__all__' widgets = { - 'button_class': StaticSelect(), 'link_text': forms.Textarea(attrs={'class': 'font-monospace'}), 'link_url': forms.Textarea(attrs={'class': 'font-monospace'}), } @@ -172,7 +166,6 @@ class WebhookForm(BootstrapMixin, forms.ModelForm): 'type_delete': 'Deletions', } widgets = { - 'http_method': StaticSelect(), 'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}), 'body_template': forms.Textarea(attrs={'class': 'font-monospace'}), 'conditions': forms.Textarea(attrs={'class': 'font-monospace'}), @@ -288,8 +281,7 @@ class ImageAttachmentForm(BootstrapMixin, forms.ModelForm): class JournalEntryForm(NetBoxModelForm): kind = forms.ChoiceField( choices=add_blank_choice(JournalEntryKindChoices), - required=False, - widget=StaticSelect() + required=False ) comments = CommentField() diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 021a2005a..8141ca76d 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -1,6 +1,6 @@ +import decimal import re from datetime import datetime, date -import decimal import django_filters from django import forms @@ -24,12 +24,11 @@ from utilities.forms.fields import ( CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, LaxURLField, ) -from utilities.forms.widgets import DatePicker, StaticSelectMultiple, StaticSelect from utilities.forms.utils import add_blank_choice +from utilities.forms.widgets import DatePicker from utilities.querysets import RestrictedQuerySet from utilities.validators import validate_regex - __all__ = ( 'CustomField', 'CustomFieldManager', @@ -374,7 +373,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): (False, 'False'), ) field = forms.NullBooleanField( - required=required, initial=initial, widget=StaticSelect(choices=choices) + required=required, initial=initial, widget=forms.Select(choices=choices) ) # Date @@ -395,14 +394,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): if self.type == CustomFieldTypeChoices.TYPE_SELECT: field_class = CSVChoiceField if for_csv_import else forms.ChoiceField - field = field_class( - choices=choices, required=required, initial=initial, widget=StaticSelect() - ) + field = field_class(choices=choices, required=required, initial=initial) else: field_class = CSVMultipleChoiceField if for_csv_import else forms.MultipleChoiceField - field = field_class( - choices=choices, required=required, initial=initial, widget=StaticSelectMultiple() - ) + field = field_class(choices=choices, required=required, initial=initial) # URL elif self.type == CustomFieldTypeChoices.TYPE_URL: diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index ed5ca53f5..e63b34d75 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -9,8 +9,8 @@ from ipam.models import ASN from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import ( - add_blank_choice, BulkEditNullBooleanSelect, CommentField, DynamicModelChoiceField, NumericArrayField, - StaticSelect, DynamicModelMultipleChoiceField, + add_blank_choice, BulkEditNullBooleanSelect, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, + NumericArrayField, ) __all__ = ( @@ -205,8 +205,7 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm): ) status = forms.ChoiceField( choices=add_blank_choice(PrefixStatusChoices), - required=False, - widget=StaticSelect() + required=False ) role = DynamicModelChoiceField( queryset=Role.objects.all(), @@ -254,8 +253,7 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm): ) status = forms.ChoiceField( choices=add_blank_choice(IPRangeStatusChoices), - required=False, - widget=StaticSelect() + required=False ) role = DynamicModelChoiceField( queryset=Role.objects.all(), @@ -296,13 +294,11 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm): ) status = forms.ChoiceField( choices=add_blank_choice(IPAddressStatusChoices), - required=False, - widget=StaticSelect() + required=False ) role = forms.ChoiceField( choices=add_blank_choice(IPAddressRoleChoices), - required=False, - widget=StaticSelect() + required=False ) dns_name = forms.CharField( max_length=255, @@ -331,8 +327,7 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm): class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm): protocol = forms.ChoiceField( choices=add_blank_choice(FHRPGroupProtocolChoices), - required=False, - widget=StaticSelect() + required=False ) group_id = forms.IntegerField( min_value=0, @@ -342,7 +337,6 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm): auth_type = forms.ChoiceField( choices=add_blank_choice(FHRPGroupAuthTypeChoices), required=False, - widget=StaticSelect(), label=_('Authentication type') ) auth_key = forms.CharField( @@ -430,8 +424,7 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm): ) status = forms.ChoiceField( choices=add_blank_choice(VLANStatusChoices), - required=False, - widget=StaticSelect() + required=False ) role = DynamicModelChoiceField( queryset=Role.objects.all(), @@ -459,8 +452,7 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm): class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm): protocol = forms.ChoiceField( choices=add_blank_choice(ServiceProtocolChoices), - required=False, - widget=StaticSelect() + required=False ) ports = NumericArrayField( base_field=forms.IntegerField( @@ -492,8 +484,7 @@ class ServiceBulkEditForm(ServiceTemplateBulkEditForm): class L2VPNBulkEditForm(NetBoxModelBulkEditForm): type = forms.ChoiceField( choices=add_blank_choice(L2VPNTypeChoices), - required=False, - widget=StaticSelect() + required=False ) tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index 7e790a68a..1d505a168 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -10,7 +10,7 @@ from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm from utilities.forms import ( add_blank_choice, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, - MultipleChoiceField, StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, + TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import VirtualMachine @@ -87,7 +87,7 @@ class RIRFilterForm(NetBoxModelFilterSetForm): is_private = forms.NullBooleanField( required=False, label=_('Private'), - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) @@ -104,8 +104,7 @@ class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): family = forms.ChoiceField( required=False, choices=add_blank_choice(IPAddressFamilyChoices), - label=_('Address family'), - widget=StaticSelect() + label=_('Address family') ) rir_id = DynamicModelMultipleChoiceField( queryset=RIR.objects.all(), @@ -164,10 +163,9 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): family = forms.ChoiceField( required=False, choices=add_blank_choice(IPAddressFamilyChoices), - label=_('Address family'), - widget=StaticSelect() + label=_('Address family') ) - mask_length = MultipleChoiceField( + mask_length = forms.MultipleChoiceField( required=False, choices=PREFIX_MASK_LENGTH_CHOICES, label=_('Mask length') @@ -183,7 +181,7 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): required=False, label=_('Present in VRF') ) - status = MultipleChoiceField( + status = forms.MultipleChoiceField( choices=PrefixStatusChoices, required=False ) @@ -215,14 +213,14 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): is_pool = forms.NullBooleanField( required=False, label=_('Is a pool'), - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) mark_utilized = forms.NullBooleanField( required=False, label=_('Marked as 100% utilized'), - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) @@ -239,8 +237,7 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): family = forms.ChoiceField( required=False, choices=add_blank_choice(IPAddressFamilyChoices), - label=_('Address family'), - widget=StaticSelect() + label=_('Address family') ) vrf_id = DynamicModelMultipleChoiceField( queryset=VRF.objects.all(), @@ -248,7 +245,7 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): label=_('Assigned VRF'), null_option='Global' ) - status = MultipleChoiceField( + status = forms.MultipleChoiceField( choices=IPRangeStatusChoices, required=False ) @@ -282,14 +279,12 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): family = forms.ChoiceField( required=False, choices=add_blank_choice(IPAddressFamilyChoices), - label=_('Address family'), - widget=StaticSelect() + label=_('Address family') ) mask_length = forms.ChoiceField( required=False, choices=IPADDRESS_MASK_LENGTH_CHOICES, - label=_('Mask length'), - widget=StaticSelect() + label=_('Mask length') ) vrf_id = DynamicModelMultipleChoiceField( queryset=VRF.objects.all(), @@ -312,18 +307,18 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): required=False, label=_('Assigned VM'), ) - status = MultipleChoiceField( + status = forms.MultipleChoiceField( choices=IPAddressStatusChoices, required=False ) - role = MultipleChoiceField( + role = forms.MultipleChoiceField( choices=IPAddressRoleChoices, required=False ) assigned_to_interface = forms.NullBooleanField( required=False, label=_('Assigned to an interface'), - widget=StaticSelect( + widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) @@ -340,7 +335,7 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm): name = forms.CharField( required=False ) - protocol = MultipleChoiceField( + protocol = forms.MultipleChoiceField( choices=FHRPGroupProtocolChoices, required=False ) @@ -349,7 +344,7 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm): required=False, label='Group ID' ) - auth_type = MultipleChoiceField( + auth_type = forms.MultipleChoiceField( choices=FHRPGroupAuthTypeChoices, required=False, label='Authentication type' @@ -444,7 +439,7 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): }, label=_('VLAN group') ) - status = MultipleChoiceField( + status = forms.MultipleChoiceField( choices=VLANStatusChoices, required=False ) @@ -474,8 +469,7 @@ class ServiceTemplateFilterForm(NetBoxModelFilterSetForm): ) protocol = forms.ChoiceField( choices=add_blank_choice(ServiceProtocolChoices), - required=False, - widget=StaticSelect() + required=False ) port = forms.IntegerField( required=False, @@ -497,8 +491,7 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): ) type = forms.ChoiceField( choices=add_blank_choice(L2VPNTypeChoices), - required=False, - widget=StaticSelect() + required=False ) import_target_id = DynamicModelMultipleChoiceField( queryset=RouteTarget.objects.all(), diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 430a4b2f8..4e50c4949 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -13,7 +13,7 @@ from tenancy.forms import TenancyForm from utilities.exceptions import PermissionsViolation from utilities.forms import ( add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField, DatePicker, DynamicModelChoiceField, - DynamicModelMultipleChoiceField, NumericArrayField, SlugField, StaticSelect, StaticSelectMultiple, + DynamicModelMultipleChoiceField, NumericArrayField, SlugField, ) from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface @@ -254,9 +254,6 @@ class PrefixForm(TenancyForm, NetBoxModelForm): 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'tenant_group', 'tenant', 'description', 'comments', 'tags', ] - widgets = { - 'status': StaticSelect(), - } class IPRangeForm(TenancyForm, NetBoxModelForm): @@ -282,9 +279,6 @@ class IPRangeForm(TenancyForm, NetBoxModelForm): 'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', 'description', 'comments', 'tags', ] - widgets = { - 'status': StaticSelect(), - } class IPAddressForm(TenancyForm, NetBoxModelForm): @@ -411,10 +405,6 @@ class IPAddressForm(TenancyForm, NetBoxModelForm): 'nat_cluster', 'nat_virtual_machine', 'nat_vrf', 'nat_inside', 'tenant_group', 'tenant', 'description', 'comments', 'tags', ] - widgets = { - 'status': StaticSelect(), - 'role': StaticSelect(), - } def __init__(self, *args, **kwargs): @@ -510,10 +500,6 @@ class IPAddressBulkAddForm(TenancyForm, NetBoxModelForm): fields = [ 'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'tenant_group', 'tenant', 'tags', ] - widgets = { - 'status': StaticSelect(), - 'role': StaticSelect(), - } class IPAddressAssignForm(BootstrapMixin, forms.Form): @@ -559,11 +545,6 @@ class FHRPGroupForm(NetBoxModelForm): 'protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'ip_vrf', 'ip_address', 'ip_status', 'description', 'comments', 'tags', ) - widgets = { - 'protocol': StaticSelect(), - 'auth_type': StaticSelect(), - 'ip_status': StaticSelect(), - } def save(self, *args, **kwargs): instance = super().save(*args, **kwargs) @@ -700,9 +681,6 @@ class VLANGroupForm(NetBoxModelForm): 'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster', 'min_vid', 'max_vid', 'tags', ] - widgets = { - 'scope_type': StaticSelect, - } def __init__(self, *args, **kwargs): instance = kwargs.get('instance') @@ -740,7 +718,6 @@ class VLANForm(TenancyForm, NetBoxModelForm): ('virtualization.cluster', 'Cluster'), ), required=False, - widget=StaticSelect, label=_('Group scope') ) group = DynamicModelChoiceField( @@ -800,9 +777,6 @@ class VLANForm(TenancyForm, NetBoxModelForm): 'status': _("Operational status of this VLAN"), 'role': _("The primary function of this VLAN"), } - widgets = { - 'status': StaticSelect(), - } class ServiceTemplateForm(NetBoxModelForm): @@ -824,9 +798,6 @@ class ServiceTemplateForm(NetBoxModelForm): class Meta: model = ServiceTemplate fields = ('name', 'protocol', 'ports', 'description', 'comments', 'tags') - widgets = { - 'protocol': StaticSelect(), - } class ServiceForm(NetBoxModelForm): @@ -865,10 +836,6 @@ class ServiceForm(NetBoxModelForm): 'ipaddresses': _("IP address assignment is optional. If no IPs are selected, the service is assumed to be " "reachable via all IPs assigned to the device."), } - widgets = { - 'protocol': StaticSelect(), - 'ipaddresses': StaticSelectMultiple(), - } class ServiceCreateForm(ServiceForm): @@ -934,9 +901,6 @@ class L2VPNForm(TenancyForm, NetBoxModelForm): 'name', 'slug', 'type', 'identifier', 'import_targets', 'export_targets', 'tenant', 'description', 'comments', 'tags' ) - widgets = { - 'type': StaticSelect(), - } class L2VPNTerminationForm(NetBoxModelForm): diff --git a/netbox/netbox/forms/__init__.py b/netbox/netbox/forms/__init__.py index d8acef94c..65460ebf1 100644 --- a/netbox/netbox/forms/__init__.py +++ b/netbox/netbox/forms/__init__.py @@ -5,7 +5,7 @@ from django.utils.translation import gettext as _ from netbox.search import LookupTypes from netbox.search.backends import search_backend -from utilities.forms import BootstrapMixin, StaticSelect, StaticSelectMultiple +from utilities.forms import BootstrapMixin from .base import * @@ -32,14 +32,12 @@ class SearchForm(BootstrapMixin, forms.Form): obj_types = forms.MultipleChoiceField( choices=[], required=False, - label=_('Object type(s)'), - widget=StaticSelectMultiple() + label=_('Object type(s)') ) lookup = forms.ChoiceField( choices=LOOKUP_CHOICES, initial=LookupTypes.PARTIAL, - required=False, - widget=StaticSelect() + required=False ) def __init__(self, *args, **kwargs): diff --git a/netbox/tenancy/forms/filtersets.py b/netbox/tenancy/forms/filtersets.py index 7f843d9a4..626d26785 100644 --- a/netbox/tenancy/forms/filtersets.py +++ b/netbox/tenancy/forms/filtersets.py @@ -1,3 +1,4 @@ +from django import forms from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ @@ -7,7 +8,7 @@ from tenancy.choices import * from tenancy.models import * from tenancy.forms import ContactModelFilterForm from utilities.forms.fields import ( - ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, MultipleChoiceField, TagFilterField, + ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField, ) __all__ = ( @@ -106,7 +107,7 @@ class ContactAssignmentFilterForm(NetBoxModelFilterSetForm): required=False, label=_('Role') ) - priority = MultipleChoiceField( + priority = forms.MultipleChoiceField( choices=ContactPriorityChoices, required=False ) diff --git a/netbox/tenancy/forms/model_forms.py b/netbox/tenancy/forms/model_forms.py index e835194ff..a27e41f74 100644 --- a/netbox/tenancy/forms/model_forms.py +++ b/netbox/tenancy/forms/model_forms.py @@ -3,7 +3,7 @@ from django import forms from netbox.forms import NetBoxModelForm from tenancy.models import * from utilities.forms import ( - BootstrapMixin, CommentField, DynamicModelChoiceField, SlugField, StaticSelect, + BootstrapMixin, CommentField, DynamicModelChoiceField, SlugField, ) __all__ = ( @@ -142,5 +142,4 @@ class ContactAssignmentForm(BootstrapMixin, forms.ModelForm): widgets = { 'content_type': forms.HiddenInput(), 'object_id': forms.HiddenInput(), - 'priority': StaticSelect(), } diff --git a/netbox/users/forms.py b/netbox/users/forms.py index e8647aa5f..0c7d7ea19 100644 --- a/netbox/users/forms.py +++ b/netbox/users/forms.py @@ -7,7 +7,7 @@ from django.utils.translation import gettext as _ from ipam.formfields import IPNetworkFormField from netbox.preferences import PREFERENCES -from utilities.forms import BootstrapMixin, DateTimePicker, StaticSelect +from utilities.forms import BootstrapMixin, DateTimePicker from utilities.utils import flatten_dict from .models import Token, UserConfig @@ -35,7 +35,7 @@ class UserConfigFormMetaclass(forms.models.ModelFormMetaclass): 'help_text': mark_safe(help_text), 'coerce': preference.coerce, 'required': False, - 'widget': StaticSelect, + 'widget': forms.Select, } preference_fields[field_name] = forms.TypedChoiceField(**field_kwargs) attrs.update(preference_fields) diff --git a/netbox/utilities/forms/fields/content_types.py b/netbox/utilities/forms/fields/content_types.py index 80861166c..76efe9a7b 100644 --- a/netbox/utilities/forms/fields/content_types.py +++ b/netbox/utilities/forms/fields/content_types.py @@ -27,11 +27,11 @@ class ContentTypeChoiceField(ContentTypeChoiceMixin, forms.ModelChoiceField): """ Selection field for a single content type. """ - widget = widgets.StaticSelect + pass class ContentTypeMultipleChoiceField(ContentTypeChoiceMixin, forms.ModelMultipleChoiceField): """ Selection field for one or more content types. """ - widget = widgets.StaticSelectMultiple + pass diff --git a/netbox/utilities/forms/fields/fields.py b/netbox/utilities/forms/fields/fields.py index bb6c3f73b..c5d2d0a1f 100644 --- a/netbox/utilities/forms/fields/fields.py +++ b/netbox/utilities/forms/fields/fields.py @@ -68,7 +68,6 @@ class TagFilterField(forms.MultipleChoiceField): :param model: The model of the filter """ - widget = widgets.StaticSelectMultiple def __init__(self, model, *args, **kwargs): def get_choices(): @@ -137,13 +136,16 @@ class MACAddressField(forms.Field): class ChoiceField(forms.ChoiceField): """ - Overrides Django's built-in `ChoiceField` to use NetBox's `StaticSelect` widget + Previously used to override Django's built-in `ChoiceField` to use NetBox's now-obsolete `StaticSelect` widget. """ - widget = widgets.StaticSelect + # TODO: Remove in v3.6 + pass class MultipleChoiceField(forms.MultipleChoiceField): """ - Overrides Django's built-in `MultipleChoiceField` to use NetBox's `StaticSelectMultiple` widget + Previously used to override Django's built-in `MultipleChoiceField` to use NetBox's now-obsolete + `StaticSelectMultiple` widget. """ - widget = widgets.StaticSelectMultiple + # TODO: Remove in v3.6 + pass diff --git a/netbox/utilities/forms/forms.py b/netbox/utilities/forms/forms.py index 9884ffac5..eee8775b8 100644 --- a/netbox/utilities/forms/forms.py +++ b/netbox/utilities/forms/forms.py @@ -9,7 +9,7 @@ from django.utils.translation import gettext as _ from utilities.choices import ImportFormatChoices from utilities.forms.utils import parse_csv -from .widgets import APISelect, APISelectMultiple, ClearableFileInput, StaticSelect +from .widgets import APISelect, APISelectMultiple, ClearableFileInput __all__ = ( 'BootstrapMixin', @@ -37,27 +37,27 @@ class BootstrapMixin: super().__init__(*args, **kwargs) exempt_widgets = [ - forms.CheckboxInput, forms.FileInput, forms.RadioSelect, - forms.Select, APISelect, APISelectMultiple, ClearableFileInput, - StaticSelect, ] for field_name, field in self.fields.items(): css = field.widget.attrs.get('class', '') - if field.widget.__class__ not in exempt_widgets: - field.widget.attrs['class'] = f'{css} form-control' + if field.widget.__class__ in exempt_widgets: + continue elif isinstance(field.widget, forms.CheckboxInput): field.widget.attrs['class'] = f'{css} form-check-input' elif isinstance(field.widget, forms.Select): - field.widget.attrs['class'] = f'{css} form-select' + field.widget.attrs['class'] = f'{css} netbox-static-select' + + else: + field.widget.attrs['class'] = f'{css} form-control' if field.required and not isinstance(field.widget, forms.FileInput): field.widget.attrs['required'] = 'required' @@ -165,8 +165,7 @@ class ImportForm(BootstrapMixin, forms.Form): ) format = forms.ChoiceField( choices=ImportFormatChoices, - initial=ImportFormatChoices.AUTO, - widget=StaticSelect() + initial=ImportFormatChoices.AUTO ) data_field = 'data' diff --git a/netbox/utilities/forms/widgets.py b/netbox/utilities/forms/widgets.py index 16ec72ecf..c7e1cfb81 100644 --- a/netbox/utilities/forms/widgets.py +++ b/netbox/utilities/forms/widgets.py @@ -21,8 +21,6 @@ __all__ = ( 'SelectSpeedWidget', 'SelectWithPK', 'SlugWidget', - 'StaticSelect', - 'StaticSelectMultiple', 'TimePicker', ) @@ -68,26 +66,7 @@ class BulkEditNullBooleanSelect(forms.NullBooleanSelect): self.attrs['class'] = 'netbox-static-select' -class StaticSelect(forms.Select): - """ - A static - - - {% endif %} - {% render_field form.module %} - {% render_field form.name %} - {% render_field form.type %} - {% render_field form.speed %} - {% render_field form.duplex %} - {% render_field form.label %} - {% render_field form.description %} - {% render_field form.tags %} - - -
    -
    -
    Addressing
    -
    - {% render_field form.vrf %} - {% render_field form.mac_address %} - {% render_field form.wwn %} -
    - -
    -
    -
    Operation
    -
    - {% render_field form.mtu %} - {% render_field form.tx_power %} - {% render_field form.enabled %} - {% render_field form.mgmt_only %} - {% render_field form.mark_connected %} -
    - -
    -
    -
    Related Interfaces
    -
    - {% render_field form.parent %} - {% render_field form.bridge %} - {% render_field form.lag %} -
    - - {% if form.instance.is_wireless %} -
    -
    -
    Wireless
    -
    - {% render_field form.rf_role %} - {% render_field form.rf_channel %} - {% render_field form.rf_channel_frequency %} - {% render_field form.rf_channel_width %} - {% render_field form.wireless_lan_group %} - {% render_field form.wireless_lans %} -
    - {% endif %} - -
    -
    -
    Power over Ethernet (PoE)
    -
    - {% render_field form.poe_mode %} - {% render_field form.poe_type %} -
    - -
    -
    -
    802.1Q Switching
    -
    - {% render_field form.mode %} - {% render_field form.vlan_group %} - {% render_field form.untagged_vlan %} - {% render_field form.tagged_vlans %} -
    - - {% if form.custom_fields %} -
    -
    -
    Custom Fields
    -
    - {% render_custom_fields form %} -
    - {% endif %} -{% endblock %} diff --git a/netbox/templates/htmx/form.html b/netbox/templates/htmx/form.html index e5a2ab6c6..e15df4706 100644 --- a/netbox/templates/htmx/form.html +++ b/netbox/templates/htmx/form.html @@ -17,7 +17,7 @@ {% endif %} {% for name in fields %} {% with field=form|getfield:name %} - {% if not field.field.widget.is_hidden %} + {% if field and not field.field.widget.is_hidden %} {% render_field field %} {% endif %} {% endwith %} diff --git a/netbox/utilities/forms/utils.py b/netbox/utilities/forms/utils.py index 1a2f62b2e..2f08a3cce 100644 --- a/netbox/utilities/forms/utils.py +++ b/netbox/utilities/forms/utils.py @@ -12,6 +12,7 @@ __all__ = ( 'expand_alphanumeric_pattern', 'expand_ipaddress_pattern', 'form_from_model', + 'get_field_value', 'get_selected_values', 'parse_alphanumeric_range', 'parse_numeric_range', @@ -113,6 +114,21 @@ def expand_ipaddress_pattern(string, family): yield ''.join([lead, format(i, 'x' if family == 6 else 'd'), remnant]) +def get_field_value(form, field_name): + """ + Return the current bound or initial value associated with a form field, prior to calling + clean() for the form. + """ + field = form.fields[field_name] + + if form.is_bound: + if data := form.data.get(field_name): + if field.valid_value(data): + return data + + return form.get_initial_for_field(field, field_name) + + def get_selected_values(form, field_name): """ Return the list of selected human-friendly values for a form field diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 089a3ced9..a3523a7cc 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -11,9 +11,12 @@ register = template.Library() @register.filter() def getfield(form, fieldname): """ - Return the specified field of a Form. + Return the specified bound field of a Form. """ - return form[fieldname] + try: + return form[fieldname] + except KeyError: + return None @register.filter(name='widget_type') diff --git a/netbox/virtualization/forms/model_forms.py b/netbox/virtualization/forms/model_forms.py index 403a04d91..e461eac8a 100644 --- a/netbox/virtualization/forms/model_forms.py +++ b/netbox/virtualization/forms/model_forms.py @@ -349,6 +349,15 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm): labels = { 'mode': '802.1Q Mode', } + widgets = { + 'mode': forms.Select( + attrs={ + 'hx-get': '.', + 'hx-include': '#form_fields input', + 'hx-target': '#form_fields', + } + ), + } help_texts = { 'mode': INTERFACE_MODE_HELP_TEXT, } From c109daf1d86843778397fa9091bf8460bb54f1b1 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Sun, 19 Feb 2023 13:58:01 -0500 Subject: [PATCH 039/174] Clean up the application registry --- netbox/extras/constants.py | 12 ----------- netbox/extras/plugins/__init__.py | 4 ++-- netbox/extras/utils.py | 17 +++++++++------ netbox/netbox/models/features.py | 27 +++++++++++++---------- netbox/netbox/registry.py | 26 ++++++++++------------ netbox/netbox/tests/test_registry.py | 32 +++++++++++----------------- 6 files changed, 53 insertions(+), 65 deletions(-) diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index 7c7fe331e..d65fb9612 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -1,14 +1,2 @@ # Webhook content types HTTP_CONTENT_TYPE_JSON = 'application/json' - -# Registerable extras features -EXTRAS_FEATURES = [ - 'custom_fields', - 'custom_links', - 'export_templates', - 'job_results', - 'journaling', - 'synced_data', - 'tags', - 'webhooks' -] diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index b56113ca1..ee74ad88e 100644 --- a/netbox/extras/plugins/__init__.py +++ b/netbox/extras/plugins/__init__.py @@ -14,13 +14,13 @@ from .registration import * from .templates import * # Initialize plugin registry -registry['plugins'] = { +registry['plugins'].update({ 'graphql_schemas': [], 'menus': [], 'menu_items': {}, 'preferences': {}, 'template_extensions': collections.defaultdict(list), -} +}) DEFAULT_RESOURCE_PATHS = { 'search_indexes': 'search.indexes', diff --git a/netbox/extras/utils.py b/netbox/extras/utils.py index 268bf9e80..f90858bcf 100644 --- a/netbox/extras/utils.py +++ b/netbox/extras/utils.py @@ -2,7 +2,6 @@ from django.db.models import Q from django.utils.deconstruct import deconstructible from taggit.managers import _TaggableManager -from extras.constants import EXTRAS_FEATURES from netbox.registry import registry @@ -18,7 +17,7 @@ def is_taggable(obj): def image_upload(instance, filename): """ - Return a path for uploading image attchments. + Return a path for uploading image attachments. """ path = 'image-attachments/' @@ -56,8 +55,14 @@ class FeatureQuery: def register_features(model, features): + """ + Register model features in the application registry. + """ + app_label, model_name = model._meta.label_lower.split('.') for feature in features: - if feature not in EXTRAS_FEATURES: - raise ValueError(f"{feature} is not a valid extras feature!") - app_label, model_name = model._meta.label_lower.split('.') - registry['model_features'][feature][app_label].add(model_name) + try: + registry['model_features'][feature][app_label].add(model_name) + except KeyError: + raise KeyError( + f"{feature} is not a valid model feature! Valid keys are: {registry['model_features'].keys()}" + ) diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index 2bd0a93d2..e70d3df7b 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -12,6 +12,7 @@ from taggit.managers import TaggableManager from extras.choices import CustomFieldVisibilityChoices, ObjectChangeActionChoices from extras.utils import is_taggable, register_features +from netbox.registry import registry from netbox.signals import post_clean from utilities.json import CustomFieldJSONEncoder from utilities.utils import serialize_object @@ -388,22 +389,26 @@ class SyncedDataMixin(models.Model): raise NotImplementedError(f"{self.__class__} must implement a sync_data() method.") -FEATURES_MAP = ( - ('custom_fields', CustomFieldsMixin), - ('custom_links', CustomLinksMixin), - ('export_templates', ExportTemplatesMixin), - ('job_results', JobResultsMixin), - ('journaling', JournalingMixin), - ('synced_data', SyncedDataMixin), - ('tags', TagsMixin), - ('webhooks', WebhooksMixin), -) +FEATURES_MAP = { + 'custom_fields': CustomFieldsMixin, + 'custom_links': CustomLinksMixin, + 'export_templates': ExportTemplatesMixin, + 'job_results': JobResultsMixin, + 'journaling': JournalingMixin, + 'synced_data': SyncedDataMixin, + 'tags': TagsMixin, + 'webhooks': WebhooksMixin, +} + +registry['model_features'].update({ + feature: defaultdict(set) for feature in FEATURES_MAP.keys() +}) @receiver(class_prepared) def _register_features(sender, **kwargs): features = { - feature for feature, cls in FEATURES_MAP if issubclass(sender, cls) + feature for feature, cls in FEATURES_MAP.items() if issubclass(sender, cls) } register_features(sender, features) diff --git a/netbox/netbox/registry.py b/netbox/netbox/registry.py index 670bca683..e37ee0d0c 100644 --- a/netbox/netbox/registry.py +++ b/netbox/netbox/registry.py @@ -1,12 +1,10 @@ import collections -from extras.constants import EXTRAS_FEATURES - class Registry(dict): """ - Central registry for registration of functionality. Once a store (key) is defined, it cannot be overwritten or - deleted (although its value may be manipulated). + Central registry for registration of functionality. Once a Registry is initialized, keys cannot be added or + removed (though the value of each key is mutable). """ def __getitem__(self, key): try: @@ -15,20 +13,18 @@ class Registry(dict): raise KeyError(f"Invalid store: {key}") def __setitem__(self, key, value): - if key in self: - raise KeyError(f"Store already set: {key}") - super().__setitem__(key, value) + raise TypeError("Cannot add stores to registry after initialization") def __delitem__(self, key): raise TypeError("Cannot delete stores from registry") # Initialize the global registry -registry = Registry() -registry['data_backends'] = dict() -registry['denormalized_fields'] = collections.defaultdict(list) -registry['model_features'] = { - feature: collections.defaultdict(set) for feature in EXTRAS_FEATURES -} -registry['search'] = dict() -registry['views'] = collections.defaultdict(dict) +registry = Registry({ + 'data_backends': dict(), + 'denormalized_fields': collections.defaultdict(list), + 'model_features': dict(), + 'plugins': dict(), + 'search': dict(), + 'views': collections.defaultdict(dict), +}) diff --git a/netbox/netbox/tests/test_registry.py b/netbox/netbox/tests/test_registry.py index 25f9e43ec..e834c4356 100644 --- a/netbox/netbox/tests/test_registry.py +++ b/netbox/netbox/tests/test_registry.py @@ -5,29 +5,23 @@ from netbox.registry import Registry class RegistryTest(TestCase): - def test_add_store(self): - reg = Registry() - reg['foo'] = 123 + def test_set_store(self): + reg = Registry({ + 'foo': 123, + }) + with self.assertRaises(TypeError): + reg['bar'] = 456 - self.assertEqual(reg['foo'], 123) - - def test_manipulate_store(self): - reg = Registry() - reg['foo'] = [1, 2] + def test_mutate_store(self): + reg = Registry({ + 'foo': [1, 2], + }) reg['foo'].append(3) - self.assertListEqual(reg['foo'], [1, 2, 3]) - def test_overwrite_store(self): - reg = Registry() - reg['foo'] = 123 - - with self.assertRaises(KeyError): - reg['foo'] = 456 - def test_delete_store(self): - reg = Registry() - reg['foo'] = 123 - + reg = Registry({ + 'foo': 123, + }) with self.assertRaises(TypeError): del reg['foo'] From 574b5551a0c8407cc549328a8a343eac31f15685 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Sun, 19 Feb 2023 14:31:46 -0500 Subject: [PATCH 040/174] Clean up model & registry documentation --- docs/development/application-registry.md | 43 ++++++++----------- docs/development/models.md | 50 ++++++++++++++-------- docs/features/background-jobs.md | 13 ++++++ docs/integrations/synchronized-data.md | 9 ++++ docs/models/extras/jobresult.md | 54 ++++++++++++++++++++++++ mkdocs.yml | 6 +++ 6 files changed, 133 insertions(+), 42 deletions(-) create mode 100644 docs/features/background-jobs.md create mode 100644 docs/integrations/synchronized-data.md create mode 100644 docs/models/extras/jobresult.md diff --git a/docs/development/application-registry.md b/docs/development/application-registry.md index c2f894711..fe2c08d56 100644 --- a/docs/development/application-registry.md +++ b/docs/development/application-registry.md @@ -8,6 +8,14 @@ The registry can be inspected by importing `registry` from `extras.registry`. ## Stores +### `data_backends` + +A dictionary mapping data backend types to their respective classes. These are used to interact with [remote data sources](../models/core/datasource.md). + +### `denormalized_fields` + +Stores registration made using `netbox.denormalized.register()`. For each model, a list of related models and their field mappings is maintained to facilitate automatic updates. + ### `model_features` A dictionary of particular features (e.g. custom fields) mapped to the NetBox models which support them, arranged by app. For example: @@ -20,38 +28,23 @@ A dictionary of particular features (e.g. custom fields) mapped to the NetBox mo ... }, 'webhooks': { - ... + 'extras': ['configcontext', 'tag', ...], + 'dcim': ['site', 'rack', 'devicetype', ...], }, ... } ``` -### `plugin_menu_items` +Supported model features are listed in the [features matrix](./models.md#features-matrix). -Navigation menu items provided by NetBox plugins. Each plugin is registered as a key with the list of menu items it provides. An example: +### `plugins` -```python -{ - 'Plugin A': ( - , , , - ), - 'Plugin B': ( - , , , - ), -} -``` +This store maintains all registered items for plugins, such as navigation menus, template extensions, etc. -### `plugin_template_extensions` +### `search` -Plugin content that gets embedded into core NetBox templates. The store comprises NetBox models registered as dictionary keys, each pointing to a list of applicable template extension classes that exist. An example: +A dictionary mapping each model (identified by its app and label) to its search index class, if one has been registered for it. -```python -{ - 'dcim.site': [ - , , , - ], - 'dcim.rack': [ - , , - ], -} -``` +### `views` + +A hierarchical mapping of registered views for each model. Mappings are added using the `register_model_view()` decorator, and URLs paths can be generated from these using `get_model_urls()`. diff --git a/docs/development/models.md b/docs/development/models.md index af11617c8..6f3998977 100644 --- a/docs/development/models.md +++ b/docs/development/models.md @@ -2,38 +2,43 @@ ## Model Types -A NetBox model represents a discrete object type such as a device or IP address. Per [Django convention](https://docs.djangoproject.com/en/stable/topics/db/models/), each model is defined as a Python class and has its own SQL table. All NetBox data models can be categorized by type. +A NetBox model represents a discrete object type such as a device or IP address. Per [Django convention](https://docs.djangoproject.com/en/stable/topics/db/models/), each model is defined as a Python class and has its own table in the PostgreSQL database. All NetBox data models can be categorized by type. -The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/) framework can be used to reference models within the database. A ContentType instance references a model by its `app_label` and `name`: For example, the Site model is referred to as `dcim.site`. The content type combined with an object's primary key form a globally unique identifier for the object (e.g. `dcim.site:123`). +The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/) framework is used to map Django models to database tables. A ContentType instance references a model by its `app_label` and `name`: For example, the Site model within the DCIM app is referred to as `dcim.site`. The content type combined with an object's primary key form a globally unique identifier for the object (e.g. `dcim.site:123`). ### Features Matrix -* [Change logging](../features/change-logging.md) - Changes to these objects are automatically recorded in the change log -* [Webhooks](../integrations/webhooks.md) - NetBox is capable of generating outgoing webhooks for these objects -* [Custom fields](../customization/custom-fields.md) - These models support the addition of user-defined fields -* [Export templates](../customization/export-templates.md) - Users can create custom export templates for these models -* [Tagging](../models/extras/tag.md) - The models can be tagged with user-defined tags -* [Journaling](../features/journaling.md) - These models support persistent historical commentary -* Nesting - These models can be nested recursively to create a hierarchy +Depending on its classification, each NetBox model may support various features which enhance its operation. Each feature is enabled by inheriting from its designated mixin class, and some features also make use of the [application registry](./application-registry.md#model_features). -| Type | Change Logging | Webhooks | Custom Fields | Export Templates | Tags | Journaling | Nesting | -| ------------------ | ---------------- | ---------------- |------------------| ---------------- | ---------------- | ---------------- | ---------------- | -| Primary | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | -| Organizational | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | | -| Nested Group | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | :material-check: | -| Component | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | | -| Component Template | :material-check: | :material-check: | | | | | | +| Feature | Feature Mixin | Registry Key | Description | +|------------------------------------------------------------|-------------------------|--------------------|--------------------------------------------------------------------------------| +| [Change logging](../features/change-logging.md) | `ChangeLoggingMixin` | - | Changes to these objects are automatically recorded in the change log | +| Cloning | `CloningMixin` | - | Provides the `clone()` method to prepare a copy | +| [Custom fields](../customization/custom-fields.md) | `CustomFieldsMixin` | `custom_fields` | These models support the addition of user-defined fields | +| [Custom links](../customization/custom-links.md) | `CustomLinksMixin` | `custom_links` | These models support the assignment of custom links | +| [Custom validation](../customization/custom-validation.md) | `CustomValidationMixin` | - | Supports the enforcement of custom validation rules | +| [Export templates](../customization/export-templates.md) | `ExportTemplatesMixin` | `export_templates` | Users can create custom export templates for these models | +| [Job results](../features/background-jobs.md) | `JobResultsMixin` | `job_results` | Users can create custom export templates for these models | +| [Journaling](../features/journaling.md) | `JournalingMixin` | `journaling` | These models support persistent historical commentary | +| [Synchronized data](../integrations/synchronized-data.md) | `SyncedDataMixin` | `synced_data` | Certain model data can be automatically synchronized from a remote data source | +| [Tagging](../models/extras/tag.md) | `TagsMixin` | `tags` | The models can be tagged with user-defined tags | +| [Webhooks](../integrations/webhooks.md) | `WebhooksMixin` | `webhooks` | NetBox is capable of generating outgoing webhooks for these objects | ## Models Index ### Primary Models +These are considered the "core" application models which are used to model network infrastructure. + * [circuits.Circuit](../models/circuits/circuit.md) * [circuits.Provider](../models/circuits/provider.md) * [circuits.ProviderNetwork](../models/circuits/providernetwork.md) +* [core.DataSource](../models/core/datasource.md) * [dcim.Cable](../models/dcim/cable.md) * [dcim.Device](../models/dcim/device.md) * [dcim.DeviceType](../models/dcim/devicetype.md) +* [dcim.Module](../models/dcim/module.md) +* [dcim.ModuleType](../models/dcim/moduletype.md) * [dcim.PowerFeed](../models/dcim/powerfeed.md) * [dcim.PowerPanel](../models/dcim/powerpanel.md) * [dcim.Rack](../models/dcim/rack.md) @@ -47,10 +52,10 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/ * [ipam.IPAddress](../models/ipam/ipaddress.md) * [ipam.IPRange](../models/ipam/iprange.md) * [ipam.L2VPN](../models/ipam/l2vpn.md) -* [ipam.L2VPNTermination](../models/ipam/l2vpntermination.md) * [ipam.Prefix](../models/ipam/prefix.md) * [ipam.RouteTarget](../models/ipam/routetarget.md) * [ipam.Service](../models/ipam/service.md) +* [ipam.ServiceTemplate](../models/ipam/servicetemplate.md) * [ipam.VLAN](../models/ipam/vlan.md) * [ipam.VRF](../models/ipam/vrf.md) * [tenancy.Contact](../models/tenancy/contact.md) @@ -62,6 +67,8 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/ ### Organizational Models +Organization models are used to organize and classify primary models. + * [circuits.CircuitType](../models/circuits/circuittype.md) * [dcim.DeviceRole](../models/dcim/devicerole.md) * [dcim.Manufacturer](../models/dcim/manufacturer.md) @@ -76,6 +83,8 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/ ### Nested Group Models +Nested group models behave like organizational model, but self-nest within a recursive hierarchy. For example, the Region model can be used to represent a hierarchy of countries, states, and cities. + * [dcim.Location](../models/dcim/location.md) (formerly RackGroup) * [dcim.Region](../models/dcim/region.md) * [dcim.SiteGroup](../models/dcim/sitegroup.md) @@ -85,12 +94,15 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/ ### Component Models +Component models represent individual physical or virtual components belonging to a device or virtual machine. + * [dcim.ConsolePort](../models/dcim/consoleport.md) * [dcim.ConsoleServerPort](../models/dcim/consoleserverport.md) * [dcim.DeviceBay](../models/dcim/devicebay.md) * [dcim.FrontPort](../models/dcim/frontport.md) * [dcim.Interface](../models/dcim/interface.md) * [dcim.InventoryItem](../models/dcim/inventoryitem.md) +* [dcim.ModuleBay](../models/dcim/modulebay.md) * [dcim.PowerOutlet](../models/dcim/poweroutlet.md) * [dcim.PowerPort](../models/dcim/powerport.md) * [dcim.RearPort](../models/dcim/rearport.md) @@ -98,11 +110,15 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/ ### Component Template Models +These function as templates to effect the replication of device and virtual machine components. Component template models support a limited feature set, including change logging, custom validation, and webhooks. + * [dcim.ConsolePortTemplate](../models/dcim/consoleporttemplate.md) * [dcim.ConsoleServerPortTemplate](../models/dcim/consoleserverporttemplate.md) * [dcim.DeviceBayTemplate](../models/dcim/devicebaytemplate.md) * [dcim.FrontPortTemplate](../models/dcim/frontporttemplate.md) * [dcim.InterfaceTemplate](../models/dcim/interfacetemplate.md) +* [dcim.InventoryItemTemplate](../models/dcim/inventoryitemtemplate.md) +* [dcim.ModuleBayTemplate](../models/dcim/modulebaytemplate.md) * [dcim.PowerOutletTemplate](../models/dcim/poweroutlettemplate.md) * [dcim.PowerPortTemplate](../models/dcim/powerporttemplate.md) * [dcim.RearPortTemplate](../models/dcim/rearporttemplate.md) diff --git a/docs/features/background-jobs.md b/docs/features/background-jobs.md new file mode 100644 index 000000000..a36192ab3 --- /dev/null +++ b/docs/features/background-jobs.md @@ -0,0 +1,13 @@ +# Background Jobs + +NetBox includes the ability to execute certain functions as background tasks. These include: + +* [Report](../customization/reports.md) execution +* [Custom script](../customization/custom-scripts.md) execution +* Synchronization of [remote data sources](../integrations/synchronized-data.md) + +Additionally, NetBox plugins can enqueue their own background tasks. This is accomplished using the [JobResult model](../models/extras/jobresult.md). Background tasks are executed by the `rqworker` process(es). + +## Scheduled Jobs + +Background jobs can be configured to run immediately, or at a set time in the future. Scheduled jobs can also be configured to repeat at a set interval. diff --git a/docs/integrations/synchronized-data.md b/docs/integrations/synchronized-data.md new file mode 100644 index 000000000..805cbe15b --- /dev/null +++ b/docs/integrations/synchronized-data.md @@ -0,0 +1,9 @@ +# Synchronized Data + +Some NetBox models support automatic synchronization of certain attributes from remote [data sources](../models/core/datasource.md), such as a git repository hosted on GitHub or GitLab. Data from the authoritative remote source is synchronized locally in NetBox as [data files](../models/core/datafile.md). + +The following features support the use of synchronized data: + +* [Configuration templates](../features/configuration-rendering.md) +* [Configuration context data](../features/context-data.md) +* [Export templates](../customization/export-templates.md) diff --git a/docs/models/extras/jobresult.md b/docs/models/extras/jobresult.md new file mode 100644 index 000000000..81ab75745 --- /dev/null +++ b/docs/models/extras/jobresult.md @@ -0,0 +1,54 @@ +# Job Results + +The JobResult model is used to schedule and record the execution of [background tasks](../../features/background-jobs.md). + +## Fields + +### Name + +The name or other identifier of the NetBox object with which the job is associated. + +## Object Type + +The type of object (model) associated with this job. + +### Created + +The date and time at which the job itself was created. + +### Scheduled + +The date and time at which the job is/was scheduled to execute (if not submitted for immediate execution at the time of creation). + +### Interval + +The interval (in minutes) at which a scheduled job should re-execute. + +### Completed + +The date and time at which the job completed (if complete). + +### User + +The user who created the job. + +### Status + +The job's current status. Potential values include: + +| Value | Description | +|-------|-------------| +| Pending | Awaiting execution by an RQ worker process | +| Scheduled | Scheduled for a future date/time | +| Running | Currently executing | +| Completed | Successfully completed | +| Failed | The job did not complete successfully | +| Errored | An unexpected error was encountered during execution | + +### Data + +Any data associated with the execution of the job, such as log output. + +### Job ID + +The job's UUID, used for unique identification within a queue. diff --git a/mkdocs.yml b/mkdocs.yml index fcfe0d21d..2487176d3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -77,6 +77,7 @@ nav: - Configuration Rendering: 'features/configuration-rendering.md' - Change Logging: 'features/change-logging.md' - Journaling: 'features/journaling.md' + - Background Jobs: 'features/background-jobs.md' - Auth & Permissions: 'features/authentication-permissions.md' - API & Integration: 'features/api-integration.md' - Customization: 'features/customization.md' @@ -117,6 +118,7 @@ nav: - REST API: 'integrations/rest-api.md' - GraphQL API: 'integrations/graphql-api.md' - Webhooks: 'integrations/webhooks.md' + - Synchronized Data: 'integrations/synchronized-data.md' - NAPALM: 'integrations/napalm.md' - Prometheus Metrics: 'integrations/prometheus-metrics.md' - Plugins: @@ -153,6 +155,9 @@ nav: - Circuit Type: 'models/circuits/circuittype.md' - Provider: 'models/circuits/provider.md' - Provider Network: 'models/circuits/providernetwork.md' + - Core: + - DataFile: 'models/core/datafile.md' + - DataSource: 'models/core/datasource.md' - DCIM: - Cable: 'models/dcim/cable.md' - ConsolePort: 'models/dcim/consoleport.md' @@ -202,6 +207,7 @@ nav: - CustomLink: 'models/extras/customlink.md' - ExportTemplate: 'models/extras/exporttemplate.md' - ImageAttachment: 'models/extras/imageattachment.md' + - JobResult: 'models/extras/jobresult.md' - JournalEntry: 'models/extras/journalentry.md' - SavedFilter: 'models/extras/savedfilter.md' - StagedChange: 'models/extras/stagedchange.md' From 927371b908f1bba34cc08d7d6c24f734fce83837 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 24 Feb 2023 13:54:39 -0500 Subject: [PATCH 041/174] Adjust inspector to accommodate non-detail views --- netbox/utilities/custom_inspectors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/utilities/custom_inspectors.py b/netbox/utilities/custom_inspectors.py index 25358535d..7cf9fe02f 100644 --- a/netbox/utilities/custom_inspectors.py +++ b/netbox/utilities/custom_inspectors.py @@ -18,7 +18,7 @@ class NetBoxSwaggerAutoSchema(SwaggerAutoSchema): if not operation_id: # Overwrite the action for bulk update/bulk delete views to ensure they get an operation ID that's # unique from their single-object counterparts (see #3436) - if operation_keys[-1] in ('delete', 'partial_update', 'update') and not self.view.detail: + if operation_keys[-1] in ('delete', 'partial_update', 'update') and not getattr(self.view, 'detail', None): operation_keys[-1] = f'bulk_{operation_keys[-1]}' operation_id = '_'.join(operation_keys) From 36771e821c38039afa10d645fdb116906107af1a Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Fri, 24 Feb 2023 12:38:50 -0800 Subject: [PATCH 042/174] 10520 remove Napalm code references (#11768) * 10520 remove all Napalm code references * 10520 remove lldp * 10520 remove config, status - rebuild js * 10520 re-add config parameters * 10520 re-add serializer * 10520 update docs --- docs/configuration/napalm.md | 2 + docs/features/api-integration.md | 2 + docs/installation/3-netbox.md | 8 - docs/integrations/napalm.md | 73 +--- netbox/core/forms/model_forms.py | 1 - netbox/dcim/api/views.py | 118 ------ netbox/dcim/filtersets.py | 2 +- netbox/dcim/forms/bulk_edit.py | 8 +- netbox/dcim/forms/bulk_import.py | 2 +- netbox/dcim/forms/model_forms.py | 8 +- netbox/dcim/search.py | 1 - netbox/dcim/tables/devices.py | 6 +- netbox/dcim/tests/test_filtersets.py | 10 +- netbox/dcim/tests/test_views.py | 3 - netbox/dcim/views.py | 65 --- netbox/extras/admin.py | 4 - netbox/project-static/bundle.js | 3 - netbox/project-static/dist/netbox.js.map | Bin 352299 -> 352299 bytes netbox/project-static/src/device/config.ts | 50 --- netbox/project-static/src/device/lldp.ts | 143 ------- netbox/project-static/src/device/status.ts | 379 ------------------ netbox/project-static/src/util.ts | 10 - netbox/templates/dcim/device/config.html | 45 --- .../templates/dcim/device/lldp_neighbors.html | 66 --- netbox/templates/dcim/device/status.html | 93 ----- netbox/templates/dcim/platform.html | 10 - 26 files changed, 17 insertions(+), 1095 deletions(-) delete mode 100644 netbox/project-static/src/device/config.ts delete mode 100644 netbox/project-static/src/device/lldp.ts delete mode 100644 netbox/project-static/src/device/status.ts delete mode 100644 netbox/templates/dcim/device/config.html delete mode 100644 netbox/templates/dcim/device/lldp_neighbors.html delete mode 100644 netbox/templates/dcim/device/status.html diff --git a/docs/configuration/napalm.md b/docs/configuration/napalm.md index 253bea297..e9fc91b72 100644 --- a/docs/configuration/napalm.md +++ b/docs/configuration/napalm.md @@ -1,5 +1,7 @@ # NAPALM Parameters +!!! **Note:** As of NetBox v3.5, NAPALM integration has been moved to a plugin and these configuration parameters are now deprecated. + ## NAPALM_USERNAME ## NAPALM_PASSWORD diff --git a/docs/features/api-integration.md b/docs/features/api-integration.md index 50c31ec4f..c3b78de47 100644 --- a/docs/features/api-integration.md +++ b/docs/features/api-integration.md @@ -36,6 +36,8 @@ To learn more about this feature, check out the [webhooks documentation](../inte To learn more about this feature, check out the [NAPALM documentation](../integrations/napalm.md). +As of NetBox v3.5, NAPALM integration has been moved to a plugin. Please see the [netbox_napalm_plugin](https://github.com/netbox-community/netbox-napalm) for installation instructions. + ## Prometheus Metrics NetBox includes a special `/metrics` view which exposes metrics for a [Prometheus](https://prometheus.io/) scraper, powered by the open source [django-prometheus](https://github.com/korfuri/django-prometheus) library. To learn more about this feature, check out the [Prometheus metrics documentation](../integrations/prometheus-metrics.md). diff --git a/docs/installation/3-netbox.md b/docs/installation/3-netbox.md index 26a2bf917..dc6c38977 100644 --- a/docs/installation/3-netbox.md +++ b/docs/installation/3-netbox.md @@ -199,14 +199,6 @@ When you have finished modifying the configuration, remember to save the file. All Python packages required by NetBox are listed in `requirements.txt` and will be installed automatically. NetBox also supports some optional packages. If desired, these packages must be listed in `local_requirements.txt` within the NetBox root directory. -### NAPALM - -Integration with the [NAPALM automation](../integrations/napalm.md) library allows NetBox to fetch live data from devices and return it to a requester via its REST API. The `NAPALM_USERNAME` and `NAPALM_PASSWORD` configuration parameters define the credentials to be used when connecting to a device. - -```no-highlight -sudo sh -c "echo 'napalm' >> /opt/netbox/local_requirements.txt" -``` - ### Remote File Storage By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/system.md#storage_backend) in `configuration.py`. diff --git a/docs/integrations/napalm.md b/docs/integrations/napalm.md index 60d8014e2..e7e0f108c 100644 --- a/docs/integrations/napalm.md +++ b/docs/integrations/napalm.md @@ -1,74 +1,3 @@ # NAPALM -NetBox supports integration with the [NAPALM automation](https://github.com/napalm-automation/napalm) library. NAPALM allows NetBox to serve a proxy for operational data, fetching live data from network devices and returning it to a requester via its REST API. Note that NetBox does not store any NAPALM data locally. - -The NetBox UI will display tabs for status, LLDP neighbors, and configuration under the device view if the following conditions are met: - -* Device status is "Active" -* A primary IP has been assigned to the device -* A platform with a NAPALM driver has been assigned -* The authenticated user has the `dcim.napalm_read_device` permission - -!!! note - To enable this integration, the NAPALM library must be installed. See [installation steps](../../installation/3-netbox/#napalm) for more information. - -Below is an example REST API request and response: - -```no-highlight -GET /api/dcim/devices/1/napalm/?method=get_environment - -{ - "get_environment": { - ... - } -} -``` - -!!! note - To make NAPALM requests via the NetBox REST API, a NetBox user must have assigned a permission granting the `napalm_read` action for the device object type. - -## Authentication - -By default, the [`NAPALM_USERNAME`](../configuration/napalm.md#napalm_username) and [`NAPALM_PASSWORD`](../configuration/napalm.md#napalm_password) configuration parameters are used for NAPALM authentication. They can be overridden for an individual API call by specifying the `X-NAPALM-Username` and `X-NAPALM-Password` headers. - -``` -$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \ --H "Authorization: Token $TOKEN" \ --H "Content-Type: application/json" \ --H "Accept: application/json; indent=4" \ --H "X-NAPALM-Username: foo" \ --H "X-NAPALM-Password: bar" -``` - -## Method Support - -The list of supported NAPALM methods depends on the [NAPALM driver](https://napalm.readthedocs.io/en/latest/support/index.html#general-support-matrix) configured for the platform of a device. Because there is no granular mechanism in place for limiting potentially disruptive requests, NetBox supports only read-only [get](https://napalm.readthedocs.io/en/latest/support/index.html#getters-support-matrix) methods. - -## Multiple Methods - -It is possible to request the output of multiple NAPALM methods in a single API request by passing multiple `method` parameters. For example: - -```no-highlight -GET /api/dcim/devices/1/napalm/?method=get_ntp_servers&method=get_ntp_peers - -{ - "get_ntp_servers": { - ... - }, - "get_ntp_peers": { - ... - } -} -``` - -## Optional Arguments - -The behavior of NAPALM drivers can be adjusted according to the [optional arguments](https://napalm.readthedocs.io/en/latest/support/index.html#optional-arguments). NetBox exposes those arguments using headers prefixed with `X-NAPALM-`. For example, the SSH port is changed to 2222 in this API call: - -``` -$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \ --H "Authorization: Token $TOKEN" \ --H "Content-Type: application/json" \ --H "Accept: application/json; indent=4" \ --H "X-NAPALM-port: 2222" -``` +As of NetBox v3.5, NAPALM integration has been moved to a plugin. Please see the [netbox_napalm_plugin](https://github.com/netbox-community/netbox-napalm) for installation instructions. **Note:** All previously entered NAPALM configuration data will be saved and automatically imported by the new plugin. diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py index 464c3eb47..a3a478be5 100644 --- a/netbox/core/forms/model_forms.py +++ b/netbox/core/forms/model_forms.py @@ -51,7 +51,6 @@ class DataSourceForm(NetBoxModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Determine the selected backend type backend_type = get_field_value(self, 'type') backend = registry['data_backends'].get(backend_type) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 21b05fece..cd911f3cb 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -419,124 +419,6 @@ class DeviceViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet): return serializers.DeviceWithConfigContextSerializer - @swagger_auto_schema( - manual_parameters=[ - Parameter( - name='method', - in_='query', - required=True, - type=openapi.TYPE_STRING - ) - ], - responses={'200': serializers.DeviceNAPALMSerializer} - ) - @action(detail=True, url_path='napalm') - def napalm(self, request, pk): - """ - Execute a NAPALM method on a Device - """ - device = get_object_or_404(self.queryset, pk=pk) - if not device.primary_ip: - raise ServiceUnavailable("This device does not have a primary IP address configured.") - if device.platform is None: - raise ServiceUnavailable("No platform is configured for this device.") - if not device.platform.napalm_driver: - raise ServiceUnavailable(f"No NAPALM driver is configured for this device's platform: {device.platform}.") - - # Check for primary IP address from NetBox object - if device.primary_ip: - host = str(device.primary_ip.address.ip) - else: - # Raise exception for no IP address and no Name if device.name does not exist - if not device.name: - raise ServiceUnavailable( - "This device does not have a primary IP address or device name to lookup configured." - ) - try: - # Attempt to complete a DNS name resolution if no primary_ip is set - host = socket.gethostbyname(device.name) - except socket.gaierror: - # Name lookup failure - raise ServiceUnavailable( - f"Name lookup failure, unable to resolve IP address for {device.name}. Please set Primary IP or " - f"setup name resolution.") - - # Check that NAPALM is installed - try: - import napalm - from napalm.base.exceptions import ModuleImportError - except ModuleNotFoundError as e: - if getattr(e, 'name') == 'napalm': - raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.") - raise e - - # Validate the configured driver - try: - driver = napalm.get_network_driver(device.platform.napalm_driver) - except ModuleImportError: - raise ServiceUnavailable("NAPALM driver for platform {} not found: {}.".format( - device.platform, device.platform.napalm_driver - )) - - # Verify user permission - if not request.user.has_perm('dcim.napalm_read_device'): - return HttpResponseForbidden() - - napalm_methods = request.GET.getlist('method') - response = {m: None for m in napalm_methods} - - config = get_config() - username = config.NAPALM_USERNAME - password = config.NAPALM_PASSWORD - timeout = config.NAPALM_TIMEOUT - optional_args = config.NAPALM_ARGS.copy() - if device.platform.napalm_args is not None: - optional_args.update(device.platform.napalm_args) - - # Update NAPALM parameters according to the request headers - for header in request.headers: - if header[:9].lower() != 'x-napalm-': - continue - - key = header[9:] - if key.lower() == 'username': - username = request.headers[header] - elif key.lower() == 'password': - password = request.headers[header] - elif key: - optional_args[key.lower()] = request.headers[header] - - # Connect to the device - d = driver( - hostname=host, - username=username, - password=password, - timeout=timeout, - optional_args=optional_args - ) - try: - d.open() - except Exception as e: - raise ServiceUnavailable("Error connecting to the device at {}: {}".format(host, e)) - - # Validate and execute each specified NAPALM method - for method in napalm_methods: - if not hasattr(driver, method): - response[method] = {'error': 'Unknown NAPALM method'} - continue - if not method.startswith('get_'): - response[method] = {'error': 'Only get_* NAPALM methods are supported'} - continue - try: - response[method] = getattr(d, method)() - except NotImplementedError: - response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)} - except Exception as e: - response[method] = {'error': 'Method {} failed: {}'.format(method, e)} - d.close() - - return Response(response) - class VirtualDeviceContextViewSet(NetBoxModelViewSet): queryset = VirtualDeviceContext.objects.prefetch_related( diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index fd3f9425e..7a27ef110 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -806,7 +806,7 @@ class PlatformFilterSet(OrganizationalModelFilterSet): class Meta: model = Platform - fields = ['id', 'name', 'slug', 'napalm_driver', 'description'] + fields = ['id', 'name', 'slug', 'description'] class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet): diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index ea7ab65cd..5b605dbb4 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -476,10 +476,6 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm): queryset=Manufacturer.objects.all(), required=False ) - napalm_driver = forms.CharField( - max_length=50, - required=False - ) config_template = DynamicModelChoiceField( queryset=ConfigTemplate.objects.all(), required=False @@ -491,9 +487,9 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm): model = Platform fieldsets = ( - (None, ('manufacturer', 'config_template', 'napalm_driver', 'description')), + (None, ('manufacturer', 'config_template', 'description')), ) - nullable_fields = ('manufacturer', 'config_template', 'napalm_driver', 'description') + nullable_fields = ('manufacturer', 'config_template', 'description') class DeviceBulkEditForm(NetBoxModelBulkEditForm): diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index e495ec34d..a72bdbce9 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -342,7 +342,7 @@ class PlatformImportForm(NetBoxModelImportForm): class Meta: model = Platform fields = ( - 'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags', + 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', ) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 34f91bbe8..74e697dde 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -451,19 +451,15 @@ class PlatformForm(NetBoxModelForm): fieldsets = ( ('Platform', ( - 'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags', - + 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', )), ) class Meta: model = Platform fields = [ - 'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags', + 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', ] - widgets = { - 'napalm_args': forms.Textarea(), - } class DeviceForm(TenancyForm, NetBoxModelForm): diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py index bae4f030f..f70c729f4 100644 --- a/netbox/dcim/search.py +++ b/netbox/dcim/search.py @@ -172,7 +172,6 @@ class PlatformIndex(SearchIndex): fields = ( ('name', 100), ('slug', 110), - ('napalm_driver', 300), ('description', 500), ) diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index f68960965..bed32251c 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -133,11 +133,11 @@ class PlatformTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = models.Platform fields = ( - 'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'config_template', 'napalm_driver', - 'napalm_args', 'description', 'tags', 'actions', 'created', 'last_updated', + 'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'config_template', 'description', + 'tags', 'actions', 'created', 'last_updated', ) default_columns = ( - 'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', + 'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'description', ) diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index c78b592d3..01ef4a87b 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -1469,9 +1469,9 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests): Manufacturer.objects.bulk_create(manufacturers) platforms = ( - Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], napalm_driver='driver-1', description='A'), - Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], napalm_driver='driver-2', description='B'), - Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], napalm_driver='driver-3', description='C'), + Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], description='A'), + Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], description='B'), + Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], description='C'), ) Platform.objects.bulk_create(platforms) @@ -1487,10 +1487,6 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'description': ['A', 'B']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_napalm_driver(self): - params = {'napalm_driver': ['driver-1', 'driver-2']} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_manufacturer(self): manufacturers = Manufacturer.objects.all()[:2] params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]} diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index bba91412d..eef78c6c6 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1591,8 +1591,6 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase): 'name': 'Platform X', 'slug': 'platform-x', 'manufacturer': manufacturer.pk, - 'napalm_driver': 'junos', - 'napalm_args': None, 'description': 'A new platform', 'tags': [t.pk for t in tags], } @@ -1612,7 +1610,6 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase): ) cls.bulk_edit_data = { - 'napalm_driver': 'ios', 'description': 'New description', } diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 62359553d..38eb302a8 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2080,71 +2080,6 @@ class DeviceBulkRenameView(generic.BulkRenameView): table = tables.DeviceTable -# -# Device NAPALM views -# - -class NAPALMViewTab(ViewTab): - - def render(self, instance): - # Display NAPALM tabs only for devices which meet certain requirements - if not ( - instance.status == 'active' and - instance.primary_ip and - instance.platform and - instance.platform.napalm_driver - ): - return None - return super().render(instance) - - -@register_model_view(Device, 'status') -class DeviceStatusView(generic.ObjectView): - additional_permissions = ['dcim.napalm_read_device'] - queryset = Device.objects.all() - template_name = 'dcim/device/status.html' - tab = NAPALMViewTab( - label=_('Status'), - permission='dcim.napalm_read_device', - weight=3000 - ) - - -@register_model_view(Device, 'lldp_neighbors', path='lldp-neighbors') -class DeviceLLDPNeighborsView(generic.ObjectView): - additional_permissions = ['dcim.napalm_read_device'] - queryset = Device.objects.all() - template_name = 'dcim/device/lldp_neighbors.html' - tab = NAPALMViewTab( - label=_('LLDP Neighbors'), - permission='dcim.napalm_read_device', - weight=3100 - ) - - def get_extra_context(self, request, instance): - interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related( - '_path' - ).exclude( - type__in=NONCONNECTABLE_IFACE_TYPES - ) - - return { - 'interfaces': interfaces, - } - - -@register_model_view(Device, 'config') -class DeviceConfigView(generic.ObjectView): - additional_permissions = ['dcim.napalm_read_device'] - queryset = Device.objects.all() - template_name = 'dcim/device/config.html' - tab = NAPALMViewTab( - label=_('Config'), - permission='dcim.napalm_read_device', - weight=3200 - ) - - # # Modules # diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index 837a8f2d3..18cc860b1 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -35,10 +35,6 @@ class ConfigRevisionAdmin(admin.ModelAdmin): 'fields': ('CUSTOM_VALIDATORS',), 'classes': ('monospace',), }), - ('NAPALM', { - 'fields': ('NAPALM_USERNAME', 'NAPALM_PASSWORD', 'NAPALM_TIMEOUT', 'NAPALM_ARGS'), - 'classes': ('monospace',), - }), ('User Preferences', { 'fields': ('DEFAULT_USER_PREFERENCES',), }), diff --git a/netbox/project-static/bundle.js b/netbox/project-static/bundle.js index 76a1581ad..6f651cd05 100644 --- a/netbox/project-static/bundle.js +++ b/netbox/project-static/bundle.js @@ -40,9 +40,6 @@ async function bundleGraphIQL() { async function bundleNetBox() { const entryPoints = { netbox: 'src/index.ts', - lldp: 'src/device/lldp.ts', - config: 'src/device/config.ts', - status: 'src/device/status.ts', }; try { const result = await esbuild.build({ diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 384195df5b8612fa825638669dd0d3f8e996b070..5481e38a3b30eb0be9e63461ad2611c122949b3b 100644 GIT binary patch delta 35 rcmZ2|Ky>v1(S{br7N#xC{970^w+n1xHezDTYIokoyxn;l%Zrr&@2w40 delta 35 rcmZ2|Ky>v1(S{br7N#xC{971}whL@wHezBlZg<|syxn;l%Zrr&={pSP diff --git a/netbox/project-static/src/device/config.ts b/netbox/project-static/src/device/config.ts deleted file mode 100644 index c9c19e8d3..000000000 --- a/netbox/project-static/src/device/config.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createToast } from '../bs'; -import { apiGetBase, getNetboxData, hasError, toggleLoader } from '../util'; - -/** - * Initialize device config elements. - */ -function initConfig(): void { - toggleLoader('show'); - const url = getNetboxData('data-object-url'); - - if (url !== null) { - apiGetBase(url) - .then(data => { - if (hasError(data)) { - createToast('danger', 'Error Fetching Device Config', data.error).show(); - console.error(data.error); - return; - } else if (hasError>(data.get_config)) { - createToast('danger', 'Error Fetching Device Config', data.get_config.error).show(); - console.error(data.get_config.error); - return; - } else { - const configTypes = ['running', 'startup', 'candidate'] as DeviceConfigType[]; - - for (const configType of configTypes) { - const element = document.getElementById(`${configType}_config`); - if (element !== null) { - const config = data.get_config[configType]; - if (typeof config === 'string') { - // If the returned config is a string, set the element innerHTML as-is. - element.innerHTML = config; - } else { - // If the returned config is an object (dict), convert it to JSON. - element.innerHTML = JSON.stringify(data.get_config[configType], null, 2); - } - } - } - } - }) - .finally(() => { - toggleLoader('hide'); - }); - } -} - -if (document.readyState !== 'loading') { - initConfig(); -} else { - document.addEventListener('DOMContentLoaded', initConfig); -} diff --git a/netbox/project-static/src/device/lldp.ts b/netbox/project-static/src/device/lldp.ts deleted file mode 100644 index ebf71138c..000000000 --- a/netbox/project-static/src/device/lldp.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { createToast } from '../bs'; -import { getNetboxData, apiGetBase, hasError, isTruthy, toggleLoader } from '../util'; - -// Match an interface name that begins with a capital letter and is followed by at least one other -// alphabetic character, and ends with a forward-slash-separated numeric sequence such as 0/1/2. -const CISCO_IOS_PATTERN = new RegExp(/^([A-Z][A-Za-z]+)[^0-9]*([0-9/]+)$/); - -// Mapping of overrides to default Cisco IOS interface alias behavior (default behavior is to use -// the first two characters). -const CISCO_IOS_OVERRIDES = new Map([ - // Cisco IOS abbreviates 25G (TwentyFiveGigE) interfaces as 'Twe'. - ['TwentyFiveGigE', 'Twe'], -]); - -/** - * Get an attribute from a row's cell. - * - * @param row Interface row - * @param query CSS media query - * @param attr Cell attribute - */ -function getData(row: HTMLTableRowElement, query: string, attr: string): string | null { - return row.querySelector(query)?.getAttribute(attr) ?? null; -} - -/** - * Get preconfigured alias for given interface. Primarily for matching long-form Cisco IOS - * interface names with short-form Cisco IOS interface names. For example, `GigabitEthernet0/1/2` - * would become `Gi0/1/2`. - * - * This should probably be replaced with something in the primary application (Django), such as - * a database field attached to given interface types. However, this is a temporary measure to - * replace the functionality of this one-liner: - * - * @see https://github.com/netbox-community/netbox/blob/9cc4992fad2fe04ef0211d998c517414e8871d8c/netbox/templates/dcim/device/lldp_neighbors.html#L69 - * - * @param name Long-form/original interface name. - */ -function getInterfaceAlias(name: string | null): string | null { - if (name === null) { - return name; - } - if (name.match(CISCO_IOS_PATTERN)) { - // Extract the base name and numeric portions of the interface. For example, an input interface - // of `GigabitEthernet0/0/1` would result in an array of `['GigabitEthernet', '0/0/1']`. - const [base, numeric] = (name.match(CISCO_IOS_PATTERN) ?? []).slice(1, 3); - - if (isTruthy(base) && isTruthy(numeric)) { - // Check the override map and use its value if the base name is present in the map. - // Otherwise, use the first two characters of the base name. For example, - // `GigabitEthernet0/0/1` would become `Gi0/0/1`, but `TwentyFiveGigE0/0/1` would become - // `Twe0/0/1`. - const aliasBase = CISCO_IOS_OVERRIDES.get(base) || base.slice(0, 2); - return `${aliasBase}${numeric}`; - } - } - return name; -} - -/** - * Update row styles based on LLDP neighbor data. - */ -function updateRowStyle(data: LLDPNeighborDetail) { - for (const [fullIface, neighbors] of Object.entries(data.get_lldp_neighbors_detail)) { - const [iface] = fullIface.split('.'); - - const row = document.getElementById(iface) as Nullable; - - if (row !== null) { - for (const neighbor of neighbors) { - const deviceCell = row.querySelector('td.device'); - const interfaceCell = row.querySelector('td.interface'); - const configuredDevice = getData(row, 'td.configured_device', 'data'); - const configuredChassis = getData(row, 'td.configured_chassis', 'data-chassis'); - const configuredIface = getData(row, 'td.configured_interface', 'data'); - - const interfaceAlias = getInterfaceAlias(configuredIface); - - const remoteName = neighbor.remote_system_name ?? ''; - const remotePort = neighbor.remote_port ?? ''; - const [neighborDevice] = remoteName.split('.'); - const [neighborIface] = remotePort.split('.'); - - if (deviceCell !== null) { - deviceCell.innerText = neighborDevice; - } - - if (interfaceCell !== null) { - interfaceCell.innerText = neighborIface; - } - - // Interface has an LLDP neighbor, but the neighbor is not configured in NetBox. - const nonConfiguredDevice = !isTruthy(configuredDevice) && isTruthy(neighborDevice); - - // NetBox device or chassis matches LLDP neighbor. - const validNode = - configuredDevice === neighborDevice || configuredChassis === neighborDevice; - - // NetBox configured interface matches LLDP neighbor interface. - const validInterface = - configuredIface === neighborIface || interfaceAlias === neighborIface; - - if (nonConfiguredDevice) { - row.classList.add('info'); - } else if (validNode && validInterface) { - row.classList.add('success'); - } else { - row.classList.add('danger'); - } - } - } - } -} - -/** - * Initialize LLDP Neighbor fetching. - */ -function initLldpNeighbors() { - toggleLoader('show'); - const url = getNetboxData('object-url'); - if (url !== null) { - apiGetBase(url) - .then(data => { - if (hasError(data)) { - createToast('danger', 'Error Retrieving LLDP Neighbor Information', data.error).show(); - toggleLoader('hide'); - return; - } else { - updateRowStyle(data); - } - return; - }) - .finally(() => { - toggleLoader('hide'); - }); - } -} - -if (document.readyState !== 'loading') { - initLldpNeighbors(); -} else { - document.addEventListener('DOMContentLoaded', initLldpNeighbors); -} diff --git a/netbox/project-static/src/device/status.ts b/netbox/project-static/src/device/status.ts deleted file mode 100644 index 8261ebc82..000000000 --- a/netbox/project-static/src/device/status.ts +++ /dev/null @@ -1,379 +0,0 @@ -import dayjs from 'dayjs'; -import utc from 'dayjs/plugin/utc'; -import timezone from 'dayjs/plugin/timezone'; -import duration from 'dayjs/plugin/duration'; -import advancedFormat from 'dayjs/plugin/advancedFormat'; - -import { createToast } from '../bs'; -import { apiGetBase, getNetboxData, hasError, toggleLoader, createElement, cToF } from '../util'; - -type Uptime = { - utc: string; - zoned: string | null; - duration: string; -}; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(advancedFormat); -dayjs.extend(duration); - -const factKeys = [ - 'hostname', - 'fqdn', - 'vendor', - 'model', - 'serial_number', - 'os_version', -] as (keyof DeviceFacts)[]; - -type DurationKeys = 'years' | 'months' | 'days' | 'hours' | 'minutes' | 'seconds'; -const formatKeys = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'] as DurationKeys[]; - -/** - * From a number of seconds that have elapsed since reboot, extract human-readable dates in the - * following formats: - * - Relative time since reboot (e.g. 1 month, 28 days, 1 hour, 30 seconds). - * - Time stamp in browser-relative timezone. - * - Time stamp in UTC. - * @param seconds Seconds since reboot. - */ -function getUptime(seconds: number): Uptime { - const relDate = new Date(); - - // Get the user's UTC offset, to determine if the user is in UTC or not. - const offset = relDate.getTimezoneOffset(); - const relNow = dayjs(relDate); - - // Get a dayjs object for the device reboot time (now - number of seconds). - const relThen = relNow.subtract(seconds, 'seconds'); - - // Get a human-readable version of the time in UTC. - const utc = relThen.tz('Etc/UTC').format('YYYY-MM-DD HH:MM:ss z'); - - // We only want to show the UTC time if the user is not already in UTC time. - let zoned = null; - if (offset !== 0) { - // If the user is not in UTC time, return a human-readable version in the user's timezone. - zoned = relThen.format('YYYY-MM-DD HH:MM:ss z'); - } - // Get a dayjs duration object to create a human-readable relative time string. - const between = dayjs.duration(seconds, 'seconds'); - - // Array of all non-zero-value duration properties. For example, if duration.year() is 0, we - // don't care about it and shouldn't show it to the user. - let parts = [] as string[]; - for (const key of formatKeys) { - // Get the property value. For example, duration.year(), duration.month(), etc. - const value = between[key](); - if (value === 1) { - // If the duration for this key is 1, drop the trailing 's'. For example, '1 seconds' would - // become '1 second'. - const label = key.replace(/s$/, ''); - parts = [...parts, `${value} ${label}`]; - } else if (value > 1) { - // If the duration for this key is more than one, add it to the array as-is. - parts = [...parts, `${value} ${key}`]; - } - } - // Set the duration to something safe, so we don't show 'undefined' or an empty string to the user. - let duration = 'None'; - if (parts.length > 0) { - // If the array actually has elements, reassign the duration to a human-readable version. - duration = parts.join(', '); - } - - return { utc, zoned, duration }; -} - -/** - * After the `get_facts` result is received, parse its content and update HTML elements - * accordingly. - * - * @param facts NAPALM Device Facts - */ -function processFacts(facts: DeviceFacts): void { - for (const key of factKeys) { - if (key in facts) { - // Find the target element which should have its innerHTML/innerText set to a NAPALM value. - const element = document.getElementById(key); - if (element !== null) { - element.innerHTML = String(facts[key]); - } - } - } - const { uptime } = facts; - const { utc, zoned, duration } = getUptime(uptime); - - // Find the duration (relative time) element and set its value. - const uptimeDurationElement = document.getElementById('uptime-duration'); - if (uptimeDurationElement !== null) { - uptimeDurationElement.innerHTML = duration; - } - // Find the time stamp element and set its value. - const uptimeElement = document.getElementById('uptime'); - if (uptimeElement !== null) { - if (zoned === null) { - // If the user is in UTC time, only add the UTC time stamp. - uptimeElement.innerHTML = utc; - } else { - // Otherwise, add both time stamps. - uptimeElement.innerHTML = [zoned, `${utc}`].join(''); - } - } -} - -/** - * Insert a title row before the next table row. The title row describes each environment key/value - * pair from the NAPALM response. - * - * @param next Next adjacent element. For example, if this is the CPU data, `next` would be the - * memory row. - * @param title1 Column 1 Title - * @param title2 Column 2 Title - */ -function insertTitleRow(next: E, title1: string, title2: string): void { - // Create cell element that contains the key title. - const col1Title = createElement('th', { innerText: title1 }, ['border-end', 'text-end']); - // Create cell element that contains the value title. - const col2Title = createElement('th', { innerText: title2 }, ['border-start', 'text-start']); - // Create title row element with the two header cells as children. - const titleRow = createElement('tr', {}, [], [col1Title, col2Title]); - // Insert the entire row just before the beginning of the next row (i.e., at the end of this row). - next.insertAdjacentElement('beforebegin', titleRow); -} - -/** - * Insert a "No Data" row, for when the NAPALM response doesn't contain this type of data. - * - * @param next Next adjacent element.For example, if this is the CPU data, `next` would be the - * memory row. - */ -function insertNoneRow>(next: E): void { - const none = createElement('td', { colSpan: '2', innerText: 'No Data' }, [ - 'text-muted', - 'text-center', - ]); - const titleRow = createElement('tr', {}, [], [none]); - if (next !== null) { - next.insertAdjacentElement('beforebegin', titleRow); - } -} - -function getNext(id: string): Nullable { - const element = document.getElementById(id); - if (element !== null) { - return element.nextElementSibling as Nullable; - } - return null; -} - -/** - * Create & insert table rows for each CPU in the NAPALM response. - * - * @param cpu NAPALM CPU data. - */ -function processCpu(cpu: DeviceEnvironment['cpu']): void { - // Find the next adjacent element, so we can insert elements before it. - const next = getNext('status-cpu'); - if (typeof cpu !== 'undefined') { - if (next !== null) { - insertTitleRow(next, 'Name', 'Usage'); - for (const [core, data] of Object.entries(cpu)) { - const usage = data['%usage']; - const kCell = createElement('td', { innerText: core }, ['border-end', 'text-end']); - const vCell = createElement('td', { innerText: `${usage} %` }, [ - 'border-start', - 'text-start', - ]); - const row = createElement('tr', {}, [], [kCell, vCell]); - next.insertAdjacentElement('beforebegin', row); - } - } - } else { - insertNoneRow(next); - } -} - -/** - * Create & insert table rows for the memory in the NAPALM response. - * - * @param mem NAPALM memory data. - */ -function processMemory(mem: DeviceEnvironment['memory']): void { - // Find the next adjacent element, so we can insert elements before it. - const next = getNext('status-memory'); - if (typeof mem !== 'undefined') { - if (next !== null) { - insertTitleRow(next, 'Available', 'Used'); - const { available_ram: avail, used_ram: used } = mem; - const aCell = createElement('td', { innerText: avail }, ['border-end', 'text-end']); - const uCell = createElement('td', { innerText: used }, ['border-start', 'text-start']); - const row = createElement('tr', {}, [], [aCell, uCell]); - next.insertAdjacentElement('beforebegin', row); - } - } else { - insertNoneRow(next); - } -} - -/** - * Create & insert table rows for each temperature sensor in the NAPALM response. - * - * @param temp NAPALM temperature data. - */ -function processTemp(temp: DeviceEnvironment['temperature']): void { - // Find the next adjacent element, so we can insert elements before it. - const next = getNext('status-temperature'); - if (typeof temp !== 'undefined') { - if (next !== null) { - insertTitleRow(next, 'Sensor', 'Value'); - for (const [sensor, data] of Object.entries(temp)) { - const tempC = data.temperature; - const tempF = cToF(tempC); - const innerHTML = `${tempC} °C ${tempF} °F`; - const status = data.is_alert ? 'warning' : data.is_critical ? 'danger' : 'success'; - const kCell = createElement('td', { innerText: sensor }, ['border-end', 'text-end']); - const vCell = createElement('td', { innerHTML }, ['border-start', 'text-start']); - const row = createElement('tr', {}, [`table-${status}`], [kCell, vCell]); - next.insertAdjacentElement('beforebegin', row); - } - } - } else { - insertNoneRow(next); - } -} - -/** - * Create & insert table rows for each fan in the NAPALM response. - * - * @param fans NAPALM fan data. - */ -function processFans(fans: DeviceEnvironment['fans']): void { - // Find the next adjacent element, so we can insert elements before it. - const next = getNext('status-fans'); - if (typeof fans !== 'undefined') { - if (next !== null) { - insertTitleRow(next, 'Fan', 'Status'); - for (const [fan, data] of Object.entries(fans)) { - const { status } = data; - const goodIcon = createElement('i', {}, ['mdi', 'mdi-check-bold', 'text-success']); - const badIcon = createElement('i', {}, ['mdi', 'mdi-close', 'text-warning']); - const kCell = createElement('td', { innerText: fan }, ['border-end', 'text-end']); - const vCell = createElement( - 'td', - {}, - ['border-start', 'text-start'], - [status ? goodIcon : badIcon], - ); - const row = createElement( - 'tr', - {}, - [`table-${status ? 'success' : 'warning'}`], - [kCell, vCell], - ); - next.insertAdjacentElement('beforebegin', row); - } - } - } else { - insertNoneRow(next); - } -} - -/** - * Create & insert table rows for each PSU in the NAPALM response. - * - * @param power NAPALM power data. - */ -function processPower(power: DeviceEnvironment['power']): void { - // Find the next adjacent element, so we can insert elements before it. - const next = getNext('status-power'); - if (typeof power !== 'undefined') { - if (next !== null) { - insertTitleRow(next, 'PSU', 'Status'); - for (const [psu, data] of Object.entries(power)) { - const { status } = data; - const goodIcon = createElement('i', {}, ['mdi', 'mdi-check-bold', 'text-success']); - const badIcon = createElement('i', {}, ['mdi', 'mdi-close', 'text-warning']); - const kCell = createElement('td', { innerText: psu }, ['border-end', 'text-end']); - const vCell = createElement( - 'td', - {}, - ['border-start', 'text-start'], - [status ? goodIcon : badIcon], - ); - const row = createElement( - 'tr', - {}, - [`table-${status ? 'success' : 'warning'}`], - [kCell, vCell], - ); - next.insertAdjacentElement('beforebegin', row); - } - } - } else { - insertNoneRow(next); - } -} - -/** - * After the `get_environment` result is received, parse its content and update HTML elements - * accordingly. - * - * @param env NAPALM Device Environment - */ -function processEnvironment(env: DeviceEnvironment): void { - const { cpu, memory, temperature, fans, power } = env; - processCpu(cpu); - processMemory(memory); - processTemp(temperature); - processFans(fans); - processPower(power); -} - -/** - * Initialize NAPALM device status handlers. - */ -function initStatus(): void { - // Show loading state for both Facts & Environment cards. - toggleLoader('show'); - - const url = getNetboxData('data-object-url'); - - if (url !== null) { - apiGetBase(url) - .then(data => { - if (hasError(data)) { - // If the API returns an error, show it to the user. - createToast('danger', 'Error Fetching Device Status', data.error).show(); - } else { - if (!hasError(data.get_facts)) { - processFacts(data.get_facts); - } else { - // If the device facts data contains an error, show it to the user. - createToast('danger', 'Error Fetching Device Facts', data.get_facts.error).show(); - } - if (!hasError(data.get_environment)) { - processEnvironment(data.get_environment); - } else { - // If the device environment data contains an error, show it to the user. - createToast( - 'danger', - 'Error Fetching Device Environment Data', - data.get_environment.error, - ).show(); - } - } - return; - }) - .finally(() => toggleLoader('hide')); - } else { - toggleLoader('hide'); - } -} - -if (document.readyState !== 'loading') { - initStatus(); -} else { - document.addEventListener('DOMContentLoaded', initStatus); -} diff --git a/netbox/project-static/src/util.ts b/netbox/project-static/src/util.ts index 9f6ff100d..e1ada2e19 100644 --- a/netbox/project-static/src/util.ts +++ b/netbox/project-static/src/util.ts @@ -397,16 +397,6 @@ export function createElement< return element as HTMLElementTagNameMap[T]; } -/** - * Convert Celsius to Fahrenheit, for NAPALM temperature sensors. - * - * @param celsius Degrees in Celsius. - * @returns Degrees in Fahrenheit. - */ -export function cToF(celsius: number): number { - return Math.round((celsius * (9 / 5) + 32 + Number.EPSILON) * 10) / 10; -} - /** * Deduplicate an array of objects based on the value of a property. * diff --git a/netbox/templates/dcim/device/config.html b/netbox/templates/dcim/device/config.html deleted file mode 100644 index f3609d3a4..000000000 --- a/netbox/templates/dcim/device/config.html +++ /dev/null @@ -1,45 +0,0 @@ -{% extends 'dcim/device/base.html' %} -{% load static %} - -{% block title %}{{ object }} - Config{% endblock %} - -{% block head %} - -{% endblock %} - -{% block content %} -
    -
    -
    -
    -
    - Loading... -
    -
    -
    Device Configuration
    -
    - -
    -
    -
    
    -                    
    -
    -
    
    -                    
    -
    -
    
    -                    
    -
    -
    -
    -
    -
    -{% endblock %} - -{% block data %} - -{% endblock %} diff --git a/netbox/templates/dcim/device/lldp_neighbors.html b/netbox/templates/dcim/device/lldp_neighbors.html deleted file mode 100644 index 2be6aba4d..000000000 --- a/netbox/templates/dcim/device/lldp_neighbors.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends 'dcim/device/base.html' %} -{% load static %} - -{% block title %}{{ object }} - LLDP Neighbors{% endblock %} - -{% block head %} - -{% endblock %} - -{% block content %} -
    -
    -
    - Loading... -
    -
    -
    -
    LLDP Neighbors
    -
    -
    - - - - - - - - - - - - {% for iface in interfaces %} - - - {% with peer=iface.connected_endpoints.0 %} - {% if peer.device %} - - - {% elif peer.circuit %} - {% with circuit=peer.circuit %} - - {% endwith %} - {% else %} - - {% endif %} - {% endwith %} - - - - {% endfor %} - -
    InterfaceConfigured DeviceConfigured InterfaceLLDP DeviceLLDP Interface
    {{ iface }} - {{ peer.device }} - - {{ peer }} - - - {{ circuit.provider }} {{ circuit }} - None
    -
    -
    -{% endblock %} - -{% block data %} - -{% endblock %} diff --git a/netbox/templates/dcim/device/status.html b/netbox/templates/dcim/device/status.html deleted file mode 100644 index 51dd7d27e..000000000 --- a/netbox/templates/dcim/device/status.html +++ /dev/null @@ -1,93 +0,0 @@ -{% extends 'dcim/device/base.html' %} -{% load static %} - -{% block title %}{{ object }} - Status{% endblock %} - -{% block head %} - -{% endblock %} - -{% block content %} -
    -
    -
    -
    -
    - Loading... -
    -
    -
    Device Facts
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Hostname
    FQDN
    Vendor
    Model
    Serial Number
    OS Version
    Uptime -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - Loading... -
    -
    -
    Environment
    -
    - - - - - - - - - - - - - - - - - - -
    CPU
    Memory
    Temperature
    Fans
    Power
    -
    -
    -
    -
    -{% endblock %} - -{% block data %} - -{% endblock %} diff --git a/netbox/templates/dcim/platform.html b/netbox/templates/dcim/platform.html index 5123699d4..a834ed7e9 100644 --- a/netbox/templates/dcim/platform.html +++ b/netbox/templates/dcim/platform.html @@ -43,20 +43,10 @@ Config Template {{ object.config_template|linkify|placeholder }} - - NAPALM Driver - {{ object.napalm_driver|placeholder }} - {% include 'inc/panels/tags.html' %} -
    -
    NAPALM Arguments
    -
    -
    {{ object.napalm_args|json }}
    -
    -
    {% plugin_left_page object %}
    From 084a2cc52c877e86f86b43d7016aa7a1b47ff79e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 24 Feb 2023 16:04:00 -0500 Subject: [PATCH 043/174] Closes #9416: Dashboard widgets (#11823) * Replace masonry with gridstack * Initial work on dashboard widgets * Implement function to save dashboard layout * Define a default dashboard * Clean up widgets * Implement widget configuration views & forms * Permit merging dict value with existing dict in user config * Add widget deletion view * Enable HTMX for widget configuration * Implement view to add dashboard widgets * ObjectCountsWidget: Identify models by app_label & name * Add color customization to dashboard widgets * Introduce Dashboard model to store user dashboard layout & config * Clean up utility functions * Remove hard-coded API URL * Use fixed grid cell height * Add modal close button * Clean up dashboard views * Rebuild JS --- netbox/extras/api/serializers.py | 11 ++ netbox/extras/api/urls.py | 7 +- netbox/extras/api/views.py | 13 ++ netbox/extras/apps.py | 2 +- netbox/extras/constants.py | 45 ++++++ netbox/extras/dashboard/__init__.py | 2 + netbox/extras/dashboard/forms.py | 38 +++++ netbox/extras/dashboard/utils.py | 76 ++++++++++ netbox/extras/dashboard/widgets.py | 119 ++++++++++++++++ netbox/extras/migrations/0087_dashboard.py | 25 ++++ netbox/extras/models/__init__.py | 2 + netbox/extras/models/dashboard.py | 70 ++++++++++ netbox/extras/templatetags/dashboard.py | 11 ++ netbox/extras/urls.py | 5 + netbox/extras/views.py | 130 +++++++++++++++++- netbox/netbox/registry.py | 1 + netbox/netbox/views/misc.py | 91 +----------- netbox/project-static/dist/config.js | Bin 107780 -> 84337 bytes netbox/project-static/dist/config.js.map | Bin 105762 -> 83629 bytes netbox/project-static/dist/lldp.js | Bin 108435 -> 84992 bytes netbox/project-static/dist/lldp.js.map | Bin 106474 -> 84341 bytes .../project-static/dist/netbox-external.css | Bin 340587 -> 349159 bytes netbox/project-static/dist/netbox.js | Bin 379494 -> 436660 bytes netbox/project-static/dist/netbox.js.map | Bin 352299 -> 400541 bytes netbox/project-static/dist/status.js | Bin 128487 -> 105522 bytes netbox/project-static/dist/status.js.map | Bin 128481 -> 106348 bytes netbox/project-static/package.json | 4 +- netbox/project-static/src/bs.ts | 14 -- netbox/project-static/src/dashboard.ts | 41 ++++++ netbox/project-static/src/netbox.ts | 2 + netbox/project-static/styles/_external.scss | 1 + netbox/project-static/yarn.lock | 44 +----- netbox/templates/extras/dashboard/widget.html | 37 +++++ .../extras/dashboard/widget_add.html | 27 ++++ .../extras/dashboard/widget_config.html | 20 +++ .../extras/dashboard/widgets/changelog.html | 4 + .../dashboard/widgets/objectcounts.html | 14 ++ netbox/templates/home.html | 108 +++++---------- netbox/templates/inc/htmx_modal.html | 2 +- netbox/users/models.py | 5 +- 40 files changed, 754 insertions(+), 217 deletions(-) create mode 100644 netbox/extras/dashboard/__init__.py create mode 100644 netbox/extras/dashboard/forms.py create mode 100644 netbox/extras/dashboard/utils.py create mode 100644 netbox/extras/dashboard/widgets.py create mode 100644 netbox/extras/migrations/0087_dashboard.py create mode 100644 netbox/extras/models/dashboard.py create mode 100644 netbox/extras/templatetags/dashboard.py create mode 100644 netbox/project-static/src/dashboard.ts create mode 100644 netbox/templates/extras/dashboard/widget.html create mode 100644 netbox/templates/extras/dashboard/widget_add.html create mode 100644 netbox/templates/extras/dashboard/widget_config.html create mode 100644 netbox/templates/extras/dashboard/widgets/changelog.html create mode 100644 netbox/templates/extras/dashboard/widgets/objectcounts.html diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 5e0a484f8..5764c66ee 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -34,6 +34,7 @@ __all__ = ( 'ContentTypeSerializer', 'CustomFieldSerializer', 'CustomLinkSerializer', + 'DashboardSerializer', 'ExportTemplateSerializer', 'ImageAttachmentSerializer', 'JobResultSerializer', @@ -563,3 +564,13 @@ class ContentTypeSerializer(BaseModelSerializer): class Meta: model = ContentType fields = ['id', 'url', 'display', 'app_label', 'model'] + + +# +# User dashboard +# + +class DashboardSerializer(serializers.ModelSerializer): + class Meta: + model = Dashboard + fields = ('layout', 'config') diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py index f01cdcd00..e796f0fdb 100644 --- a/netbox/extras/api/urls.py +++ b/netbox/extras/api/urls.py @@ -1,3 +1,5 @@ +from django.urls import include, path + from netbox.api.routers import NetBoxRouter from . import views @@ -22,4 +24,7 @@ router.register('job-results', views.JobResultViewSet) router.register('content-types', views.ContentTypeViewSet) app_name = 'extras-api' -urlpatterns = router.urls +urlpatterns = [ + path('dashboard/', views.DashboardView.as_view(), name='dashboard'), + path('', include(router.urls)), +] diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 75f0eb464..7665e949d 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -4,6 +4,7 @@ from django_rq.queues import get_connection from rest_framework import status from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied +from rest_framework.generics import RetrieveUpdateDestroyAPIView from rest_framework.permissions import IsAuthenticated from rest_framework.renderers import JSONRenderer from rest_framework.response import Response @@ -423,3 +424,15 @@ class ContentTypeViewSet(ReadOnlyModelViewSet): queryset = ContentType.objects.order_by('app_label', 'model') serializer_class = serializers.ContentTypeSerializer filterset_class = filtersets.ContentTypeFilterSet + + +# +# User dashboard +# + +class DashboardView(RetrieveUpdateDestroyAPIView): + queryset = Dashboard.objects.all() + serializer_class = serializers.DashboardSerializer + + def get_object(self): + return Dashboard.objects.filter(user=self.request.user).first() diff --git a/netbox/extras/apps.py b/netbox/extras/apps.py index 965eb033e..f23e62dd2 100644 --- a/netbox/extras/apps.py +++ b/netbox/extras/apps.py @@ -5,4 +5,4 @@ class ExtrasConfig(AppConfig): name = "extras" def ready(self): - from . import lookups, search, signals + from . import dashboard, lookups, search, signals diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index d65fb9612..12ff21b31 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -1,2 +1,47 @@ +from django.contrib.contenttypes.models import ContentType + # Webhook content types HTTP_CONTENT_TYPE_JSON = 'application/json' + +# Dashboard +DEFAULT_DASHBOARD = [ + { + 'widget': 'extras.ObjectCountsWidget', + 'width': 4, + 'height': 3, + 'title': 'IPAM', + 'config': { + 'models': [ + 'ipam.aggregate', + 'ipam.prefix', + 'ipam.ipaddress', + ] + } + }, + { + 'widget': 'extras.ObjectCountsWidget', + 'width': 4, + 'height': 3, + 'title': 'DCIM', + 'config': { + 'models': [ + 'dcim.site', + 'dcim.rack', + 'dcim.device', + ] + } + }, + { + 'widget': 'extras.NoteWidget', + 'width': 4, + 'height': 3, + 'config': { + 'content': 'Welcome to **NetBox**!' + } + }, + { + 'widget': 'extras.ChangeLogWidget', + 'width': 12, + 'height': 6, + }, +] diff --git a/netbox/extras/dashboard/__init__.py b/netbox/extras/dashboard/__init__.py new file mode 100644 index 000000000..2539f0cbe --- /dev/null +++ b/netbox/extras/dashboard/__init__.py @@ -0,0 +1,2 @@ +from .utils import * +from .widgets import * diff --git a/netbox/extras/dashboard/forms.py b/netbox/extras/dashboard/forms.py new file mode 100644 index 000000000..ba07be4b1 --- /dev/null +++ b/netbox/extras/dashboard/forms.py @@ -0,0 +1,38 @@ +from django import forms +from django.urls import reverse_lazy + +from netbox.registry import registry +from utilities.forms import BootstrapMixin, add_blank_choice +from utilities.choices import ButtonColorChoices + +__all__ = ( + 'DashboardWidgetAddForm', + 'DashboardWidgetForm', +) + + +def get_widget_choices(): + return registry['widgets'].items() + + +class DashboardWidgetForm(BootstrapMixin, forms.Form): + title = forms.CharField( + required=False + ) + color = forms.ChoiceField( + choices=add_blank_choice(ButtonColorChoices), + required=False, + ) + + +class DashboardWidgetAddForm(DashboardWidgetForm): + widget_class = forms.ChoiceField( + choices=get_widget_choices, + widget=forms.Select( + attrs={ + 'hx-get': reverse_lazy('extras:dashboardwidget_add'), + 'hx-target': '#widget_add_form', + } + ) + ) + field_order = ('widget_class', 'title', 'color') diff --git a/netbox/extras/dashboard/utils.py b/netbox/extras/dashboard/utils.py new file mode 100644 index 000000000..8281cc522 --- /dev/null +++ b/netbox/extras/dashboard/utils.py @@ -0,0 +1,76 @@ +import uuid + +from django.core.exceptions import ObjectDoesNotExist + +from netbox.registry import registry +from extras.constants import DEFAULT_DASHBOARD + +__all__ = ( + 'get_dashboard', + 'get_default_dashboard', + 'get_widget_class', + 'register_widget', +) + + +def register_widget(cls): + """ + Decorator for registering a DashboardWidget class. + """ + app_label = cls.__module__.split('.', maxsplit=1)[0] + label = f'{app_label}.{cls.__name__}' + registry['widgets'][label] = cls + + return cls + + +def get_widget_class(name): + """ + Return a registered DashboardWidget class identified by its name. + """ + try: + return registry['widgets'][name] + except KeyError: + raise ValueError(f"Unregistered widget class: {name}") + + +def get_dashboard(user): + """ + Return the Dashboard for a given User if one exists, or generate a default dashboard. + """ + if user.is_anonymous: + dashboard = get_default_dashboard() + else: + try: + dashboard = user.dashboard + except ObjectDoesNotExist: + # Create a dashboard for this user + dashboard = get_default_dashboard() + dashboard.user = user + dashboard.save() + + return dashboard + + +def get_default_dashboard(): + from extras.models import Dashboard + dashboard = Dashboard( + layout=[], + config={} + ) + for widget in DEFAULT_DASHBOARD: + id = str(uuid.uuid4()) + dashboard.layout.append({ + 'id': id, + 'w': widget['width'], + 'h': widget['height'], + 'x': widget.get('x'), + 'y': widget.get('y'), + }) + dashboard.config[id] = { + 'class': widget['widget'], + 'title': widget.get('title'), + 'config': widget.get('config', {}), + } + + return dashboard diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py new file mode 100644 index 000000000..cee8f5f67 --- /dev/null +++ b/netbox/extras/dashboard/widgets.py @@ -0,0 +1,119 @@ +import uuid + +from django import forms +from django.contrib.contenttypes.models import ContentType +from django.template.loader import render_to_string +from django.utils.translation import gettext as _ + +from utilities.forms import BootstrapMixin +from utilities.templatetags.builtins.filters import render_markdown +from utilities.utils import content_type_identifier, content_type_name +from .utils import register_widget + +__all__ = ( + 'ChangeLogWidget', + 'DashboardWidget', + 'NoteWidget', + 'ObjectCountsWidget', +) + + +def get_content_type_labels(): + return [ + (content_type_identifier(ct), content_type_name(ct)) + for ct in ContentType.objects.order_by('app_label', 'model') + ] + + +class DashboardWidget: + default_title = None + description = None + width = 4 + height = 3 + + class ConfigForm(forms.Form): + pass + + def __init__(self, id=None, title=None, color=None, config=None, width=None, height=None, x=None, y=None): + self.id = id or str(uuid.uuid4()) + self.config = config or {} + self.title = title or self.default_title + self.color = color + if width: + self.width = width + if height: + self.height = height + self.x, self.y = x, y + + def __str__(self): + return self.title or self.__class__.__name__ + + def set_layout(self, grid_item): + self.width = grid_item['w'] + self.height = grid_item['h'] + self.x = grid_item.get('x') + self.y = grid_item.get('y') + + def render(self, request): + raise NotImplementedError(f"{self.__class__} must define a render() method.") + + @property + def name(self): + return f'{self.__class__.__module__.split(".")[0]}.{self.__class__.__name__}' + + @property + def form_data(self): + return { + 'title': self.title, + 'color': self.color, + 'config': self.config, + } + + +@register_widget +class NoteWidget(DashboardWidget): + description = _('Display some arbitrary custom content. Markdown is supported.') + + class ConfigForm(BootstrapMixin, forms.Form): + content = forms.CharField( + widget=forms.Textarea() + ) + + def render(self, request): + return render_markdown(self.config.get('content')) + + +@register_widget +class ObjectCountsWidget(DashboardWidget): + default_title = _('Objects') + description = _('Display a set of NetBox models and the number of objects created for each type.') + template_name = 'extras/dashboard/widgets/objectcounts.html' + + class ConfigForm(BootstrapMixin, forms.Form): + models = forms.MultipleChoiceField( + choices=get_content_type_labels + ) + + def render(self, request): + counts = [] + for content_type_id in self.config['models']: + app_label, model_name = content_type_id.split('.') + model = ContentType.objects.get_by_natural_key(app_label, model_name).model_class() + object_count = model.objects.restrict(request.user, 'view').count + counts.append((model, object_count)) + + return render_to_string(self.template_name, { + 'counts': counts, + }) + + +@register_widget +class ChangeLogWidget(DashboardWidget): + default_title = _('Change Log') + description = _('Display the most recent records from the global change log.') + template_name = 'extras/dashboard/widgets/changelog.html' + width = 12 + height = 4 + + def render(self, request): + return render_to_string(self.template_name, {}) diff --git a/netbox/extras/migrations/0087_dashboard.py b/netbox/extras/migrations/0087_dashboard.py new file mode 100644 index 000000000..e64843e0e --- /dev/null +++ b/netbox/extras/migrations/0087_dashboard.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.7 on 2023-02-24 00:56 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('extras', '0086_configtemplate'), + ] + + operations = [ + migrations.CreateModel( + name='Dashboard', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('layout', models.JSONField()), + ('config', models.JSONField()), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='dashboard', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/netbox/extras/models/__init__.py b/netbox/extras/models/__init__.py index 33936cc4f..14e23366f 100644 --- a/netbox/extras/models/__init__.py +++ b/netbox/extras/models/__init__.py @@ -1,6 +1,7 @@ from .change_logging import ObjectChange from .configs import * from .customfields import CustomField +from .dashboard import * from .models import * from .search import * from .staging import * @@ -15,6 +16,7 @@ __all__ = ( 'ConfigTemplate', 'CustomField', 'CustomLink', + 'Dashboard', 'ExportTemplate', 'ImageAttachment', 'JobResult', diff --git a/netbox/extras/models/dashboard.py b/netbox/extras/models/dashboard.py new file mode 100644 index 000000000..cdbf85b60 --- /dev/null +++ b/netbox/extras/models/dashboard.py @@ -0,0 +1,70 @@ +from django.contrib.auth import get_user_model +from django.db import models + +from extras.dashboard.utils import get_widget_class + +__all__ = ( + 'Dashboard', +) + + +class Dashboard(models.Model): + user = models.OneToOneField( + to=get_user_model(), + on_delete=models.CASCADE, + related_name='dashboard' + ) + layout = models.JSONField() + config = models.JSONField() + + class Meta: + pass + + def get_widget(self, id): + """ + Instantiate and return a widget by its ID + """ + id = str(id) + config = dict(self.config[id]) # Copy to avoid mutating instance data + widget_class = get_widget_class(config.pop('class')) + return widget_class(id=id, **config) + + def get_layout(self): + """ + Return the dashboard's configured layout, suitable for rendering with gridstack.js. + """ + widgets = [] + for grid_item in self.layout: + widget = self.get_widget(grid_item['id']) + widget.set_layout(grid_item) + widgets.append(widget) + return widgets + + def add_widget(self, widget, x=None, y=None): + """ + Add a widget to the dashboard, optionally specifying its X & Y coordinates. + """ + id = str(widget.id) + self.config[id] = { + 'class': widget.name, + 'title': widget.title, + 'color': widget.color, + 'config': widget.config, + } + self.layout.append({ + 'id': id, + 'h': widget.height, + 'w': widget.width, + 'x': x, + 'y': y, + }) + + def delete_widget(self, id): + """ + Delete a widget from the dashboard. + """ + id = str(id) + del self.config[id] + self.layout = [ + item for item in self.layout if item['id'] != id + ] diff --git a/netbox/extras/templatetags/dashboard.py b/netbox/extras/templatetags/dashboard.py new file mode 100644 index 000000000..4ac31abcf --- /dev/null +++ b/netbox/extras/templatetags/dashboard.py @@ -0,0 +1,11 @@ +from django import template + + +register = template.Library() + + +@register.simple_tag(takes_context=True) +def render_widget(context, widget): + request = context['request'] + + return widget.render(request) diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index dfbaa1bc6..e127e164a 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -87,6 +87,11 @@ urlpatterns = [ path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'), path('changelog//', include(get_model_urls('extras', 'objectchange'))), + # User dashboard + path('dashboard/widgets/add/', views.DashboardWidgetAddView.as_view(), name='dashboardwidget_add'), + path('dashboard/widgets//configure/', views.DashboardWidgetConfigView.as_view(), name='dashboardwidget_config'), + path('dashboard/widgets//delete/', views.DashboardWidgetDeleteView.as_view(), name='dashboardwidget_delete'), + # Reports path('reports/', views.ReportListView.as_view(), name='report_list'), path('reports/results//', views.ReportResultView.as_view(), name='report_result'), diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 3edb70cf1..62cb8db36 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -1,14 +1,18 @@ from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.contenttypes.models import ContentType from django.db.models import Count, Q -from django.http import Http404, HttpResponseForbidden +from django.http import Http404, HttpResponseForbidden, HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.views.generic import View from django_rq.queues import get_connection from rq import Worker +from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm +from extras.dashboard.utils import get_widget_class from netbox.views import generic +from utilities.forms import ConfirmationForm, get_field_value from utilities.htmx import is_htmx from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict from utilities.views import ContentTypePermissionRequiredMixin, register_model_view @@ -664,6 +668,130 @@ class JournalEntryBulkDeleteView(generic.BulkDeleteView): table = tables.JournalEntryTable +# +# Dashboard widgets +# + +class DashboardWidgetAddView(LoginRequiredMixin, View): + template_name = 'extras/dashboard/widget_add.html' + + def get(self, request): + if not is_htmx(request): + return redirect('home') + + initial = request.GET or { + 'widget_class': 'extras.NoteWidget', + } + widget_form = DashboardWidgetAddForm(initial=initial) + widget_name = get_field_value(widget_form, 'widget_class') + widget_class = get_widget_class(widget_name) + config_form = widget_class.ConfigForm(prefix='config') + + return render(request, self.template_name, { + 'widget_class': widget_class, + 'widget_form': widget_form, + 'config_form': config_form, + }) + + def post(self, request): + widget_form = DashboardWidgetAddForm(request.POST) + config_form = None + widget_class = None + + if widget_form.is_valid(): + widget_class = get_widget_class(widget_form.cleaned_data['widget_class']) + config_form = widget_class.ConfigForm(request.POST, prefix='config') + + if config_form.is_valid(): + data = widget_form.cleaned_data + data.pop('widget_class') + data['config'] = config_form.cleaned_data + widget = widget_class(**data) + request.user.dashboard.add_widget(widget) + request.user.dashboard.save() + messages.success(request, f'Added widget {widget.id}') + + return HttpResponse(headers={ + 'HX-Redirect': reverse('home'), + }) + + return render(request, self.template_name, { + 'widget_class': widget_class, + 'widget_form': widget_form, + 'config_form': config_form, + }) + + +class DashboardWidgetConfigView(LoginRequiredMixin, View): + template_name = 'extras/dashboard/widget_config.html' + + def get(self, request, id): + if not is_htmx(request): + return redirect('home') + + widget = request.user.dashboard.get_widget(id) + widget_form = DashboardWidgetForm(initial=widget.form_data) + config_form = widget.ConfigForm(initial=widget.form_data.get('config'), prefix='config') + + return render(request, self.template_name, { + 'widget_form': widget_form, + 'config_form': config_form, + 'form_url': reverse('extras:dashboardwidget_config', kwargs={'id': id}) + }) + + def post(self, request, id): + widget = request.user.dashboard.get_widget(id) + widget_form = DashboardWidgetForm(request.POST) + config_form = widget.ConfigForm(request.POST, prefix='config') + + if widget_form.is_valid() and config_form.is_valid(): + data = widget_form.cleaned_data + data['config'] = config_form.cleaned_data + request.user.dashboard.config[str(id)].update(data) + request.user.dashboard.save() + messages.success(request, f'Updated widget {widget.id}') + + return HttpResponse(headers={ + 'HX-Redirect': reverse('home'), + }) + + return render(request, self.template_name, { + 'widget_form': widget_form, + 'config_form': config_form, + 'form_url': reverse('extras:dashboardwidget_config', kwargs={'id': id}) + }) + + +class DashboardWidgetDeleteView(LoginRequiredMixin, View): + template_name = 'generic/object_delete.html' + + def get(self, request, id): + if not is_htmx(request): + return redirect('home') + + widget = request.user.dashboard.get_widget(id) + form = ConfirmationForm(initial=request.GET) + + return render(request, 'htmx/delete_form.html', { + 'object_type': widget.__class__.__name__, + 'object': widget, + 'form': form, + 'form_url': reverse('extras:dashboardwidget_delete', kwargs={'id': id}) + }) + + def post(self, request, id): + form = ConfirmationForm(request.POST) + + if form.is_valid(): + request.user.dashboard.delete_widget(id) + request.user.dashboard.save() + messages.success(request, f'Deleted widget {id}') + else: + messages.error(request, f'Error deleting widget: {form.errors[0]}') + + return redirect(reverse('home')) + + # # Reports # diff --git a/netbox/netbox/registry.py b/netbox/netbox/registry.py index e37ee0d0c..23b9ad4cb 100644 --- a/netbox/netbox/registry.py +++ b/netbox/netbox/registry.py @@ -27,4 +27,5 @@ registry = Registry({ 'plugins': dict(), 'search': dict(), 'views': collections.defaultdict(dict), + 'widgets': dict(), }) diff --git a/netbox/netbox/views/misc.py b/netbox/netbox/views/misc.py index 3c8c93f84..c7255916c 100644 --- a/netbox/netbox/views/misc.py +++ b/netbox/netbox/views/misc.py @@ -5,27 +5,17 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.cache import cache from django.shortcuts import redirect, render -from django.utils.translation import gettext as _ from django.views.generic import View from django_tables2 import RequestConfig from packaging import version -from circuits.models import Circuit, Provider -from dcim.models import ( - Cable, ConsolePort, Device, DeviceType, Interface, PowerPanel, PowerFeed, PowerPort, Rack, Site, -) -from extras.models import ObjectChange -from extras.tables import ObjectChangeTable -from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF +from extras.dashboard.utils import get_dashboard from netbox.forms import SearchForm from netbox.search import LookupTypes from netbox.search.backends import search_backend from netbox.tables import SearchTable -from tenancy.models import Contact, Tenant from utilities.htmx import is_htmx from utilities.paginator import EnhancedPaginator, get_paginate_count -from virtualization.models import Cluster, VirtualMachine -from wireless.models import WirelessLAN, WirelessLink __all__ = ( 'HomeView', @@ -42,79 +32,8 @@ class HomeView(View): if settings.LOGIN_REQUIRED and not request.user.is_authenticated: return redirect('login') - console_connections = ConsolePort.objects.restrict(request.user, 'view')\ - .prefetch_related('_path').filter(_path__is_complete=True).count - power_connections = PowerPort.objects.restrict(request.user, 'view')\ - .prefetch_related('_path').filter(_path__is_complete=True).count - interface_connections = Interface.objects.restrict(request.user, 'view')\ - .prefetch_related('_path').filter(_path__is_complete=True).count - - def get_count_queryset(model): - return model.objects.restrict(request.user, 'view').count - - def build_stats(): - org = ( - Link(_('Sites'), 'dcim:site_list', 'dcim.view_site', get_count_queryset(Site)), - Link(_('Tenants'), 'tenancy:tenant_list', 'tenancy.view_tenant', get_count_queryset(Tenant)), - Link(_('Contacts'), 'tenancy:contact_list', 'tenancy.view_contact', get_count_queryset(Contact)), - ) - dcim = ( - Link(_('Racks'), 'dcim:rack_list', 'dcim.view_rack', get_count_queryset(Rack)), - Link(_('Device Types'), 'dcim:devicetype_list', 'dcim.view_devicetype', get_count_queryset(DeviceType)), - Link(_('Devices'), 'dcim:device_list', 'dcim.view_device', get_count_queryset(Device)), - ) - ipam = ( - Link(_('VRFs'), 'ipam:vrf_list', 'ipam.view_vrf', get_count_queryset(VRF)), - Link(_('Aggregates'), 'ipam:aggregate_list', 'ipam.view_aggregate', get_count_queryset(Aggregate)), - Link(_('Prefixes'), 'ipam:prefix_list', 'ipam.view_prefix', get_count_queryset(Prefix)), - Link(_('IP Ranges'), 'ipam:iprange_list', 'ipam.view_iprange', get_count_queryset(IPRange)), - Link(_('IP Addresses'), 'ipam:ipaddress_list', 'ipam.view_ipaddress', get_count_queryset(IPAddress)), - Link(_('VLANs'), 'ipam:vlan_list', 'ipam.view_vlan', get_count_queryset(VLAN)), - ) - circuits = ( - Link(_('Providers'), 'circuits:provider_list', 'circuits.view_provider', get_count_queryset(Provider)), - Link(_('Circuits'), 'circuits:circuit_list', 'circuits.view_circuit', get_count_queryset(Circuit)) - ) - virtualization = ( - Link(_('Clusters'), 'virtualization:cluster_list', 'virtualization.view_cluster', - get_count_queryset(Cluster)), - Link(_('Virtual Machines'), 'virtualization:virtualmachine_list', 'virtualization.view_virtualmachine', - get_count_queryset(VirtualMachine)), - ) - connections = ( - Link(_('Cables'), 'dcim:cable_list', 'dcim.view_cable', get_count_queryset(Cable)), - Link(_('Interfaces'), 'dcim:interface_connections_list', 'dcim.view_interface', interface_connections), - Link(_('Console'), 'dcim:console_connections_list', 'dcim.view_consoleport', console_connections), - Link(_('Power'), 'dcim:power_connections_list', 'dcim.view_powerport', power_connections), - ) - power = ( - Link(_('Power Panels'), 'dcim:powerpanel_list', 'dcim.view_powerpanel', get_count_queryset(PowerPanel)), - Link(_('Power Feeds'), 'dcim:powerfeed_list', 'dcim.view_powerfeed', get_count_queryset(PowerFeed)), - ) - wireless = ( - Link(_('Wireless LANs'), 'wireless:wirelesslan_list', 'wireless.view_wirelesslan', - get_count_queryset(WirelessLAN)), - Link(_('Wireless Links'), 'wireless:wirelesslink_list', 'wireless.view_wirelesslink', - get_count_queryset(WirelessLink)), - ) - stats = ( - (_('Organization'), org, 'domain'), - (_('IPAM'), ipam, 'counter'), - (_('Virtualization'), virtualization, 'monitor'), - (_('Inventory'), dcim, 'server'), - (_('Circuits'), circuits, 'transit-connection-variant'), - (_('Connections'), connections, 'cable-data'), - (_('Power'), power, 'flash'), - (_('Wireless'), wireless, 'wifi'), - ) - - return stats - - # Compile changelog table - changelog = ObjectChange.objects.restrict(request.user, 'view').prefetch_related( - 'user', 'changed_object_type' - )[:10] - changelog_table = ObjectChangeTable(changelog, user=request.user) + # Construct the user's custom dashboard layout + dashboard = get_dashboard(request.user).get_layout() # Check whether a new release is available. (Only for staff/superusers.) new_release = None @@ -129,9 +48,7 @@ class HomeView(View): } return render(request, self.template_name, { - 'search_form': SearchForm(), - 'stats': build_stats(), - 'changelog_table': changelog_table, + 'dashboard': dashboard, 'new_release': new_release, }) diff --git a/netbox/project-static/dist/config.js b/netbox/project-static/dist/config.js index cda30523c16aac960023ea1c9610bc5181c21861..02e2c5518000441a0c9f62f088a917ef62dfd4be 100644 GIT binary patch delta 27925 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 delta 22132 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 diff --git a/netbox/project-static/dist/lldp.js b/netbox/project-static/dist/lldp.js index da3c3bd4607caf419cc615353f67d4906349d905..77430ea575cc25dc7056ac80379706644c6ce1b4 100644 GIT binary patch delta 30644 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 RKVI1sI_Y??eINVf{{y`oJo*3t literal 108435 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

    W7aHcQ=1IB8erN^RJui6wNRN;WHBL^sUN_ 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 HD>w2#owq?A diff --git a/netbox/project-static/dist/lldp.js.map b/netbox/project-static/dist/lldp.js.map index a36df817f3ff73bc39626aeb596402509a3161aa..d7a46d3204c968be9b0c467a3b4f50f5325c6a76 100644 GIT binary patch delta 81 zcmV-X0IvV)zXtV^1&}e55C#*oeF44^lS>m4lRFa-lU)-b1~hg;K|w)-vS_!mXaUeE n1A0zcw~~?pp9BI!a+fog0Zsv9w}Y1fV*>+2ct*GBngIa?m^~ZX delta 22145 zcmcJXO^+m3cAmkJW>%IgQKOHQU;%;w3q_E`h#C>lLebgT$z(E_EM^yr)$pdeI!QK% zpUoyka+44YcrBKKb&6Km6Sf5078^>f!f?m-|hg{@H`W zSC^kXc=Y*8pI$z``snc0OJ5y6Ib1#d{L#xdAEf#3eem%6@4tKL>xVCW{rKT0U%i~` zlmGp1{^++p|N8MKm)|kz!^ekDoo;;6Z}RjHKE3?v^UofA_Y#eH&K&;a)t~*3+yC=- zpZxg6-+A(b7hinxr!W4=@s-DmVHi$6zcri=C)_Ph&W7RaWH}6{xtyIWhSh7g@@8|g z9fsY>J`XR3;p$|UpX*`y{UW!gC#zxc#;oBc4H@i$7c@K_mai}REpOJt>f>7{N9NeI zR@-6q#jR(|u}`OM+MGko^0nJ1m&0-h318lV#O0$~X|T?vNIn~uk8g$0Z60nK$xL*f z6=jeGB>QG{90qSMdFtI78rC)3{)9Dbpd50&W5{zlF27x5q>Eg#Yy4}Pdcs7BDK%6&Jdw3JY-(kaoMO= zSkix41b%Tl%|C>FtY7)Kf{W1U3cg>4X}KLPxrE0Poiy4$gQu@brxRby*wtAWF)kVc z|ym{@-REqCMwe4e=P9TJhPAKgBt(=wM; zE{d^MC7VaAI+Dvc%WMdQ=Bma+ZzHS4PZv%9;Kdsaf()`rOJPeVs12(SE&BIu)4zRt z3T0xFr?x^I1x&Pi-}P_NUyHoc2*TF>yc|~VoNO0A<%&VsKhK~nCxdeAkpEY+C@eO{ z25ewZurQh?!0_pFnC-;M~2$3LBvW}A1>iRC9UR$I8uh^JZUX%M(= zcyQsc{S);)wosr2hb$-RH7q}xMm}$x(vZ2_<*+y+vJTSN+69yLsWRZ^gGld?H>~Aq zSPUnJTrP9@@YZwloTlltkGi}dKV8p=8&o&6pK`R<|>Y-?iNB{;@ z)l(sGq%DiN@ic9(J=K`u_Hb*R3SEV&dm04oMZ+U$*)-tvGin2(jB@yGJ& zJWvv@?J^8}T_14@+|m>ze{Z)M8)UK=gaYH~8^r9t$z<;ibY9h{D8wm-qsSZdnm;0-_5FE41{ct5UU)mWi%ljw6$8V0MBo7k?2Q*h$_kSSRX zk!6IePkgF@^A6+iS!~7|N?Mk<;$jX9zC`y{I0%~mN(i^70RPF8zy0>d$H~4lIds4i zVJ#Z*&~2x;nPWw$(bsWkOAt3j?BW{$tI5rQ3JOhG4#weGiQezZ) z=?nE!AKe~#AZ1=+k{3*Zyf;BIJYYP82LYQ{tUZIE+5DW1_u(byC;8l{J5Caf*N2um=_nLG z%lj9G-UNI-_GXE@SdGXSUkQQNePt(rWvdM|(|2KivDN+QMSAIRqq@uXMzu^thkJmt ziSV03ZVY(NG* z-?%P7c>4CqUvizo0ocuNB9lI^OF9MX3CE{VVbCwivjAmc2*QQ!u)b3Xmx%xmbx6Eo zmAezdh4p)lp|HOhc8#T!EWfG5s|!Av&&Du(n@;;Njw$g#JTl{r>5l0k!B7&uRlwtF za&bn?*eRS4b$-;*Itx^@EQ0UlUVNLkWPP3*tSf+1QW|xv?KHj>D92tSP6C( zu6xooT#kI47LJ*lxDNU2#E2$TIAxI_L$QC#W8H*E!x)Jze%U2!=#QPP6SN7(plEc9 zpwlJOFyTck`)asd7-67TO=3dsj0;qU1;(Q|aeBYaR)(cdXH1DpI?4=923$+%0%M)Z zxBxC`D7S?Km8Fkis7?yqd0~FOYttbhI?4;#3oq_=Mr-l5Vtv5>g7cIP`lt?IY2i_Y zJe^w_2RPj4Ggf?%j)j+L2!h6z6`n@F@e-HAnh1_1ZpK(fJMte5=mV(6xCh7zwUqD~ zOCg`-@L+7YL3z57)T`=~qo(kzjN_C4{@?!S*lG;bj}a0Wtu=XFy$*`56R+S+W)pTT zUX}?K1ri4)?Ca}SWd*vN`H2MTj4#XdGu?4)fG_R-Vpba%iytvQ*rA|p`CyzQy&Jkz zx_g~b;VI$G#W*CWWwu>|op)Gr3}XPM3I_YQpUr@y<$VQ-ebowMh9%W*A82`W%9dIK_G z=zY()bh|b~`>4xtdvy{6Gq%t#WVmN(7L4Y-@T>A30&8hcBj-HgzyUIDL64-hsqr%u zFy0199p#MN+%$YKDsGrT>NoVjNEY>GLmk|{lh?+7>dhE$B~;+eyGW!*V)T9^&njgJ z0$O4kJ%i3TxMVGnAW9x664g?dy3=NZL_L^fDxky@$;Hc%x8ZjU_zYHt`gGU_L5tUS z#*OReg&fCUwOq@x-#qyn!lB`W;=nTGEmw^QaSHLjG_FAI6<4q<^SkpS<5bL537=dT zF*GUo4()m>;HVN8#DN)%sHGaLwT$2?n1#*PLWlbN zgz;&ufXlGempr9B&Eoq^i|I=7oR|ol{D{5tNpNHJxpX~c2rgN zn(}gp1Q~If>$kIzs%MiMeGrI^sS;7JKjERq;X!CfDA(|qNR4WUP0ok)J0ou^ZZNn& zGVs4E^;zhDT!u$(G3iWwM?(}Jz3G_Pf6?aOo8ZZot4PfHPaAHX;S*Ra2xaIx{!WpU-?d#K zA?Pra_o%4SONVQb5xmhZuEP`|81tLhvF#`oZ+Z3n_7u1q2Q0`LGt`))v3kav+HW#A zSK-bZ-dMo-2@E=ZYqi4nS6NoPSgV7CwM!JD_}`pbTz(A2jfbdf$KF9*?*eUEOL-4F zb%p^;raPKxy_?&AmSk5=vfAH6^bgn_h=xR!;o%`L-6yGUA!ai`hZZA*`8LJ>-dGBh zSWj;#kk5rI*M!??Q~L(vh@nA_9sLq?1!q|fX0Ko-K?N|d*TNMEIue6u!&>UX3Jb_pj|uTx-R&MD#42x+KVd{Pc+l3dq0*ayH3Xf?_O`U)_# z;UEQnFfFMh4@vY)SqHoyxfYBm>fp`(e^3J3}23RU9!A*9*A={E)%xU?`5jRG)b{Q&HNRaGyrj#UOD1ynA;A_qi!iwg zPTm2S27&VXV}WBDlAYG2-^^#l}9T~$``oxws!o5#82mC7R-whY7e1D@Pl=+t!JHf6X1c39YgQv*< zG$qNlt5-?LCgK2nAdxAr!S|{W%jpba*XNRW(M%K5D#;neE6x(A=v=JFp?^$cf(6uN zg55xQ^R{liyqhJ=N2LbV>v1xYCCTgxQYT(x*4>`dK$Q8>+fytLKe0b~x_Y}UtfKJ} zQ;O&rS4SB@TBndD*%NECNO{ejD*aoKLybUAKTB9zfuind;F1QXVl&s-+(iHH`bhW+wxG{4{hD;?ASU*w9dZoG=gYS;QeKaN2^}iOnbQ7$07zoPoP{jN*u4o zLmn%}=nS3kPc@d7sw}Ip)1jBaV^}}D#i^Lpa6btz*ktGt-U;F9P(`#4Zyi;5*A1;W z3uaUOXhNWb9x#N7SXB|64PDU)WPf~`!aLZ;QfH&^REA+CI(u3wpiWVPLkC-^z`P}> zc%d8P5^8mgFqA32V_C8VQz7BD@1olTKh*8%cX~sYDLP6nDROssr1nz2tz<-ztOcsu z3NpLUlo__Qug5)8?8w|GRpwxI(sDgqaei16jIZw5JvE3jfXr1Gha(f<+m8`uD})an z+mo*W5;&`s214`U`*sJbJsZKM9Gbf*Y35W}g?%_fu;Ca+un0Phwm2STu07~{?V^yG zyfL$=tY~k1-HFlHCf>vzlPa+;-#8HKK>rkBJ5l9CaB)1tQ^{pjreT$42d}PCbEL_@ zOupokJAEO>Kvb=5%4OSwlv>F#X`(L=!}6VW2Q72o-PeqMi%BjC(kQ0bPxY)dxW*I? zIx=*lV~kwq8Csx4NW^lFD6qqP$`GF}^tE3s^QL*(SL!tuD@NF+^4rjhp9at=$^xDl17pb1PEuHd9ZP~$xnS+7jqIDnVM78cznnJy(AX$h zj7>B8Wuv-G8t^CMc9E2<3MKBdCI%lvji`(q=Ez0uGO@ra(qknt3a(!R8K+dge;o-h z{|yNo0O>OX0Jwgk0O0r-3L}7v;6so;M*smUU_KVQV!#kUr2y1q-wi89XK2O-x=0{r z8k9Ouw&qkDHN9gMTOzB9HNEl#EvAF1p}PUJcau{uO7zIk}d zGVbtN{&46sIY-oD+^MVx0yI3 zoXdF@#zHuzw92%$EOJn)2Cddu9{DF%uFZy!{avqD_d|T!Ik9jRRi!N5r;h0}GOCRf zOOFjLH6u`)()P+QwWwEy3lkR{l8UwayLzMxSZyYR)fjhlS4~bHDzF@loDK$2nrIM` zL+ID6!3{-&Pw)5TqxYkva@?RIf_7lcRFhFTi>*Ip<#$^EK%eFPEIma54E~MRAa2S; zgl5(=W~1^}xO0iU-y$AS!<*@}gnDQ=aP0ExY!4W&M+&Z+F)Z)T;8-;U!E_Z-vHE^u z1#49?g!D5w_=+YJ27HS6ud;Ijto%(V)y}wi&QXt)1W_IzMmOl4^O2h4f~MEnI%HoW zfRmF9FiwfLWCfFrb@k)84?s>|TMe>`#XM-Vf%_`|(D9U2)TzV*dZ)kQu<0lBuQ)}h zB5+|nz7wG+ADjfCEpvUNy<$6IegvUhK$Oy+5K?NVNT!)^>%GLmn`tXHQR0rHqPyWO z6SdZ3je{k%JSB-WZpo4*UH2&Up;g(S_4AL4YvbL@Z5Ggk#hHz5lDI8M zgA@YP$9El9br5PjoYvYy9>G$FT?=)mo#}R#f>?hdT)7;E9ThFdj^1Isy90Rq%MvWB5cJN(P%3I47{5mEJ2Mm)ISohM_x)o6BldtW!td_Bj0_LO4(4ONug zxT$nWBrs=X7lw5Ntp_ETj_m5i&>jymKPEN+-4Xld&Ey5OyA9)$#3b}#cyNdPt(>%* z5Qu^aa0^X_3MP>a)4>{;XJ0N>T&)1*?ry7SdQ^@^D*|t7C@zB$tx{$CFJrY=eNF{4xa zB6Ee+Tn%@se$2jk!cAKxbk@RniGgmTX76xAkZsTi2d($ku{p&w&XI3n>J-tu){~T! zXqxf)b_Fst=dI*<1Ig-O`%Q9^Vk_|sjJF;5VdyJNTZSn!4)IgNc zv;46*eIIx|oE6V2+U^UG=g_ERqDXeVaZ$--cJ&5XRg-`9+ue4ALL|ln=7I?V~ zSWyvY;oT*P4`c(<9DM%H&}TQCHNcMbqvf#eMj)+$T4&`HW4n7AOAJLRmaWmpPFxl6 zH~V_N#eb{*Y*pE7&9NoBjEPEZ({Y9P?Yj|_K^3}7REjS zW0H9u7s!}SBx_~6Fhrhwv7rz{9I9nJ^(kjTd9%%VoB8T1iU*x-?|5zfDwdFiu@dCR zyN-jdFNvFBQFJE8W(f;cSlupk0Ti>B&1DsVMmKDe8mnz0Gn;J-fWOo-g8-)2LlS`@ zY_-Y49Iq(Z`<2ZN4W9zq8Sqn_KI_77{O+<>F~*d1Notb%Sy@2X6(U(3OGv<{{X{sR zHZ4a}EbL`28m0~B6FASUicTgg^w7{c?HImQoix)db4Z7&k6bHdRGer4)`ZbjkI>o! zQpQcH+FFOc94VkrZ8-KfvB8(EOkZJ4Uw!A6^Kx+pJdr({5t@{c_o*W(_0wK-a%tCw zYko)$Hhq5fXtgwkJU8Q&dpu+hxeq5(h+Kv`09;J#Oz|vhFx8J18LrE4?si>V*|BnP z7u8>%Le-A3`?xOmn+-@hp3 zVV*2vwopG;iu0|HPKr0IaXe_#K8~hqDWf2T1~rm%OcLL;!B_E5VRr1F+A5`tXg#+K zt(>Jg0#l)P6WcV)mOM33QQ&2aAT6m)I8*Jo)Ks;PC`>A2mv&B}6nd~jsJpZ@ zVxf}+jtA)Wn{Iva8ab{F3lU?NbD)81ltWwO-?w<0yk{Q~D3;#4Y45IY z$W`y;Z!@s8y(_7p!TC5f^w^4$b;XkheGM(qzw&Jr^Ip&Wk`-$dCmo?HrTytQXo!N; z1RR-l{=z{gpG9^Elz?r|8R}I-PPq)Y8cBp%#o*>b3Z!=Oaxt8>SmVa|Z~g2nARB!T zijBL%w=k~0HNJGwJ`yucn+$GcFzqbCY80_7mGP|b7ta$PHnB~HU>y8@Tg9z}5}SDJ zp|6nl4UvGb8CGkLauUObZoaaVLO3@dJB{QeEW)ClGxXPB!kA8YSR#LOV^lV?Q%lV7j; z6&G)W0HAbI97gwpk{(IJ|Ei8aQWNSgjpksP8;#)xm?zy&m+lEz_@u&jNElSSk3exTO3Wd^JQJkEi_~^ayFH&w&hu28VW0&J#!gLQQ0Nm4cmIn zSMe~Q$0^rXrAUlZC}8r&;anX|EGQ`ge)dw1)&xs16_Gom=X`GAc`V_Iogn97_j)QQ z)-IxRUkV2$4-*rNB&PE>7u7m@CZ1Gkbra84SJ77vIRQrLf(b9$DftQ>@(s@lL*jmn5nmyv0|>uCVc+75>YMC-o+#oNX-bzOMO4Wo(vQii-Ucfw!LcF1 zUaR~2y8a=F1sQeCK1FdOv7J(321>K^PMK%(R>*m!O`H5D{9`GjhF}m`Pe&QjnLclE zxz!MLt<$E~UaPc#IGV1w4KO`yD5_)wN^)3}jhg-q9dq!ovZ#h}rN^ZLCP0?i14miK zn>e}c2aQv3E)5xHyZH7m+Lrfozj&WMT*Cepj!=-N|7Z&4ylI7M`f_RUFaL5$@qrK5 zp#|X#AFMWJjM_f+jgtJ1Aea1)z~!)+PHOl@ElbDBLQj*X)S+~2!nHV%$^C4es03(L z=F-oPgsYkfa?B~{NubMsIio)YOsW0xSXJOLM%sS()c(y5(usobX$`=Awl@^!M1QZ{ zs+~akluJQr&@9K~5|QGt{8_%g9V8Ph2T3X8-TXzbms3^bGdjJ7os}-q#L#qHX(FkA zI5+gRny6a{Eyl~g3M^5zX#l#icII{qf~IT?_hXNIE;;E*`8iV^;8>}vRJb`CTE@#^)C>*jbjFN@+xv57I#K2m1Uz!1{XN+ zqgzdkKbB6rFPx<;wW*8M$T_h%P&!!JsyBKnUXMRllox4JstXaIZnkX5FE7Hs0*z;itp{u$PAr}J;{Hc8VJwU()*_> zur6UcToQonhW8DW=Jkw3<0#UfqauHKE2Y;;dPRdX0vT2o^yCv?Zpr^y<^9~oHbaa^ zuVE>{y8ne=?UNTi$~&%ewI%*%o{PGd2onkjcw_Uq>}e%zF0br9~_h#h>TS? z{$$k8j=Xg)?ews0sQk681IudxeLXg7VFK|NM&vsje;-->ThP$}Sq08f8CSeHt&BQD z_qqYAlz@H|#@`tK1fxvCC2GA(!R^f=k2yV)RuEMg==P&g~xZFzL@HYJ`G;G5cD5raB~8=2Xm4cFX_eu*d=rZEJ2>TaDs`_-$@kmIvmw zKu9)=x)G1faE(863Z5i2%FMcdpf?dZbDsB4^${WY^)0;U|m zt1nj@;O0rIv{c111scBqE2JKZ{~Ecg9#gtXi*{2;7<}db@0eO6RxC?c=Ax!jPMJTs zhf!&#q$ib-!$DQf0i+aJJhZ*ic8co01bVEgA}SwtQxOa&pQQQRWZ8y`WMrN#HhucT zDPOzX7`*m5V;n{kn>A-8?cuiOSrTlvFW5C8I(Jv$A0?7}Ii?U6LH_K@J@F+7TsLDp zb36Gv0pt#icH=U+tNJ>UXN6u|X5)uE6XN;w9f{bP@6kM_s&}^x>=%;_sy?iS94z-p z8fGq~fbr9U@`7t7=^Tqk!NSn0_EFIqv&aNxq+T^~qfqrGqQFkviRM~wRTYej%WEh= z`*CLZ-Ck3Ee(exLW9x%Kw#t<4Q=`HjSYRhpwcIXV+ui+7n-_n+dGUq+^&fuc^&M-6 zKO`Od#h174|NM{t>V^OMn?Jby#eeySzkcEEAG~tw=f6F?aP=F%v3#`p`QzI!yzs;S E2SkDQfdBvi diff --git a/netbox/project-static/dist/netbox-external.css b/netbox/project-static/dist/netbox-external.css index 8164a7fa8b89e24c534174cf0e3320adc523ab30..edbb6aec56b4256e3e0f9951fb356cca551459eb 100644 GIT binary patch delta 8443 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 E5AC*h8vpF7M3lnSw@Up0B5oXfdBvi diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index d0058eae95429ee31dd23e08aa909162466a1ffe..fdd9dc9433599baac375e2dff44432d30bb93e5d 100644 GIT binary patch literal 436660 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 z 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�v;{Xh(49nhO-il$x(l9 ziTvD5FAqeQyq`K~y=djleK$kea#ddYi(B@>ZH1lPrZNjoX-dVGh-|VMc-2Cs4K6SW z_Vo}{ZKKg$Q^68^eiTw5E+N9w9X4tG)e-&F4`phYBK}-5O{|GwqP(iijAx$;=Wi!O z%pkuu=RgRT(xCFyHqm0HeF#x#St!{ZlQ z*O*^HbbM%u2uL zFSDo~xA5H5NF(J@P6KPrq(6jArr)?wtmA+&p@z(L)R7}W_+uZ$hp{hWaoB2eA+{zZ zXO8Yn2#p`m@}A6ZMS3yZR}FZ0lg7qbft2x^bqAZmPs~odI6) z;gm5p`~AW^rn$S{Dj3P+{A2GYFWuF(9ZEsj5TP_AzWN(3aV4 zYi_=U3}TETkz8M6dcGrgJ0?g_`>(kDn-DRfgo7F_c&hWyv_YdB9>+$Q5 zhxH}dTZ>jZyF_F8vmjtfbFyKDweB1ou{A>5TexBmMj^5=IbzbtWWKXTSl*`Rt>n^8 zi8xVTw$zg@hII~5bxI8);Ez=eM7Jzr;eD*gl`%J6mCaA$u5}JrBU)8N{_4<@&IW5> z#9Go)i*Y~#umXIm2N|rTq)4(xtw<>(`e$1#T?)$-x`YW$LKPIf22>&9*8r%mKoz21 zCZx5YmLQ!5BmC7-rFHPIW!Moy>%jG|Fy217tK(G(2oM1FbsiPh@al!vcv7QV zb}r{h#P?GAF_bjKt_VftX0E&_SHAKh?9C}ZqJHjSTk}6ZFLa{IC;8RsiwZtu6z{@#S^@V8dcRZ`yQLHr*;piOGSdlvY9&x zs_J%)^c}+@M`rytfaIRG+l?rOF~F<3Yq3Si#oCn%&xapplv-}pFyIlFaU6}KsRmZY zoh}KaYIhd@?9$FwvmeAwob2rcLrNQw;n|2^GVA+yP~tW1v4%PgfjKE25=-YlCBo50lPd-WO^aF38R z)MitW6|b$F$i%t%dn5CZruR$g7Q*n%l zw1L4BWb#^ry|^ec32xqhyt?)D*~`@*S2yUwu{?gjnLhu3Mul100r4F-(u5ldX0;aW zIPnXTxP&hdGFVds8KGZdkp7zpbvymvu-l%~dvGkh2R&dWaiPNoMfE;Cc-w!vKhwtDC)zIyzK ztdpNwX8y1M{2SO4zud`OdlxPLW&wV1O$+dYD=fedWC4B@BP~k{@autFdVlV_<@b58 z1V0af20xBlfS>nTMpYF^m7;&*B9JvxaHXOYUc_Qx8)#-`W+67<97mt z28pVNDA=NB*Q-;A{5)ao^jGVscV$&Qy2Tb zi7b?A93YM6^WJz+!OUi|H@&9M@?#LoW`0=luJdK_3?+^GJKJ*$gOl2{5aERgfkFt( zUqq!ZRaekhG|RHbF?qVKA6pR|l|5>z0XIx!wpr?1VL7#>48JSEIrZBDg7$Iod~Jvd zkUA_Dg!U_dS-^h6JM`a&q)_n|9E@Djbaq*P?nL7G|^b_QaT=TKeP2Js$f3Cx1OCOqe#sCuS|* zjq!%!_FRZ$SA2l0cS3niVDXk1U@nppC&59dqul)T`urFIhaJ82A^DXvH0YKBJNBo` zQQ5<5{ZmEy4RNnVI}AkG4x7n{&VM>gYt%G;9WFVm|M4x7o$^t1cDu*&(6(ya*}(la zf24Yhp9t|}$*NRp)N!rjqEw)wm%*$$<9+`qy^r^9%`kO)oPn)TrtsBBhZ$c^4|Did zqUrrdnyII+kxU~F7U&SaO5ro(kupW-&yKX@0F)f)y+~r+T)wz{a3WJ)1=uADLKqvs z2Du$8?ITvfEIipKP6F4KuFieh_|0<>_OFpqw)6y!$q1ZGQ=32G(s8&6i z`wDAc)ih;oN#q_67^)sFx2|lKilsAbOcJ%4uo*=WdrKvcNdZq%9>vmBKy}m>N>jl_ zZ8k{IFFa`&XB(=!JJ(;C-i{8+WQpe+X{pUi!wuifhk9|=i9o+Ks zTT;KsC%`1OdlM

    8fd-9@Fn#Nc$oWMO13kt*m<5Fr z4m5X!7-M+|DMayCUuK7k#&XU4xm`IIn!N4?f5>vGz(`DZaJ#%Kx;y0Fl}g8Zb9!2B|IhGfZI$^Dp3{Ngdv_S>{0ez3VfU@ z?$|fiAmXz>{zf`JwIQR)lce1Eh(XA)JY0TeG$sf3P9^C2wBQ6rget*MYEh-lx^~Je zg#FrP=^Us=z_Y5YHMvP=wLJqZ$88YS_;u`;0mJinLdd2=+;>>mK<**(9T9*U&i!C3 z`1b99!ac@wW_TE)mt%2~)42CS6=uQf=FM4eU~%p(A!-ArNn*yl;iH6OMO8KxUyU`J z=;@62nVLfRGQ{|a?8q5~8K?ok_rLL*!ne};vA^g}BE#$CpaF@;95lKSTrF4pKZ#nq0voo& zmD*Y$8XS)Z!dZc7zH#I7!AXPaXyj@bk1}xdxrXI)!3|mOa1c||9(*A+Q|Dk_YKAz~ z**6R7L?tgywUmR-U7#(qhc)NS9A=|&9}^iMx@R4t$8G520vQ#Lbt|FW9UaFEGmZza z{95C@y$qAePwZ^oB^j5j{bV#WP9CU_9c0(F9J!eyVn_0{vV}jn-8kM>3>O z$Jhc=?s-yPjN$R<0EIDw{fa)zYTC`R2B;BMm+k!1@Le?DS!FZ~bx$;k0iu z;TkV==0FvEcTk(XkSo8*m0uDuD$K_NV|MRSrYj`Ih`*)R*vD8@DzUOE5tIynjLTz) z2-`AJyqO%oY`^Gstih{2$rFSAx+{6&ld%pHvfu~LFSlD6`s-FJuK}JH72c=<{d3Dz@IUcvK#Abs z+4;-mrF0ZqxF~TXRvz`h8Dj@@CCYwnifQrA85xmaHbCE-9%D|C7BTK^Vz)1s$2>7V z_LnD#BF>{sVzJ*=a!qg2mn*zUU)-A% zH0jct#C5AmchGzkyhuOpxmW2;-;b?+3TI5xd4fm(GH+545=^LF#T;3Qez$@d-WxkH z?Cb7&sTE9Ajy{~wk7@n)Em6lm6V~qhGn|*X%C7?sPZRgT8cg+vRK$}|v9RL%L}+)0 z)e|GeSVXZwF{rAuzwt)z*`)%6&NCabq!{J`fbZFwVe|1H7#FRkXPcWZ?9O4=xukErgY%grnnZ& zC6<4+Vu|x#J-YBux_P*I*3QNR!eF}~n7eHENZ&n|On=xboIiye8gEm<@ZOWLvOcRK z^N#tCiPD%fvr#5PMfHsBSYydPaP#J@H_#+4$LtCrU%;9&VrX{FSuI&RCW^q9f86^P zPeK0(Z^>ZXa-*`Utx_c@OM&(v2aJs+*-Ox{4avrhJE0?#*o@v+klh6vR`fa~5SZ3T ze8bi2JT|tp76IbXwQ}9HHOL~sd^2S6A7T3AOMvBC%^uWTfLR=ZMhLV3?u+dj)1I!n+2zu zPYwyL@Dr#PKCO13rcvKqTuA%+@-iCt&ZjKeKZ^O|Sg$^+OJlwMb69V|&vN!T%@L7` znj=#U2Vp%eSqK*J2W|Lx79-J7!PChiT8%EF)9qSEoL^Bl36s^^U$$)oJf#GRGNg2>5d7!pW+eS2PRU# zx~53|>I#wil|<^Go|FRh!;h^GI-q+Wfm(mpKDKV?@8g}R`Cozy&Z4a@amNs1O@^;< zNk&@HybR?cimeFL+9v^OH*dammu}Pk1Xob_jdt=bMclFk z3b?hkEE>p1LEYF|>_KnaR2<>D5nXk|PiV^QmV?MO+44qA3Zs_&07M+HKcPXz;wfW~ zrncSGPVT}GE*)n5_W{e#$NuX{&4Z`F)_1gQPX9(+r^kAF@ zS-ECC<#b?tLky>!5|r;Jo_5!T5A*Tu^vt@teRXU`Re9v9CoZ*#t*5>l&5+%7xP*~8 z9+B5k1Kt>OQ^bt@@RP0f#34{?p`(}nOIhMWG(GZcpr!wAgh~`RAXIw3212E}_U$-9 zAH&6Qgi0URzY(F*0_GO3xn#Q$s9#wa)zh-*@A#rTjcGHkySe3O#vDDyaCJzTD=XpSyEF|sf zQ*~1FWySK9(#nbek(pB+;Z>!hjY#*taYpGozn>~886_hrI*KV` zWIMPnx})>!W>r3g4H{z`bQQz)TFfMvJrdhIseR@UcIL>*6kK3h@-cAEa1VwsGxcL$l>Bpu>Kk6ZY zR1)D^X7G=#M_kVSsJFPa<(}3Dob|+1z=#6L_2QLClRS^IlQBS?qbwj#Jju8hKHMec zXRjk0X@|?T^&K+Pd54?#Is=OmC59E~Ev1(8idk$wobociw5--wcH8YrYzYSgpbbCn zQS$u-bF2}e=ybZA>2Q|^qY}P?)8|Rt@lIo zt9Y23GS?ZuBO}`xE>@9Ba{9;A7k)^MhID_D-5gl>+)`Xwad|*ZMfb&Ta(tF;;Fk&;Tn1q`t{t#&H)#Hs9%D?^r)u z(`%{6>2Dk^$nsy2wG!AYUhq+;b5wudGescYgaIv%R|BWYSHjpK3z#o1#+nL1j~7G? zL7wY13G^3O`l)R?ns6J%8Q1~~x^|oGv)OH0V~GE<3u+fG72|x+?;lJ>F0Vdykh!HP z*LVoo{aok61CDO4)D~;Eszs7+y#~Rd;TJbzOo6auU^C8!TDk~>AZ(H7=N30QOt)sx zQo86P_L&Nn#+PoqnINV@*X2~3*l6Jzlp2(k%iSJcqGY9KB;icELU~;)e1qE}zyUoK$;t^k8I&n9O72xR9`F9o}&CaJ=w`Sm@ zZ~L+4Q?Dc)GY;ar$ysu|Hqj=~u}V&!>qG|Bhu^-NIQ>DD8Aw!xGnXYZij zjtN{v?(8{o%DiySI=QXMgUq?OFC>}Z2EcIE&O3~k-1i5oT#aehE1pv)W%JnU>L$O@ zE&kb;LeFK2F>2ZF2bAfNE0uZOIar?GhgIuZXQEu(FMVaD5tX=k((j`?M;qtT z=k8KH9k+beeg>2pD|b8pxy0XF)i>|M8~%Q`*F{;g)w8=o8B&uETK8C(E+ImTg(+Oi znm{2U9yDG}Lt!_*ZAt*M;dQrW#=f2X?!oS$({pVc$WOjnE<@3s)`X9QVtz+xpod`m zE;nX2F0B>JO!w_RHYLu_?FI>BDz1b=@5OY4`PN-B+}qbzvYA>*EA7j&EeU2%(azcB z$+jrtq?97-%quv+ih}1ZwWX~v@xcUayrJ-2(O&h=bL5~mrHv;Yyakh|zp#_eb{Tth z3?^=7K>))@)RCeffQ`eNSJI}WJMDOm8BgBTlr{& z`u>12%m6`6&H5IuB{oV~AL#%kniOrF#m-=WMI*=x3@%51{81zY{;~tNLms$)a8Pf| z_4;#?bmmUm!?`_Ow>)>+J>H-51PA@WT7dV# zRC(opJ?nFm;amkEbKT+Gp!4aZ%QfwEF_=3to6y|D&CNOSK{Z8khB;y3Z5hj(_Q+Sq z`<=NUyr(`Ee_yIq=TS@|j!=_1f^Bzp7m@b-FjBE{nmOSwS1n$?E_GrV?f6?F$uftY zgRTN1UF>{hCCkUSk_1)FSjot;klDkIjvF)^EzGl4E%>xxc^<_HG~Y%?oHzFT~M z^{@ivFX|VoFa36ES0+|B$5o-EyB3@=m4%U&9hED_3d9w&9!miKzs$W2cT-2Q_xn|h z4wtclY+%lF?p;fc@gmp>B)knK2^k+B5Vk-{gycvTwqyIdKfixfb?@F=vVocBu6xg% zVC~+$`?cPyy84oF1mCfkg7$ebO~XAffn<9)){y=vGeRyvCQn5D@vQ!MQhyxO9|imb zzvn_bcw{l3rY(e$SuhTVgz{9@m*EBDY~Tg+gkGldB*y{ie(5}Lh#>|C0e4^)1dSOl zmSUD+mBb183Y>-0(K^ou-jJ`LmT(cVlTWumcWSI`o>y%&N~jX1+(qAg2lQ2o~5`!~;f!%L?^ zi59U8X%q`Pq$P5CO7x2pTcfin)gzi&@6gmM-X~dyRa(OO5QfYXkYAN%Ix>G&G03i0 zqd7@kumkFR8yIdXU3AD6rD9ZcyiN!s5A0TB4~s$;S0g0)fkTH*NLh)mlqb^R-6hct z{J>vTb>I@>Lq8A?)wg5W$dABTNX?-DeC++v-*xL1UN1iBuXpN94Q2w^u2*h%gw()ctY#a@MqFL@z$|DB zj^)Aot8Z)|2SYSI>(67?=hzEw91y#XS!ETlMIDsxRJBi8(m9egf&(Uop|&8+`bxk! z&RTl)J#U-eL-4c`IFXhi!gcvhuv-%S|IHK11DXC}Ax_8;NCrQ0CcZm!-{V6vveLtD*>eLU`m82hhsGyKV z2tET*pgbP5(D5^URF$+lX$qCVb=~Z<{1!s$-!PXgT?M*mP*E*}6QUs6+o*O@y}vs% z^34*+qNAV_>B{Wm`=Q)a?sFlqZvCXQV_-A54WJD=rwNA4Zn|+w)JT;lp}ZZ`$+81U z=lrC5h9RoEgLSHz%%I+cYls@lr;_MMg-1XzRRfb2)IV6-GIZd?itfde`|6YBka*Sq zcw`ok_i|#WOn&2V_J~&Ns0+oF7-(=WJ%{?tJ6ha|tdAIh%KRHo65z~+ALrz6??2Vu zvQE_XabJ|5gUc%Am(H8g8Ms4Mj&7}6+d#o(n;M9#O2PDCmY9~52#=32q=OJh?6K{= ziUjh$NdnnjBY`)j;sA`VwD6e%>-{EvApTHqo}r5cXC}AQz>Iui+W6`1Wg}caWzDfR zFMW5$x)*yvMLL840XvQ1VvtPewPYDk+HT$Y`YnT7F55D?_ANul^HYFMsV(Db7@BMl zu^ds!4xRCBGN#+QIFmLZpCQ@=;f4H7Jl3!g{%5bgoL-YBE|DElwr{~LGldE6q-77n zba4^#MpcxBf2B)Zcx!?HmD3;rt5O^cKu=**9Pj#nt@+bSyyBJH@(59)umOTj=x-=ZAJKKvd%~4^3x6?0yod{OaUpd}f1t`L~{2n87 z$n34JuX8&|XD#k4FC0$clg3h=;tnEjD1i|*9ZGa3GxfTnQZ&cfStT2s^K&Wc>$3?n zhP2#=I-Dxw$VpD=iyzy4A8fpLaD@_*M-a5`0GDE@L2#6X!HHEkWJqgGh+3}LMrkEa z#uq&3K8)u2LM<|&Rpw!9xX#RU0iLb6`YhShex}DL3HOPf^gO_5BKJ`dhKJzPf&L;` zFCRjw7#2HkmC?N98w754c7~nF z-<1&2lo#Gy0pE$rS>sLggBJKEEP&-WlOp@D`QFBJS&j#rt|&!NmU1ffT1->Inhz&T z2Tp8~>7e%N!IxMYeCcF^ewbrvu;Q6GlVHVZM}A!beZ@W?G${^Kmj9I7q4BMPSC&WJ z=#6+7X`fQA2TvH;XkkM_RuXuZXtAXaH(TIL{8}bCfx&g^Qp`f5x1CH$NN}xP<*^BfmI}bx`Vowb2XN+ zN0-&6u#*T(23>5XVNbg1V3q_&-m!5Mg2&~OO0`BnKTm3AJp^7;=!#;PYU@=(5i^Cv zzPdb^eB{nDY@+q1owenc0bffErW!ZRjOHKXsF-5SKJ|A=|IGzj#`1>W-m8(FWW;4J z3$kmpMJ`HhNU;!t5@Lp&-WS3Zdyu3~`=of~tomjt2!-z8yLd$tHWcHO4r+CPb@a41s#C0Ag6jv}Hym4(vciwO87RT0!YSuDx))tQQr> zwHq%SxGcO=3?{)$wfXi++8h+W;22iy1S<$Zlni}qmV&!4n85UV#g{H8z1B_=48I3z zUyz_!aXKbZaKBK^+GV27GHC@o#>au?zFDM>yq&$U=|^G~epjfBCQA3HOq2S1$?13h zKHw%Kuw1WREFs|1=pZBuL{Sr@+qAzZIx0FQ!6qgA(Q$dYIT7<;V1e=&i&L9B@y@>} z-r-#^>5dg85^OfSBUSLMESn7P$Of;~<_Ju+ANCo+y+JUXUZzUE!A2SU1dUF_KEUC- z^YMrSeuYc04o(3lSJgGYB9xeJ3=%`$#(+Uda=$iZAaOonO?vOn`2azuDi0TYvyRO{ zkH1J;0rZxn=Rw~c$Ay?HEk!7ja)+U(E3_%gdOlzjc8;;^Jun!@1<@fBM-Mop^Pyi! zr-z3m#|cCkrY%;aeDVYP@VB=i?PIK2J0_KJB?2$DQrA-$mvk#dFb3i&No&L;B}a(WTL zNUrr2d@L(BS4*EJKtMCt-P$WBaNYjO@8{YIM6PqZ$%O zyL02lOiZ=1AJ>`-1oC-qZwosE-@IjiApwUznLcxG^Jj4aR^qjlYAyVVzHy(o=G06y0Ma_I+d&22C3^1R+AZ|0hhM+#z3KFaZVJtIQF5H zih1!qlT?~lt}B86mxZwhtoxhh%rbSehTS?)GYLfHi$He;yNUW*j?tuv)K=f44$8WPL}t(56p z?i*sCNL#TtAMQgNUA0zTpePvI4_JfZqlk0it3(C#A#sMPh=IJ;0C`{ZjJ&Nsi3fP0 zl6u_eiBvLRM9yCo{G}g{{0G-rryq}Uzvcjm-FBUU-dUnAjy~E@gc$K%*@w+!={A~V zFy*`AO^ykuDw|mkiJsGB_;_yIvj{ZFw-N#n9fs}skT@bbXuZMh>c!QdIQu z@%NDkm8HpqGfZ(T>GhPoYT>kV);Z}6EXP`+PFIll>LXG3v(A^%b;x%fo|yAp5qAhW z_c2X_py!__Rq3GmSrX1%dYRaR?MO8=P^Pxk!I7&90O(~BW@-o`u(ihG6Z6c;fLa>R zjBo?fWkt4Zdq~0^$6$oCJTzfXGLJ$i?4zq(kzu@!&PP48ons3%p7|(YIo~lzp5aqCKi9~nuauF@Kaya z^3CG9hDs4|5OwR5DI2Iyr+l+yLf&Wyk3pSfsM5(28jrZ09w47)rfETf>_<&DcmFj0;tUS z)|c~9+@=^qU{N}6kzm){!rSEl52qS$a)9*mt!o|#&(^R_(`5%FfWj+N$r};~n{{0> zSY92;v02@OgXOi68=JKyUo5Z80fM(T@zvSBN^uK^qqa2#lD|~|`SlP{d|9}a1S`d3 zZg1nWt4(@|x0u|u%p7yt;9MP{mwGzT(>n{))FY`}H&$e|*TbBLw?$z7Ra;3!`2Ty2 zzs;*TA9@;HC1hF~_uqEjmb3VhfnL$p|G}na&9)kg`)`bLe8P;2t@$^a`ajpwtkKlM zbalJ|Vl1L;NEGOAWCBth88D2BLC&EanZ|+tWRv}-HzX`1>x+pfl+E0}hwk>RPWDju zwto*nnAp+L&!dI|#U4qT&o)~^kF|T3t$o%y@Wt9$BlX6YFSr^Tcw^pFMdU!RuWQF> zWZ71oXnuWrHdsipw_KMQWZFi( zPPmrOpqi4>FrHD5+N_3j?J%9$jBM7(GYbc{7)6>{ntUakn=ynY5z#8fRbP)-rR84lSH)buNkVP)SMv>ICS3-Iftfk8(TS_bWYggr>E(}_5nx7dNL?YfpE%d&38t+9tTAgHOvv0c?gp1Ol)_jEDPM&E++F^M18vs${O! zg_sksnPJ!0wZx6{aJ8Rb>Qv}yB_8J}RXT?>)oG5UmJ;lIVSQzp60DtkVSVN7RQqW5>T9pQ@DtJg)Qv#kOFuIp$-O=L($5T5;xhp3OFxsA5OXWxxcvOe#G-XL_r4Rb zeo+GzFsF|N(O>$hLA-zHu>M6KW}~#WlOqMopiem&GpGE9M|?v>s@H@mm?6LYB8Vr7u87-5}>*?F8` z0?QbCw9W0a(>XDvDQu0ZRXc8LP>4Y`#_`@u>nIzou^~nTCgT_8=yn%Dng6BF`T7*8 zSH{t%RTRN6lSGzEiZiWYEONK{4-wDVu2#h(W?6YFs#jZA24}S#XN*#GtrY%d&5j@V z#do1I8i|M%{*Yxy#A0x^fI|Y}E%fus^sLTWT=elcSHf`_x!j!PFs(*-fujfU!kj=Z zM=ZvCkc{qm41E#08({F+w(jD@FW8@5Q(p_DXKaLN#yEWN`4Gjc!4z$G)DM7sQ%P>= zO+M+d_^QC(Xl8Y_9bqO_?G*laMb80!>r1*OJi%9`Y%F0%EuNwbD*)fKle+p%)J+|_ zlS?R!V2i<~2GFiQA@n9IapRCe!V8<+G5~8UMs^WggE-i}2ztVt^(YlkAuj`<0;auq7 z36ayK6-0~_$H!ijK~$cGA^V=Py{Gm?&VE&uD(2@a8?rE=lt+t}M8K+1HuL@_mty70MB6CTGJjvhBT7LEi#>Yb-vinV zma~a*;rnnhB1S3`SvI};2iLe7`QYAun-GYq$N#mw$K_4C!_rz==(ZZu->(=G-~A`O z3jp%E!$D?!S3nE((I-+;UoSCNF0#j5M(mmz?C&L)3>H6=pEiX>c+g>EeTg^x;!)Aq z@*dQ84Y!I;7Qcw)x^h_Y{%c0&pIq71*NxA&`pV(?>#rG|eX%$=Z|LjBW)Obm(CqEk zj7%Ra4$RT$tM;@8o=voLAZ(WUo|S?@fy5z?6>wy$wOD(3+fjJ|uNKHj2I+!g65g08 z>o3#v0ugF{q?9q$Ra~p!VJ-g<4F8K0#OAu4ltH}~2BNm>MxXiXdp_0T#s$DJg z3l(-(Yh1~AOZGoo$CBHZM2cG;|lJ2qt6j{`;$jA99_jbUL@RY|%?26zBW3RZ@FOnr!(y zsy(m3p{6DIt%M$_Pmn~RpqoM=v`Ls`GfemXS;K_+meYaCZMg9PiYV!rXhiwa@z6t8 z^#&IiftPY1Gn}|RL^flh6diuKlL_aT0!FQ+E-;=22-L}t zn&=W>+D2Q@FakVpSjvfnzOukM)J)aJBaplr-8}yQyd`|M$ zT!Bcgml||o1?tb>Jw{-zK{K&k)d}7z$)aWU*Fl*2Dw&q{1upXPB!?I(-;LQn4 zMf7h#%)imWWi_3Wzfr7}F%JkdO}T z9D(q_;V6D28Yi>4h*TYZy=XXl1@8-r;I_sp`y`7AGg>yn#_*Kf5F$Tv?hcqWH(QK$ zmGT-UlWt4nEijpqVt49vmyozfWKe{o)z1;S=b(J;GvfMnNh!zzOTklv3mt6fd-4vm zgnV5a{redGty}RcaKmXSNo%8cgG99fs6HBxnf5$k+;gNvwn;582#PeK?Oa-mO;`20 zQwF+Kb>U)ux?_iO*BM-X-##1nJ`N9VRd+U3du{xI+|1yxO}8ZOC4wC=;@CLnpa$bL z`2zaHTFeMUfefV`SfpeK2G$cSnq{}MQij5ypG@SGEU{SmW;QOz4%8&p9EwQJ-^ zD9bR3GzpQD@JA&~6Ah+36dZw`=d%=-iDOkMrq!^xs=>$28pxcv(?l^BWGDubh9BA4 z?XiGTkSu@Yb42#C!(k8;kCWAuqnRo=1G1vsu*FMxXB4FKuiT-4Bwl7yR(FSjdM<{l zLz@^6vET-{f z#i~h#V+ZD3W)vnW7HH-A82zb}wJH^nwxVGIqD7YhYc$3XNu32CZqx)7N7?NLQ{- zt~q%}&`Zfo8iyMV{tP?v$Ci)|drVL(59~)WkU28z-Ps&jl{n1S+j<_xPl**~ji>}_ zKaC!q;rgax-4r&~J~A2OGvD09yjU2Za;n_3_1C^a# zs7!@RYPo7C=3K35wObb#el|KUs$dVN(e`#0cN{+87l^+J5g1lfv$&-Okm1OTMGlv<&HlZAe)<;wUsa(0hw_VW~$2!(Ndk3)d>(MP1gl&=74yFCGhMavhszc`qE1$IYMrcSp;~~!)?P_32{^}(;XpDRW*!NmV;vfK@y*7 zK)S@iqHFlBFI8Go<;{Lc8uyvZ0@yum&c+?xFN~{pq0Ice3#IN^UMO~FcQFr#GM_Yq z2Bs>olng=V#3nlIdL)M6^>6EZ~bi55X&!MAQ*(;@G3 zhfzO$_~@D6Xqf|Hphg05lnfvYulMT-i{TqyUQ!8XwRK-&mg zX%@$xVIHVi>%LjPXV=#hg=_(ZVtWk7%Gn-%bIVOzBgsWDauqR#Tm&KJ3Sjl0i~LsL zI(U-`%owC5-z5JbR~aAXj+g@LQaZhic^mrvN;IriKzn1N)w(DCgGt}uVy(=fLVG-z zPDZ1I?h#>?0|9SY1&_s2Hv$UIk&bxSldqAFvyBb{I_mx4{tw>!*8|n)p|44eu2GwsMZp< zuQAuB!_u8odu8qvYp6C!HvD<=K3sV$ni-rzsI{&}chCIcECAB`1S=K@oWrud`}QvS z=u)tHDVJ?uTpPI331h0CIFk5235Ha~Q$(%-BxySbOREj+WYrpwp#2riJMrkLX_#K@`Zx|s94dR*K*1#*6sw@o>GanB62Y(1>>h!(-gvrJn3t4-@*VWO(BiqX^JXzPQ za7`0`87w-BNoY7<(;i+Sj+8^P`!PyBZ{|)&vf1oH(}%T+Xyy#52W#a%^yaB15H@o;wgO%4H8$&RtL&7a;^b-hS0f7pK6|Fqfqf7buL{-4-R zH{?zljL2iQv2(II7&^NvbbGSheru&aj63i8*DA)ZwWx=q*cax6a@%%zI>8X{yAJxjfW0+RH zGpLn6Nz;C#@Z@8N*K>UAWFYp*9*;X%!tKM+tYeftomKGhoJ~6K8K&15Av$8^)i@7E z9)6?W52qZ!j1A}!88A5vF z=i=@j0BfK9k&2(2``TMRnRFyxa&o9PuR{!t2?WLie!uT?-Nu2BeL6>ORKf`j?a36~ zTuQngUW_~NCHwh&+5y&vJ?TG<{hxAI?67~Ltg}}2xYsdKX%F*3K; zjc;|H@Z0)sbsq9l47%0n_u|Tw&JW|wJvs(97}mq-e&@wldC`=Hw=DLvmFUUjq(6NKGOUvCh;!hV`SWf69FcX13sLWq z&DuRdQt@c;coIjkmDIYmH+lCvc)aqf9Uj_8wiQvt?LbY0h5mGVa$t9LC%>beJyVkC z_r3Y(_!mI;;=(8q=q-O)1*+5w6FHo7Cb^2RwmGFlzFit}ia?B~iU<^o38f z-sq!OPoI2c!^J06;~ytBV%93WG`oY|;pF7yWWr5CEw1Y6b0?OvD(1kS`Z%iUUpl|) zi)s`nC*``a7p%{5e+GmYU@ahlB4 zZBjF)0B`7jICDnSx(gA6cI&TO%k&d@-WYq;K&vO&2L8&sOcuN67;nRyRo4iLwMBQ) zU}EVIYW*3RIF@rJrKtSC`o4AR>K3g+)u26Mu9=VRDn~@JFNYqs(ss!KC4rgFRAaGg zzz&55R5*qj9XQl2Ij?Q`4vz8Z&(x$RScC_#uNKHrr}<_l%T#eR+&dt_YzvA!yzFpE zwgD)h<^XCx8UQN&MxB{9Rt^zpUIb9COpyB31V!DXfOw0(A$0#R**qu21y6h-{lRf|5N;k2D_v7x#NvHq*z4X1)6JX`f`4FYAyC=!F63|-vErng9hFKMZ2r`!xI@acG^i_R|L+5PG(I z$Grr&LqcoBLT=rH_`KsD=EK=m{)N5usABg8uLoYO?(Nnc<E)> ztqI*F9aq>p?Pb;B#-0CJUER8Iz+q |M?j=U{ui{mrcR9$D%RoUA`PUe%lm^nWLY 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 z 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 z zW>O7O<;Rv{78*l7<)kelC@128-%D;~J;~&txQL=JytLv)cW=GcbywWSawKtdxqT4C zD#qouja?tXvrd=&6R1?-SzKppW9fsL&G9|9v#@Ayst8+GOyfTBDVb!#`>UAEUkIU^ zvU!bIQF_$`XFvPM@3SfHJqP7z`n;UP(9KR2B$eKC?YYx^Dk-^TP&t$X{bGeDpifTq z^3xLmo3NPHL>Wlkug>=*0#^zoQ4zBYRvKO6ofw1NoYaq+K01T{m2GDSVC~P5D)-78 zHXsNL67#l4KhM2_r6W6ZU+=!2 z%YlD0_IpK!^%k02%&)cRbWW|GQ!y>18_9`hT8#Bd+-6G!FwqA6?qLY@WHz`JO`6Eu z<(1m+RwPWB1o^y+{rN@ur8*P@lf@wx6Jpi-$iIg&NSXct{XK{qmL=}N;S*8g8i+!&bvE`k8hS`{Q} z+$Yn1lw`5M?wHwVli$B7W1~;<=ng8b`>Z@)pC;05xM@A7+%1XLp2v%#5K%>Kyj~|HKigq0U;( zMI?h0>7C4N2?tu=GCQ5Oc$_p$C5-8#^%GgLr;Nne#u2N`dh&&~%JGknD_g9(xIOQ-j*C(fJw3mW z2tNOjO)o#@jH#rabY|=rs{+=QUY6w~C?)}?lopRg&0U?w z9?=%`l`9%afSxA$RuEI88AYworN@`zvIeUvqXW#CqA*ma60QJHtz+rI=xG9t&J8k| zGn^}Jf&>dWabBwO%G!8Ib*Fh*B3X^CFPppdxYBN<8i~>^()L{Z@}S{`VdV3SeOn3! z>-I#gs@w?YaasM9c1c|3mBwaimu7wTT}4gTS%@uj09neVTu){x;H9-d(yLsz*2k6e z5ByhGLcDZ-DP$NPKcHjb^lK(d2INb@RA?7b9q7go-x%Kv;zGF;;zq*03F2aMVt$?o zPC#7yxI$du5`}4G6AMhRI_JrgElFV~H6aH0mi(X+WG_OXcLwrIrHM0GHNJZzOH}m22F`feX=R6YVh_n2yQVnx?zvI zpaPqgw|I!+Edz6LJW?>yyVF`u+g8c%C2J|S0={sHnrDI+mTX$Fk6|Nzggao{5x%-l z(+0dv`b#lJM9s)69k%?K_!6n++%=Lla{8fZYdO-c3^{*)x7r4|kAO%gZo1jq67MUc zG4mFi)k5x`axpg#$BjO^gj}xe?k~u009mq-OvjlTMa<-sIafM&+QrI=(+w|B;EAh3 z!v&?6HEpmld#qk!adGrLr9`rIBwAZ=--fn%Kw%E!{2(TGE$s&{BAj1}*6;@6d~bmh?k5Xh~m91}&wp zb3se`AyV}+K}+e!f}kb&cvW2k%PeT=h+o1`2ulLHJl@SZ_*b{$mkRUhtNaPePhBMt zY)|&^WYV*@nXs7bX~&nDZ@GAyjm$@fx3ZRmH#jr;yRB&!{gle8qU6upGeTH}g(^%K z_q}aiC<&y^G&X)LrPR)@y~qj4EwSuf%xN_#UQl9bdVL z>0ekjTv^7b^D@uJvJ=4cy_3xhY(ASXbQL5TcbgK2D-sGXAlt$9ALM2cEu{5Hk3nw6 z3`0e%0$#PYRXy5*TNSa78(Wj0i#M?*rI3vgB4_~sDbgstV(X}*o+*;adi}_~|EHya zXa5~(;6G!tufF}T*2tvR{|1|V<<1Hn5m#v^h=+ZmW!@O0joxf^^akWN!fN01H9FM4 z&Y^wB9BcE{<2E;IwS4HZ9SjlsO!XtY!xi(Adnk+_134SssmWg^n2_Xoe$CN%Rh5vo zxD%?wI6m>phUi}0=0&I1Aoi8-w=dqyU|&L{{k1f+a@46@{dLIQkL=77EY;8BL-Wkt zgFz0|f0lW2*)SIvbO^SemGe{B_}>64c|9vfqRQ1*u9R#1g)oJ*xbIRy=5AFeSAUNm zITc_R@&3kJgHXBpjAp{MTh+bBH{}jB;H;Ww)5+tGug|$`6JlX`zW(x$@`{}<$sYT3 zaw+>1CBZddbXhz@&dhNR zSuW(UR>OBMhBMzuKEIxcjm`4;qcMj}T%#F&wVN3hD;3Yft?HaD+sbcSOV9eMDa8cV zHlwVuo+J=OJhK4yJ;Vw=b8KB$+NMu9N3hbovC7>q!#)FJ>ngNmkOy#%vpJin#~sn- z`s5PAAR+9PNIPzym#{s8-+IYP#16TLL1XMC=!AGJEpE3X(bsc1;|MW%p&5-fa3aiT zL?!`fdJOFHl3$041kTX1eS0AKsx*bl-oJ}!zK89@@i&duM_;t_cQQQ2G&k%GMl*~~ z?vj@`#2(CY_-TG8)hu-;1_B4SInEs(Qb~$skvDv)?Z}yg)Z=H=je#m~xCIp*h?3N80zetXl3cm$gg>B$AOTkAX zEDqX#tlI&kqVEQL`cTt~?GBI8UgDl!&q`iizH4yx%yHu^oOidgck#AK)BJjanBVGg zDa1V{ZsWbdFw(iZLxdJPVrj#QhCO8`xt(*BlEo~=$}YxTYuWJv+XX3&3T6HHg$@_bhRZcF4cbriG7bBK~?)I^lbZ0m=hWi;iVrnoHC5AJJ!WZsQ(hypypYdwvl@l+UhoL;#SAG>WtW1oCeT<#3yf<7F>(A55-AsDp5}~RRIiN{;n3r%dBqrFG zvy^#euiMlSG-Uds{7I$`vcfVWyWhsPIHIeB1kk%NV|)`&;T#1^AtMN5oG9i@ygJPP zOsU&zh*{wOD%&X_$6IeyzIxjv*#OAr(b7sr2?_n$YsfisiS5Shx<{b9BzgN0Rb0-1VX>Oc%7?-Q7oCGv7#tBz6Bs0^!ybaxlq0h>6(}Db+w<_*ttcqE6;zullK)UFdQyN9;DKcA14*{iLLZWR%2D$cf zZuRZzJ5GP@uC2dZ%~vbfdaIl> zZ!o6vuW!qeGc)sU{VhJ}{?bN&na%F0MkApY5hYQ|+A-K9G77&h^NSft9ppxo` zNqVG|$BmH~6VV2)jdlNJvWcYdl8A2VuHv`{ev?chM-Re6zUhV|vr`yDg!TMEb}~Or zjwCyoN$Tty{&TV_lej`^cY!*AR}%{?-YC6HRj@_$p`{+TYi9U4Wn{1_D#B4_L^|W2 z%o6Id=H9Wa6|eZstciN7cb)}iM6(% z%s3sa>wqn!>k6nGE=wBy-g*|)mF0=aruC&LoGG%!S|)1QKtjRPEm{!%V`3BzysBK9 z#HL9HC&CFM6J724mML(3%ku~Z;@#J_(hheFi01L5|9u<0aO7T+Gk{y z;eI$EJWqBJ8kWmwz47Q9eYn3*H!dqRbrxhuVbIg<6zqS=vb?22h}U1M?e%JRJ)7l= zMf=CHdR;erA2OxV&F|jY4bLC-8j?Sy0sqycE&BAQIg}ggh#wsTbxX@Sb;4SuN{(>&1y-#4e~&GHK3S4X;`9HhK*A*!RQPSPAJ4b@*+C zAGQ~+s^_t|gb7RT{qk@|{%IEoG3-5mgh7a}5X5}01d3d5d-j3T z7YncIMORtz@n?7CTH$y0tY|WA&PZeR^&1nC=dHR~FYDfVL6OuB_3=8xFW-Ws#+C_^ zNX)0J$Lp0ZhzFVYIp~g7H@L-zn(e*g@j2pp0+Dn2baa$)vaI7U?*j{*EFyej7d|m{ zLI>-9jK_`hz4m7Fu-zJW+OI)h;O$HC%<*?M+sa!K8!D@@p7z1|(Ae zS8z0RvY)_2nE5pcgAcD+%bS2-rlSCuhAjQ{FGElkr$~OeF+5W9v_my9$oTkG-}dz| znyyH=WtKsH!Mg1o(AfyZkKo4*0wWowH(|pQ-F2HHb@*UKu`}N&VDD{5#LbQgAN78Md>iGI{=*=1DJnn%o@#Z&ZIvn+sz@w~s(*84h zTYI{k3v~bg6?J8qjJGufCtJe~`ocANhNX@%jxp zci+#?xVUVb$fKTYK(aTwpyM({s~6+$u(97p=)ycFcDdfm`jKP_Z@jGkGUhMtvOe+2 z)y~VhT>Ycj8jlYo=DhJzLd*xPdhg}-@xe~@S))E^p=`7;68p}ck$>P>!>cm*1d>)| zG&sNLyqtN~sB-c`lO}RocB^vL)IeJ-g%@cJ%4-_LL5t?A#|JvZlcqr6M53nkx%r}= z$jpB`sQ=E(<>rf-i-S&Yuh9`%l0KSTE{=w#;@YkHZwFV^3;(#pleKcjj-!jU^usCaSM@8J@jR%Q)Ce zn$!c$$TRf8%e9s8YP+_xWT~13<#ez|&jrqJxe~l!CTGv;@A!hUxCUha_y!6`%t0J*IOPkNc%v?>-eb1IaXA*a~@Hm8JL2anxORFo9 zZbI3uCiWP4(28j9jsZaYpTaO4s8(p9Gj2N&;X&={o>>z8|LhseM2tG3kZUV<#U^0Z zz^bc=&%`R##BrhUeXsC+zvuCI?X7LSk9)TD-Zbh0{OXS_-Fns6d-b1XYyQ!yzuudX ztjY$@O7+v8zVSPIt0B2Pe{R*^H0E^JL*2IBrfl1fd$MgmwCZbbx4X@q>gR6#1KajP zylp>sReAs1-TtE`A^1PruKlq!^SMj+G#zhOD@Gw_|ReoRf>0;3e@#|LE2vh zsMVMH`w#v7)49AKoO@uObqVm%8r}1HgeXqtJL?jGaonB>UxY5XZ{*uG+y0hD0EOKo zIHxTRzoEt9QNlr4m+Wn_$sTcI8+wuM`X1>QIfY&7^m_eL(^(c< zZk4({l2|8DRg}}=9EOO@sANNsC)mlT8a}T9z{sk8&gB(6IO4!bzs{7~Wa4BCUmO#% zL_ot*?;N8w8*d`c;N#^$(a@dILh&XZLz%aFt~4#X4yv@h#{#(V+-I4hAkDA6DP)W=gi&CrPtC+fUBgo!PkBRb?jWAuA_6(jz+lv(`-!C_)p*9f-SXe{KtD zvI%te)6(n+N1|8@FEFc!lXQvwYj2W`7t&=RXwO11Hq}^B} zRzTBL*mtRH1+>qCsigu1;Zr|tuCK)qbd+6sa8&)w5%hC8WDQAX0CMP=U)rl!^7(Cf zY&G$j(CYn>O5H8LLF{+-+9eA|J4R~~Oa5!4Rpl@yAmZ(ZY80SG^ z5`r&vNLp_ZFwKN42H@j9nY1NjES*8bs%5V!9?Sv8WDel^;e@fk)zYQ@*hrg+5HHE3 zZ{Pgw$5r#e-MLeS(>{OB)_?-jOtsU@HULTfXwKRT2Udpc>cDu6X&QixXWHx>fH<1O z+2BEanCRhHXGxspudUIfR={g+Fez#Vq%b->dT~|OAAv!Ul>8bdDwnS z+NXX>w@?A^+{um#$D$t?3lPy{na$9YWgy`1YBPQL&D#54x5`U8Mb)F>nPt7gpj?TR z;@?PkwO%%r_}Q;7-}v9F@1C!|bnn-n|Frsowp5FL@oeq4XPfTf&8rQvZup0HKbDt_ z;h`J2A2lOkgPuRXZnT=%yfj9~hxu{_P~hEcDhv4?#r+-k$~DjH_r$t;X`yEKsvq8Q zDZ^;*5}w5ovqtA$^(WpO50myKVjYI+(LI16**yn6@w6wLH|z~iGoq(C#O%Sd1Ikva zuZoGW%KNVf-5!A?9D1KGgG+0@82v>;7NbDm@?nv2^T6QdE0l=e@x;3b8SE5>ur%4p z-TfIbO{_hv?B(TrRh|;!WA%yd$LDp(_=WMB_rhvk7=vPm3Y}kC>#^yKp=DkoHKHQ%nSGD{<;%2px*)`0=oHP09$k3Zjy;XuS>%EU|_ppva2!xHvM(Z@?SM&{G zo;OUX!hgE(z=aAgkWa=Wi{6y{r&32~v(XsY7`DS=5|tr15RqFyy;E1>p27q3wAZA)zcy@4d%xzjukWx_3@Rsx^xv=Sez1dL&jtjsUH>av9R z`dSkHyTzThRoKMjmknVHz@g~db7hR&Z&LK}EzCpn5k_s=^3$1;&k6^Q;~wp+cIdtz z6nwW%U4fH=0@AVje4f4%cR~ARCa{@ zwUY7K^<N^ds4n?(-*|VsmzMcj@7sbW;5(slQ6@(#1>Z zrFuH}@@CZ7TD{_CW-a+X+P51bWOM#eVsGvHtD^6Cbtkv;2N{Yud^N2+u#2|!n#?Tz z7H{u;qzwc?DIyxBBU|iKw--+b$kGGIOoqvOD(84%Lzt$JJ&~1waE96l`{rGi+*lw4 z+)gCq0;;v_c$m-_;W4G8H#betb_|A4d^PGy(NvcAAJNDiYxQr22vbU9IuTjR%hDzq zK!unhV==p}i_G*QwVRm=WbEwd(TlO7Ot=LR{2+`!3nCw9pM{YxntkibP6eH>HnqS` zfxO?!EU(h_EOoBkn^*PUBqh$J024N38nQst4H3M_-Fi(vDTNz=hZ1e$v5GiW1$Zlx zR}8=~MmiAAh&qkd?Bh1QVk0SpFW?x&a26@e$T37D%g`Xhv@lU@qP-^VNqNYt@VO58 zR76^mQ$R{qTg~VNm&e%22kwA@SkG|?f3(r=bG8DOB+At|DlG+ird_vKb7}2|1lGY0 zfhGzYMI_V{7GZLg3yDo7Pl8+rVuvqz;TjpBT(DchqqHzFKS{aUY{RQ3zL@<;t#JTh zM&``&aD|?zCal>HY)vb}gItj#C3F{qr8}gx#!(Yw87GWI`vqQnQM6tn0)YeB#R{XG z_~J$h=FH_x7;&B4r>K&_N{tlWoHDWGqOYPfa8bIRsgpG4Gb}T&+`2za63Jni9kZ?! zRVA-@u7CfD74bHk{UXe^jnfnwNuF?QN6Qtlbnv64*^O;R`YrJpEE~2PxULO!MXW37; zNX9AKct(;ufj1Xotp!RXXP0Zx5bT#_54{^peurMyo0d`oOBlp2?_>}jm1ms2a~$E5 zVoj6^iX5?bA-u@Hv$M#&An4TH(`* z=1iX5jU?wkiPBGF@7j9hT?k;wJ>oMU{U%o``3acaR!(hK%iNhI2R`L*3uTjE8u+8{ z^jG}WQdBtKsa$1JmK$C9qux(KgHLjR3S<@6taZ0#7(#XKsJXi27@(EiBe?O8H^*nO+*Jr?zBHJeM0kU zZ&a$cTA;rSeO~bh1V1@fUVogyAV(KTi7c)sDC9o-^oqED==QnrCYwE743b$|py5A3 zcOM~(SPBJUfHSrB0!iT5-~I&yRq7&3EHmSOV&n<_uQ2TxC~6B6=+Tk>mZsU?nTn2u z#Pwtvj8Uq{Ru8lw>`^gTIODKJH~I(vHhUzD9l`z!1Vge*FfGnvqFJNisM*zrDb19* z67Gd5r*A=j2`+?Br z|A@z+X!`8JN%9k&3sbWS;W4%rpWg`V85v517ae?7ehRN`QDWgO_es;RSrcALX7y6w zd#6P|Yb9Kbh6+{+KZr$O5q_bTsqkB`i8nDxMDTSoAuNw-jscLg^;`trNr+4TKd_lGpWnHIK<{N+6#9bF?lXhmJ2xp#!o$&N&noCU`!g5- zW%~ziZIc;yi>G6A0UFU<;I9moHj9VqYzmVf)4A5rU7psXDLGAo$*FgJUGA8zR2gbv zFJ7$1K|>MdG+T3IJte~NvSefpk&@HN$T`e{kq%X5E0EeF#~LC-fSO=Qs;r;|(rjj* z`5}vPm;2&WrFbJBMif1Zz_L-;2qCB=h3R}@zO?cyE>Ol*JnG>{c7f8R@3Ma8@0Lon zl5epzXnz_fc$cfCy)h{ix}EdVe2H_NLktFUb~BZ8Da%FbsVn(E>3-|5UGmg}zIt*Y zbF0)CmM~$U3!u#~!el58?^2`WnYE1Ob;ECCrNKf-q8*-!Y16R#8)4Z#Yd0}^m%elD zeka#>{ik5P)l)mxmjHRgz6(IdRnM|?rSF`3%HZdw;Ne#w_60Q0ZmRzjcA?q-BhdR& z?>B~DXvjk_FJTrDrOE}En|{!$R?mm{q_Fnlf7okW*e#msx{ni?sgy2C+x=eWoG7sl z8@<)uDP19lnIm~3Ms2)d;dHHy`Wea)Gz8!H{ziQyu}JIqtR&j-aHGB@no=L(aAOAL zxHWFJF)|!))NdVb+_|%U{`B#p{Q2R@!t!_{4W*kF#@64sygc3@l+Btd3}tJXWW_d~ z2xXgi@$~xGw%ZzewTBI&AhzxHMtzkp)#n@aKB`&u^G5wA32Hmrw;;Dy8};Ah`PYs5 zu9W-jynf2x<=a3)(}iXt)?~P;CbI466WrW556u6SO-#&#ctOl3?cLDbWIMr}=Ome) z9O^U&^$He4A&WF5E&}!Xx$B;3S}M()?IRTk^9!9u^2c-EDFdLOuvEH71ypV&O^T)M45cL1y8d$OaJvZkx{g!NJ&_9DKYLQf^zV5wZ6WQ@I1sBFVEpyL&(IB zHjl$>c?PR)Xr#ha^*F4-36~ke9BXo$fo&9lZ=Erl1gWKFkLHKQB zW^rifcYkz0O{hcW{Fr)c;<)vFSsn}nu-x{sOwn)Kh zMn^5bA5lYutk~<)Nkm|5|7)*Mlf?H5EuaQ;NeG9N5M>x?QYQ{VN76m( zH(S$_*8U|B(q%=O*VdVb(JG>@7@H$k3^*oNJSw@;kR*xeLcl@J(VqPtEpsCx<{REI zLLrw*03bxrIwNl~W*kV`E#!vxo~ZFN5SRMACd(0cb7vvTzic(|*mu^Tqe6S6C7ylM z(M-!Zlnw#qExK^QJkkb^e8zKV)>s7gsZi-K4)I$-^F4vZ-%rk9-1_t4 z(npU>xM1BW20V!4h!M4t?NzvpuY}J(bS{4X@!CvlZDz2>cx;t~lDbAJgqTQ30Aw6* ze{K0)@&l!ua*nV4ZsnzSn@RoccO;-U7;h$ZO4{BK$avS@292Ge$wLhZU!B2skAq&I zx0#~mojV(@)lEJKz}DOX$~BO+NNdPR`9sJOIZ>-JW`3GdQN2{BLHtT4c=P1uJ zggFq{FlXxBxsP*b!pnq(xSZtoG~duLk=z1%jlt~9+}+AOMKQ2g zJ7Nd2w`)6r(%k#&_q#K5m3s$i4?v?saYw7zFqtcRAVtofPv5$g+zGDHVB97^EZHWL zofzPwC^T-K;)qxHR*SxsY*oTHUoSZLtv%mYFw+rKSS8-plPF9`%N|6f)uHePH-TFc z-NirI(7l3Jak!ojS<6?8%Up&2&NcSw+Wx{#1B8BMN1}x2P0tc;{c>*d%}LYubmxwX zex9_zd;gOk2ZtJdIfq~PaOpH2L!(hZXPTe#vFb*E4=sh`pK8awU?1|;D3oClP6wN_ z?9rgzLyyv+KQ91MbDqX^KZ4~j3-K$6QAPL$r}su}r(MpJTgeG3CI*i*L4%w=EvE=A zr`={PP!Rk^kPshlbjL3utM-J*Of$$zMX4Dr=ogbAo8TB1+6(dWskAFV2)4*KJzbiQ z8UK}swM6Bb%Tu=%{fSl7d>Xy(rly?AivVm_FcVB}bF(vlY#txBxUZe)F=aT6sK~dO z7DfikS7fh6Z~yq3=Kd(CD9yR!oNj>eJm|e~MrOA&eDCY-<~IfpZppYfS#DEjac0VA ztPgK}O4;m}sAmrW9FCce|0wGC{01GLPwkj5QO6!uX~(^;9^zl+R6u6gr%o<6W!iUB za4o$9ztU+46{acm<9>h;yV$r70w#U=NBZ)Qs(`cz*q4{-2iSA7{aZ3jP!G@>_z-pc zsyBGXV%bWB)^ZR`TT_484O*L+^I4By0_n z{s~zKNRUIE5E_!}LB55z!(0yz6^Jq*uxKETh!hR%WxF$tDWYz1T}{xCYZ&~4}uqwba&dG5hQP587LK^2ueW3qa-OAmBsM8|FX~e? z(0KP`1{#0(rh&!{OdTlXuyCNf=l|SZXfd}bJHV5sZDuRQ4%)3}a3Wl>GI6mupeIT1 zQb4xKL_slFODZw71WqmFDU5{HQwa4?j`RIK5SehW+L_+ER+M;8>jNDg`@J3q7hGv~ z^mKxb>I{{ItpaCaWkm1(9+So_mV)KUVV;RKl|p<;h8rWUb4QJV!SVBT zy92DnBC1~k`+C!r13c~4%0V(qUmPH?zLq#H@D(i~T!xlr4-#rCFZjxpwwWu0>IX@n z<*VJ+%PB}K3J$YHRA*yGa{Je;^_SV6VgpkYL%Y*PvLQZ3%S1~l3u~0}5?OM6aS0Zr zaIfp;r!{zCocFzvQ>8v{T58@%6T5i*23>O6h&a#Dzm`w18sDgW64ES=e)rBzBd|GCce+@p}IJG|Dy*EwIUs@uzvKt zuzpTmVF)ktL&z41TZ#7>w|gD8N!Ap`l~J^c+Rf@>al4NmR7v{UkOO%Vs;$WcGMq}L z`qOy|syruigOKC|_=2zUpHt#vwlFMNA!K%%J9(Kz${v(o&s+|EZu_wRR+ zA#7hRvv+1TwRUBrd^09A@yLit+KkJSHKEqFV}}9Ee0l|PB2d8nwfWKNFrE7 zbr%;GwKme=L`gF~Uqm8BBnTiTVR_#lVe3RRl3$#(upeBIk_kIPHgmd6{nG7dJ7 zIXtLLN~+4=Q1^ok@S3`BDNe|_f)yhzU=1Nn-cpX)RZu&fB>`eFp^>wKj?gG)s#IfR zw0)8Wi2p|G(vnnp)|4BIlD(vpjLwCgCPag^#b0hE{3BX!OX7A>_!i_EFF zE_e&InJ!LE+mORBE;LVP;a;zHY_{}-Wp{AC*|r-6X3BGA%W;v)4V+b0#l-jpnZVZpUsc zbu|v)u-D<-rf))ScrdGfcB!gGixSm$EgDIFM$!TwQWZ4LuUPDBTAA99pTLbEIN3p1 z4-qP{;hQb6?NBram3F5P99rjY5ugrxgXFX95x?r%i#P53Q@w~*DQO{yh`_0+QQ+A{ zyq+B!K4!N7Q1zs^&?S|_;Tx27_D?D9XV=he8Bb`wP`7}%e4$1}P{NXZp-L!MM2NmnH(Keb#2_r?7uuR!#ZYr< z+pDp~zG&$t8-=)-{}bY*Vxvi#geOC50xUAJrhdv4cTHs1mchhM-fV;Ug`ZY3KWE#b zlMQ@*ZU6>j?|u^0fs-Nl0a`BGgk3q+WpQ%LT)NS`6cvqKnw7egG|bszx|pwZ^2ytd*DU90k{Qr(Umh}E()T$h6lduQ@kv4+cuvtY6_67rxf_PVj# zSZw7OrnX7EJZ>K9fm=YW50)xGf2z^{ALxvnJN?mkpwZ)f$whpkISgb34uq4Vj@=G1 zge08hi`B5ciMt}Mr@6oqX?^}vCwS;LKojG=xVvVFT# zhVI@<(A?m8T_Lwcg}vWoNHAixk1F7fa|u6m>arVRsNd-H^<{T`$j0=@?Tf z%})F3hc)ec7}vA{GB7oz5?v54g@8;+(rdNN%Ymc9&uZ+<06SeogSlY|6lBG2OJ6G2 zF!MXYml4?=MAZIIi~vyscgy`V4tj{7b4>-;!`O=JYzQidhntScR3Dp^4ckdg8P6U^ zthj&G7EHgNOmF6@Nd=eFtaRUU-Jc+(>Rb zQ6t&tLW6=A9OAe`8Q&RKJ}Q`|6u`f!pB)G?)vv2Z$j8kZ7T4KCpr9@w0UT-9Iq2GL zi9z626vQ`6#6?;q2cY6A*C0UJQm*qmcfgHCLYzhhHd*vYYv;ys=Z<2|EQux4GI3m$ zILB-nvF|{<*cRfN)`|u7>uO2ZXId-n3t7&#sO`AoLejKu!>EzC>(GeXg=s5ssaz|? z@zT~q!M894yDfH00<6@{uCAB}FjKx$UpZ|aq$#@FJ)>G_Jv~5m)E{O|uwDB}0Q0^oOd*fH-23SkTmFD5@XG!rin2#s<_x2K2GidV!rs9>I0!JESQ zT0mboH>E<<0XWe09K8NHIHxgl+OHQ2^8JMsB~~s-HDOf);3X>X=Hp-22o}W@j{YL= z8O?TTp9*(h3Q~(a$}9MdIqa*{70%KjJdSJZaRf?WAs9oHrbOY%POW^;p|oXQe@mAH z8hJnT3M@Hpc8SEgbP-4f*xnt&RgObPE0$qS!aWu^6+-Anl2;(Sp+QH1QH($q)(A}_Nv0O{#fh40Y?@X5mqE(s8o(WkAl6Y8Q z6);Q`^|5ohGet(002YpWjYhrWsJEQV5cigq&%V%`VssRu^vX`Vq!1ClRfx_hw~qLV zXnyG^qYvyzcw^B)vRpd>CZe_G+e$x9Z!fdWa*_DiU6i&aT}fb#32|X3r3L8jdNcW< z><^A{dz-ILeM)_IcEiXt=Zl9H0%1)UIIbPHkqjO4iW%^-2dG%G98f)xxzQx@vnZ&1 z?F?d+iq8jqD^6u#e{IjXS$_-ob?_N*-eA}nlqm2yMb@LN zSkgt14J#mGiMA@S`k3)7P~ZLd5J5+3rSY)H5Fu_q-C9z{D74AA`b8?a0}}LPn2^Do zA}5$uLV{60*rWwjP23hX9{SJ_ZTQQS(h@bUWwUYP`l>KW)uoI;h$(2&na#C!9J@Vz z5W&E1qsUi+Uu!g0bnolbp*e&r9JN@1Jsxd^V>lCtBm%Xw%^NS4ri9|Ss|0rI2GofzYOwIV1je;r^iBS-7HynM@QUzkXHk%RS za~828T_YV!B&&<9EbJ!jPBHZlk-yW&4?I>eQ;}|cer2|{RI@WHZ0*?^ax>>vke202 z3d|bb$X+@jRjuf$)RS`h8-oIgG%jQ@&l809Svg))v|OA0HYx1*I4SWd`QiHK8L~bQ zu6~|u3&Pu(rOa-|@WoeE^;pp&hrkJ5E=xVZILDF|!^XBMW4rsGjEzJdjBWMWv8{f2 zY|96dBVMU2IQPPwhAuocKpHT+@JL-T2eH+TuVw`MX0-z1WGjaQ8Gd!?GfH)&V~ekm zi3bvztd^ISW>4FDN69FSur62P474g+2T!T*RW8E{F$(@i2+|d)mMXKy!&$Fc%&MsS zEsjvg%DS?sOR&@SA5eIUsZtlcTJmWkyHk^0t zxzXfb$GHgu_Z4%A_ocdeIsavp(J<5sXIj#{0lH%%RWJ7?D<$SW=1z+ETm-tk#**%# zn~8&H;`&fg~hrcJ(b*JM~S{dzbZCcsgho|D8l-Pa`&K?(* zefYt@y~|g%JI9~@&`O*o$h_1#a=?HpW@Xlyg;?HrBq||0!G_|Rn6N#(yp-OQOkyM@ z&+pL3sOy*G-si&C!`xeczTmo;dpf!{++O&v1PgI$t5dlDeC+To;J$e-k1miv?@(QAMF4b~Nx2%;rhTV&k$o4zQ! zYbaxHwwqlR1cpeRw`P{FL1M$g5c)3?-i0pRg>|Bh)1 zPmVw73p>BZVX1@olh>iJt_6TO^Z(NQhtWH;`Qt`&=hT+C_u*<8lnm$dG+x48X=oqx zgp6H{;6#T^LJu!i6mrLc%N3M(xN|qzR>S?cg$%*&Cb_Qm#D=vi*lj0`a&Fxq238BH&F^8*0oO$uDDArne1BO7)Kel6h74B*oTT!#6gR zuo+zeo3!ULDYbLvP)pb6KWDg#wu%WUNkcH`G=YQPPFM_fPN)Z5>|!A9#Gg9_(J z@qGImr14W1lpyq}EJ(#(A}PSFP{RN4a&oI@!iP(xCLxVmcFNNjXCVkt@_jo6HigM6 zN2ae+*cFr3jxTNr!>k;p( z+bzprLY~VR-W)cD*KD7a$xTB`ckToL`X7Epgq!L4H7(}$7H8qvTardC+FMK4PJ_Z< zM-9vnFIJ3erXzSYl^W2(O(v$r+{6YtF(i(7Vis<9?d&w@Ohn}yFVpq&k{zY4^%S-4EUV5FRHy>8!!QERzO zZ5cIh(^2bkI6}@xFWK!im*X?1+D0JLWYzX;rqo`I5dC#Sc5?LT{E}lFlJVI01GGWPq}%L_p&L>IpP8kt)neW8@gAYf z*_V609(tz4S7qDm?R~^!1m#X7bE=FooyH5j)-D)#d>5`9YDfAenpj#t5wIa6Maj)e zuLer)s~UXk*Wi0ttN;t|+^MEin|raj@oIN#{pZy+B!6Xrui;6OSLUMpVOG9w^^OP^ zBTp#tvcqKAjit>jqOm|XG1|(7@OU3(krHn9Vx!QgUV;j5Q!#&5sCeyKw4_K{rR%n!+@UJN(v+*1+R`&ux!UQDNJfNJ zNcz=uOxz+WCJCw(xxbxj-*ybU)&%~-l4786J3UH*aF<({+*{MXu(DrJRCd%}`;e>M z!SV9&xYrvUvXjzT4*e|4!?ql!FNevJWZN&mKo@{PO$aLjdJSlEGqu{x;6idKLt^IS zZ`Jt;mbQ1uE$o(gsEty>eXL-zurah-mVVP6&(}+r_-GlDNj6*G24j%6aEthK(IhPn z`{tR@UO{~HrgQ-um4m6oRL+aktV=e+9aNaGZVPwGg_@Kl332Y7TEd}jBCd!Nx%@Tt zqNVA&{jWSA%x;iCm;DaDb<5`r1}YJ`8>SNnaLzg<)H`n88e;XbrEy`Y^`?Eyp8!8@ zahHD6UE0&-qxDSrsTiVnYpBD}5i72xcE7kxccPh0qv2j5<*{k4%y#{lLd%Y)uv52p z0zQT4vh;m3uX74x}EK8h)noyf-mF2RDZUW`uJ`qXT zO}@peh#(;|dZ9!SJL~CJTt*VhZHo$gHh0i61z3vgJ-2_uwiTKEU85h`^LsZyg0$iz zve7dr5kWByUS9GWos!>rc+3IVc-#JsWX7b>|6 zB`|%IzDv6O@#wqs4y@zxa8%lBbJvt_Lf6Nd)@!fadqg>w1jt)jCm4j?&rSaWK?-)R4F^PL&VJJC%w_I6N;y>!gOeAcqRW-k8+GicNYwK)!&q;AJV%2K*Hx zCh(@%eIVkBl5)p^-N16#O^*|tsr>!6upVq_6?hEjo3zNMV?QMLB;ABT%_=9u*`%vQ z%P3}=V|p5`P<%aO#m?Ranu#1ti1v?7*Y zCLPhJ*WH%wUgsv~<75Po!-|-&2C+2Q)9z3r z3m6@U95{Ozs&7K}p4fm~&`U^lyDS()zf1{cpR=BT)5t7=N8|)Ms@R385*-*gVl3T3 ztCc_^;-NZFe$rItW)5Jk?821Q`o&47(bXF|mXOHhxbG6zg|ymB+vTLSJLE2F{fKhq z9_<&=t*U0(llUa;7fLO+L=U1m?hUtBiyUIjx^u;NgK(2@kDe8eJ9Z`h(O%QZ(6Kzk z9sVN|3d4=&FCCTX(034b`i*d#+Kqnj9Z9F^EQ;3itCr`6-DokLiL9sV;RZ{Zhf#L$ zX{-9H2#Ye4p>Ow`0k9ol*Cx{C506uEIY!n@H`&2(uexQi{wMm3n~q?UpZN}VeO1Tt z#@%J3FO|bS#---B^llq|6Mu?sRe2jU6mcP=x_Lru!Wx8F$f6Qq$PyGyRL87H>>>u; zl`lf?VzE7@Q%FgvCW3B+lxJ!QmQH&Tngdyl$Z#t!RlLUD&lSLv^{vy7`E~BLv6>a+j7C4o8LBl50f244<}f|%mxXDxaO*aN-Je&DxBQc z5C9QQQzjJ@4jFC*5dQtI+ho?g`nxJ;7e(LpPz)u7>Sar+KIUXI3m?l?vVnO2xE>S8 zHeg+)3~PUqVZ}w#y-gU+l-E8MGw(t1_iDg8@G zezVta?+Ty&yHW{Df;d#fUhuITV27Kj)Srf;k+yd@G0yGAYPGXk-Cy0Qa05%_%9{_l zNTJam!rHsuqVTTQYIN%Tp?`kg>x*l+{wloL>TwHTy&XOGI-{gt|I|$_%qMF_5{I*$ z?+s_|L`|LK4O+L1`Me_H-lwj}fErbR%SAlWVl~VdG4^iQ^s!7NG~8DMaJrPgYDX%9 zisb6IU-`I2{BZ|YdD&|K>`oK?PaA`-*sn3OkSjke7hTvi9BV$uvXk7pOyb50??zDKO3g+2bQJ(l(kW)_Tej-Dd);j1^x=j@Rm1~FS;%V)m>Gi z4&?&TMzPRUBo296`zGHzW#*7`(E=i` ziBS!?7QhDt<<_5|2hQxzMiwYZswI0KT z*?h4b3ENcu6&Dpqymnkl&TED#2wXB>F$7kHG{dc)*JB3-yiivl`%OD+Q} zagBVlH)uhcXVEZFm{Tq{lFQ6ep(R+~R9kE(S<6FYRDjdtG82P&Y8x<)sOACttoKd| z#WhgChCFPn9~Qk})ahXTHWiD=R1H-m2%sj@h#dvHg*>+0m`jcu2klu=x0Cj1WIOo# z_62c`hjRnsLR&LVx=^a42OaM{s9g2W{>Hdn$8=jxjt|P!i{YRt2KB>1d!O6$n2wp& zj~i!N$CD}qOQ|FK+PtCa65VbiLN3cXzf#NOMH79vcl<&^6PM^98s==@L>P(bOE@uv zP~n2xL^RVVNAoMHlQhDV{-hXnvR2(&tDdh_ch{<`YfhL7sC6uov{3~^@aA|R_Vq1A zP?P4JUORQIu z80y9_S(-jDtl(BJs^u&%g4a`>l(wnVim4VWZc23X6>E;Ig5LBho@_xeZcKtzwJ~AU zpF+reJQfx$(#}-W`X=X*B?{OY)a{g=WV8+^PT`9E{knU&SYyV1G|og0rWvt^stIXt zwXPUZLgEmesiY=ik(3b##?2AtVv1f2#{|XB{QU~gGg2Hv0O-b*t<7EspJ2(6NZiy{ z)!U8w-fBwAPR&^?c4K39Vs-PO6p@+zwmMgh#5=B&)dWTVBv!TyJOua4&h@@MU)BHL zQ3h_?!l;5u5iyw|;`>br~NFWVbl1)QYf?N$P^OJsoiefRr29Xt;x9+~#nm0C! zL0hCzLKWIvUa(Ms9rfixt)~i5}d%sexJz^c>*S%zWVTbc7`(^j}-u;IU zcU0&G4PIu4<_e7;Ik5&luv z3qkjGxlC9bE(?z;)y=hfx%TP)V*BuLuT<;KFAfim9+zrIom%^7f48)I-g)qO*zJ{S zk3KK9laqx~t$%;9bJ%}U+TCj{emE}S*mol@=K^Wx`^pXW=v3#U&y56<|sx9>)H)LuBzz8(w)M|%vTw(#-s<45P8 zOS_LAJUZSx?38x9gO7s;GQv*V-qgAakK2bwd!^mu#}D@xYu(cBr;iJJpZ7nPYKset z=jR8fj3;^c`Oz6OtsOnwe{#kMAI>j6cu+g$$LL|_^kBcV+i2e(9ZL1lL2Z1{JSo*W zefQ&0yY^t*r_;lS4+p1@Dbqb)Ja3%v<8h-_JKJNZ$>D~(cV4PByNjPYt!8O=A8cujX}SAgalZNFQ)zc`fAMny9@q93ADkRL zegIS-FyX?s!2I~Idvr>f zPRriZ77iCbKRP|5R%7w=eLPCLomy@2?0{N(!-tK*N8oe#@j>_WXt7lLG;%)<+K)~f z<43^oBV%sLQub@@R+G6O4{M*BXCUN*g~j3Fks##A0rT-g@DSH%t^*PYZ!IQ$+PVK=)I6f_lk-|Dxlb3Lll$Yw9-uove>~_e z9s~=t(jbIN#q}-0M8xN8{+>`RTl@`{JXM z2QtFZ-jkyz9R@$7_v8_MHRtJcp58kLPv(1{y7a}SesW3|_ZOaY_Zkmb!B3A)53m~S z)&xF7R%g^*_T_c{5droiu?;jH?LS(4yl`0BJ?TA81`n9d-bfZ#8f-#A9x&9y!GkvB z8dN{AAG?Q5aIAg5w0pW(Yag5d-r>jlgWAy%EjOP$K9`+)@~Ph%3eIdycvC5 zJpZ&$?@8nS#|HzT`uJ>duO?(?zq#0HJz!t7KMAT2O1lp~3aw+A+R&fQBY=6_dQ{v0 zd|dW^>k{Wx?I!pc5u}hD&~u#JcfJZ;p2L}GK68}})P9}+uu!QW+H4}FbPs0Wyy7-2 zmxGE5^PA@m!yg{aqG37iFih^=C79!GoeaP}f+I;pS*fEUVc)sU;ZCgBsLOf{dr%!r z=+PlM*(kQId-Wbpw7Iqoi(*1MA`9Ju+2nbd>sKu1L`waweCK(i9^eoOP5M(BGto$- zW+brS>#we_#lHT(|4oxfOy!yF#_az5?BY(Pj8t`%KCaxxK#!rT`tl1EFYaIE>dKo? z{Y9?&;9#$@^4DK8fBiN5o_|_*EAq<*Q_a2$ui<9Jg zgvd(u-I`ksk?1Mdu%0HR!~5q0h|njo}vHSQGsc7)l7}zEJ~99S$H&mm;WmA zY9up=<`~AL*H8T<)MCggidKgu;!L!Gix?6a)IBwFZmG~38}p#{QT~l8i}#<9YxG|2 z{@mjIC0&hv?{o$U`tIC3C~&VPf2#NAFpP+}On@UxOpIgQ^f2Gr-2K}9N8g))=I-3% z+Qa+ufMP*2kMHXP(tRquuGvIfRF_Tb>0IscVp`;LCOXzCtd##XKR-_{w14gI@0XQg z?5@_2*3u~-NyE@l)mg=n3BIZ!1~k;a;GKsLR)zuQzZo?Oq{_0=Pc7@}l1>EhMYW_| zaE1hgL!4Bg)SWvW@m2|9M#Y5gL9^T>5uqg<-SPuev9##ftf6JEp*1<{B$yVyNN&0| zO%vrPdriAy7I6o@N!4HIp8-dDe_T@YJL;rvVV2d58JeoYFQ5m{zE2P#7I8mxf+2|@ zar3~)Ylp05&+xEMgvTK}hu^v&NBi}+-I?3iaBOO;O;T4Zr^l#_&0RR{ov-zBIJDqi z;u0HkH(vO#VlU9H`@O+vh(5!K7*`gDI}SL?`q^^!!QRa^j$14Cb7p%-c&o1yce=9L zoZIXq$95>%nr5kTudXW2SePE`^5L!$gKam5k_CwC_1$uQiiN|*9vU5X-lMIh1cZhMHwRaS-5*1R~v=Da5^ zb#?(B>4U?Lq@9xovOhICttJ?rA~GW0PfgS^n}OdtDLrNRQOP|b?0>`Q7J@%dm+V7cU|pj5R^ zT-#SN!y<_IEqlo&Igji4v9ecF4*&%~XMtc}o7-%{wUSYf~I$%1d-Y zsv=idClio%>QwwD1CCJ!U7l2qVS%EzJl(2ANAS!mBMS*RhDsq8%rCc+9|9Nf5_?M| z_WfkLC86A&dB~Gyi9(3rn8Lm!Olk_tK%B^MHF3oO^J3<}2jvW6b7BdMOOd4+{3lF} zng=wQ|Ddp$w$}?lCCtzydVe$yo0(@YjbyvSyy^^I)fRyVqayEv;0YeyA6QtYx{yif zt|5_TklG#t0vc~I#+EkY|7vrt2W~=LsE z-z`^WR&H(o`-h$HpYre1o$r6RhpHf2g(^J2>sSo!?Dc4xx>$@m{)&T?h$R5cwe7N3 zLp1_tQ@yU@<}c+al&XaYi>#5yL7)aP^R_yO&UZhMW&g-uN&B5pHzGA>NUrr=`RR8R z|BM7t{^6b!eJW8`SrQ+?$RKZH@V(sIem48(U*q}t7xT07=j9{)_lZ5M?qDwrEeHXy zm5^Mcu%%3N7XGy$WiWeI5X?|0{3=N_EQE^<8cn}ekrQgp^G$G;+uW78h}YpD@B@&G z?yS^mQkRib z(H}Ssa5dtlkvH;JUl>u`diPeFmLR&Ry+?k6i})4IG& zUrIRA)EJdsACNByi0CHbH~E-#%N0BjrDCx4NiuE@aBIB)&A_vo!V*Y(h^aj10ER5(;UKT0d8-VlPGUFNFddn3 zc&I82xL6$&%}h|;6`Oi`?xi4GK%Z!;@S8ww;Y;(vArubBh(B(HeR#hm_R-7+1oZp> zjdA1{`NEN7h~+rh#*ZTO4|**L57s1p5aE3N{nn2g>%YF|B%e4Ygu+4vFZ6aOVcPm5 zK9Pq4%zz~SAFD2oWb67c`QadReHgkvk<5JcgJc`EdHH;z>o@CbvhPpa^!f;NwyOj* zoXjMwB!rUM`jg3zG&V6J-nqlG>=H-+5%0>wa<&*V&?Y-Bp9A=5P$L&qaHF z<$&QRmb@#)*Z<0R^C*=8TZ&Oxf^_SaY`ta0Z~`um&7)U+){(_w$5n~S%8qorF* zM&U?LW{*Xqi?*KNtA^rP0;xcE1edEC`ysC^2&6e2$|Bn~Oq+`+(NuVZJskSW6Dc5Q zJGk9?g=gP>r*PK{m=oxi4Bw4Agt>Ow=~iQNBU*ZaDy?FW3R~O-Q-4|9y2W zJ6YRAb~j~DMyp$ka>{C|W&ML>A;KA`pqQbQRv+Eoh$buo3MWiBA*R;=3&DUQOdGmI zuE|m1c1z>G*`2eJWraEfwYiBw9m23(l(>yfn~gh1_zfezW>wO)V6^Us#j4ADERE= zD{)T(IH+g}1R>4`7NaHKpo#NFJC{$AA0VA>EGJzs~mNM_;U{O~x ze4Rb!ftyZ0v(qnxNi898L?q4S-W9f9%%wO%qBP?FeGKⓈU%C+K64p82u`?7sw-j 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%0DP6{hZ}} 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%0A~iipP2fSu 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

    ?bA>u3nUXWNW7 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 diff --git a/netbox/project-static/dist/status.js b/netbox/project-static/dist/status.js index a6a5534eccdef330c0756012d52aa2757b5797d0..cf9cd63abde60a60ad1d3b1bb2498b9c159e127f 100644 GIT binary patch literal 105522 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 delta 22083 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 N)}KH6)0ckp{{ibn?KA)Y diff --git a/netbox/project-static/package.json b/netbox/project-static/package.json index 8258c2be4..f10b5b7ac 100644 --- a/netbox/project-static/package.json +++ b/netbox/project-static/package.json @@ -29,9 +29,9 @@ "color2k": "^2.0.0", "dayjs": "^1.11.5", "flatpickr": "4.6.13", + "gridstack": "^7.2.3", "htmx.org": "^1.8.0", "just-debounce-it": "^3.1.1", - "masonry-layout": "^4.2.2", "query-string": "^7.1.1", "sass": "^1.55.0", "simplebar": "^5.3.9", @@ -56,4 +56,4 @@ "resolutions": { "@types/bootstrap/**/@popperjs/core": "^2.11.6" } -} \ No newline at end of file +} diff --git a/netbox/project-static/src/bs.ts b/netbox/project-static/src/bs.ts index e819b7cb1..ecc99ba1a 100644 --- a/netbox/project-static/src/bs.ts +++ b/netbox/project-static/src/bs.ts @@ -1,5 +1,4 @@ import { Collapse, Modal, Popover, Tab, Toast, Tooltip } from 'bootstrap'; -import Masonry from 'masonry-layout'; import { createElement, getElements } from './util'; type ToastLevel = 'danger' | 'warning' | 'success' | 'info'; @@ -12,18 +11,6 @@ window.Popover = Popover; window.Toast = Toast; window.Tooltip = Tooltip; -/** - * Initialize masonry-layout for homepage (or any other masonry layout cards). - */ -function initMasonry(): void { - for (const grid of getElements('.masonry')) { - new Masonry(grid, { - itemSelector: '.masonry-item', - percentPosition: true, - }); - } -} - function initTooltips() { for (const tooltip of getElements('[data-bs-toggle="tooltip"]')) { new Tooltip(tooltip, { container: 'body' }); @@ -194,7 +181,6 @@ export function initBootstrap(): void { for (const func of [ initTooltips, initModals, - initMasonry, initTabs, initImagePreview, initSidebarAccordions, diff --git a/netbox/project-static/src/dashboard.ts b/netbox/project-static/src/dashboard.ts new file mode 100644 index 000000000..4efc3899c --- /dev/null +++ b/netbox/project-static/src/dashboard.ts @@ -0,0 +1,41 @@ +import { GridStack, GridStackOptions, GridStackWidget } from 'gridstack'; +import { createToast } from './bs'; +import { apiPatch, hasError } from './util'; + +async function saveDashboardLayout( + url: string, + gridData: GridStackWidget[] | GridStackOptions, +): Promise> { + let data = { + layout: gridData + } + return await apiPatch(url, data); +} + +export function initDashboard(): void { + // Initialize the grid + let grid = GridStack.init({ + cellHeight: 100, + }); + + // Create a listener for the dashboard save button + const gridSaveButton = document.getElementById('save_dashboard') as HTMLButtonElement; + if (gridSaveButton === null) { + return; + } + gridSaveButton.addEventListener('click', () => { + const url = gridSaveButton.getAttribute('data-url'); + if (url == null) { + return; + } + let gridData = grid.save(false); + saveDashboardLayout(url, gridData).then(res => { + if (hasError(res)) { + const toast = createToast('danger', 'Error Saving Dashboard Config', res.error); + toast.show(); + } else { + location.reload(); + } + }); + }); +} diff --git a/netbox/project-static/src/netbox.ts b/netbox/project-static/src/netbox.ts index f19b879fe..ed294e655 100644 --- a/netbox/project-static/src/netbox.ts +++ b/netbox/project-static/src/netbox.ts @@ -10,6 +10,7 @@ import { initDateSelector } from './dateSelector'; import { initTableConfig } from './tableConfig'; import { initInterfaceTable } from './tables'; import { initSideNav } from './sidenav'; +import { initDashboard } from './dashboard'; import { initRackElevation } from './racks'; import { initLinks } from './links'; import { initHtmx } from './htmx'; @@ -28,6 +29,7 @@ function initDocument(): void { initTableConfig, initInterfaceTable, initSideNav, + initDashboard, initRackElevation, initLinks, initHtmx, diff --git a/netbox/project-static/styles/_external.scss b/netbox/project-static/styles/_external.scss index aee6aa95d..a44238653 100644 --- a/netbox/project-static/styles/_external.scss +++ b/netbox/project-static/styles/_external.scss @@ -2,3 +2,4 @@ @import '../node_modules/@mdi/font/css/materialdesignicons.min.css'; @import '../node_modules/flatpickr/dist/flatpickr.css'; @import '../node_modules/simplebar/dist/simplebar.css'; +@import 'gridstack/dist/gridstack.min.css'; diff --git a/netbox/project-static/yarn.lock b/netbox/project-static/yarn.lock index 9dca72d25..c4bee7557 100644 --- a/netbox/project-static/yarn.lock +++ b/netbox/project-static/yarn.lock @@ -875,11 +875,6 @@ delegate@^3.1.2: resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== -desandro-matches-selector@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/desandro-matches-selector/-/desandro-matches-selector-2.0.2.tgz#717beed4dc13e7d8f3762f707a6d58a6774218e1" - integrity sha1-cXvu1NwT59jzdi9wem1YpndCGOE= - diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -1411,11 +1406,6 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -ev-emitter@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ev-emitter/-/ev-emitter-1.1.1.tgz#8f18b0ce5c76a5d18017f71c0a795c65b9138f2a" - integrity sha512-ipiDYhdQSCZ4hSbX4rMW+XzNKMD1prg/sTvoVmSLkuQ1MVlwjJQQA+sW8tMYR3BLUr9KjodFV4pvzunvRhd33Q== - event-target-shim@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" @@ -1496,13 +1486,6 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -fizzy-ui-utils@^2.0.0: - version "2.0.7" - resolved "https://registry.yarnpkg.com/fizzy-ui-utils/-/fizzy-ui-utils-2.0.7.tgz#7df45dcc4eb374a08b65d39bb9a4beedf7330505" - integrity sha512-CZXDVXQ1If3/r8s0T+v+qVeMshhfcuq0rqIFgJnrtd+Bu8GmDmqMjntjUePypVtjHXKJ6V4sw9zeyox34n9aCg== - dependencies: - desandro-matches-selector "^2.0.0" - flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -1582,11 +1565,6 @@ get-intrinsic@^1.1.3: has "^1.0.3" has-symbols "^1.0.3" -get-size@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/get-size/-/get-size-2.0.3.tgz#54a1d0256b20ea7ac646516756202769941ad2ef" - integrity sha512-lXNzT/h/dTjTxRbm9BXb+SGxxzkm97h/PCIKtlN/CBCxxmkkIVV21udumMS93MuVTDX583gqc94v3RjuHmI+2Q== - get-symbol-description@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" @@ -1784,6 +1762,11 @@ graphql-ws@^5.4.1: resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.5.0.tgz#39d19494dbe69d1ea719915b578bf920344a69d5" integrity sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA== +gridstack@^7.2.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-7.2.3.tgz#bc04d3588eb5f2b7edd910e31fdac5bea8069ff2" + integrity sha512-1s4Fx+Hr4nKl064q/ygrd41XiZaC2gG6R+yz5nbOibP9vODJ6mOtjIM5x8qKN12FknakaMpVBnCa1T6V7H15hQ== + has-bigints@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" @@ -2163,14 +2146,6 @@ markdown-it@^10.0.0: mdurl "^1.0.1" uc.micro "^1.0.5" -masonry-layout@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/masonry-layout/-/masonry-layout-4.2.2.tgz#d57b44af13e601bfcdc423f1dd8348b5524de348" - integrity sha512-iGtAlrpHNyxaR19CvKC3npnEcAwszXoyJiI8ARV2ePi7fmYhIud25MHK8Zx4P0LCC4d3TNO9+rFa1KoK1OEOaA== - dependencies: - get-size "^2.0.2" - outlayer "^2.1.0" - mdurl@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" @@ -2341,15 +2316,6 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" -outlayer@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/outlayer/-/outlayer-2.1.1.tgz#29863b6de10ea5dadfffcadfa0d728907387e9a2" - integrity sha1-KYY7beEOpdrf/8rfoNcokHOH6aI= - dependencies: - ev-emitter "^1.0.0" - fizzy-ui-utils "^2.0.0" - get-size "^2.0.2" - p-limit@3.1.0, p-limit@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" diff --git a/netbox/templates/extras/dashboard/widget.html b/netbox/templates/extras/dashboard/widget.html new file mode 100644 index 000000000..4ed84f067 --- /dev/null +++ b/netbox/templates/extras/dashboard/widget.html @@ -0,0 +1,37 @@ +{% load dashboard %} + +

    +
    +
    +
    + +
    +
    + +
    + {% if widget.title %} + {{ widget.title }} + {% endif %} +
    +
    + {% render_widget widget %} +
    +
    +
    diff --git a/netbox/templates/extras/dashboard/widget_add.html b/netbox/templates/extras/dashboard/widget_add.html new file mode 100644 index 000000000..e752a393d --- /dev/null +++ b/netbox/templates/extras/dashboard/widget_add.html @@ -0,0 +1,27 @@ +{% load form_helpers %} + +
    + {% csrf_token %} + + + +
    diff --git a/netbox/templates/extras/dashboard/widget_config.html b/netbox/templates/extras/dashboard/widget_config.html new file mode 100644 index 000000000..6f8f8cc20 --- /dev/null +++ b/netbox/templates/extras/dashboard/widget_config.html @@ -0,0 +1,20 @@ +{% load form_helpers %} + +
    + {% csrf_token %} + + + +
    diff --git a/netbox/templates/extras/dashboard/widgets/changelog.html b/netbox/templates/extras/dashboard/widgets/changelog.html new file mode 100644 index 000000000..dfa4dba3f --- /dev/null +++ b/netbox/templates/extras/dashboard/widgets/changelog.html @@ -0,0 +1,4 @@ +
    diff --git a/netbox/templates/extras/dashboard/widgets/objectcounts.html b/netbox/templates/extras/dashboard/widgets/objectcounts.html new file mode 100644 index 000000000..d75d88218 --- /dev/null +++ b/netbox/templates/extras/dashboard/widgets/objectcounts.html @@ -0,0 +1,14 @@ +{% load helpers %} + +{% if counts %} +
    + {% for model, count in counts %} + +
    + {{ model|meta:"verbose_name_plural"|bettertitle }} +
    {{ count }}
    +
    +
    + {% endfor %} +
    +{% endif %} diff --git a/netbox/templates/home.html b/netbox/templates/home.html index cef797f40..5bed19d9d 100644 --- a/netbox/templates/home.html +++ b/netbox/templates/home.html @@ -3,80 +3,48 @@ {% load render_table from django_tables2 %} {% block header %} - {% if new_release %} - {# new_release is set only if the current user is a superuser or staff member #} -
    - -
    - {% endif %} + {% if new_release %} + {# new_release is set only if the current user is a superuser or staff member #} +
    + +
    + {% endif %} {% endblock %} {% block title %}Home{% endblock %} {% block content-wrapper %} -
    - {# General stats #} -
    - {% for section, items, icon in stats %} -
    -
    -
    - - {{ section }} -
    -
    -
    - {% for item in items %} - {% if item.permission in perms %} - -
    - {{ item.label }} -

    {{ item.count }}

    -
    -
    - {% else %} -
  • -
    - {{ item.label }} -

    - -

    -
    -
  • - {% endif %} - {% endfor %} -
    -
    -
    -
    - {% endfor %} -
    - - {# Changelog #} - {% if perms.extras.view_objectchange %} -
    -
    -
    -
    - - Change Log -
    -
    -
    -
    -
    - {% endif %} + {# Render the user's customized dashboard #} +
    + {% for widget in dashboard %} + {% include 'extras/dashboard/widget.html' %} + {% endfor %} +
    +
    + + Add Widget + +
    {% endblock content-wrapper %} + +{% block modals %} + {% include 'inc/htmx_modal.html' %} +{% endblock modals %} diff --git a/netbox/templates/inc/htmx_modal.html b/netbox/templates/inc/htmx_modal.html index d15e5b799..771f5d595 100644 --- a/netbox/templates/inc/htmx_modal.html +++ b/netbox/templates/inc/htmx_modal.html @@ -1,5 +1,5 @@ {% endblock content-wrapper %} + +{% block modals %} + {% include 'inc/htmx_modal.html' with size='lg' %} +{% endblock %} diff --git a/netbox/templates/htmx/object_selector.html b/netbox/templates/htmx/object_selector.html new file mode 100644 index 000000000..f0b6da404 --- /dev/null +++ b/netbox/templates/htmx/object_selector.html @@ -0,0 +1,32 @@ +{% load form_helpers %} + + + diff --git a/netbox/templates/htmx/object_selector_results.html b/netbox/templates/htmx/object_selector_results.html new file mode 100644 index 000000000..67529967e --- /dev/null +++ b/netbox/templates/htmx/object_selector_results.html @@ -0,0 +1,13 @@ + diff --git a/netbox/templates/inc/htmx_modal.html b/netbox/templates/inc/htmx_modal.html index 771f5d595..5361fc5f7 100644 --- a/netbox/templates/inc/htmx_modal.html +++ b/netbox/templates/inc/htmx_modal.html @@ -1,5 +1,5 @@