diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 42a716ae7..b43968731 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.5.7 + placeholder: v3.5.8 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index b04fda1b6..5df3069ba 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.5.7 + placeholder: v3.5.8 validations: required: true - type: dropdown diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b71fb515..301fac079 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,12 +14,25 @@
-Some general tips for engaging here on GitHub: +## :information_source: Welcome to the Stadium! + +In her book [Working in Public](https://www.amazon.com/Working-Public-Making-Maintenance-Software/dp/0578675862), Nadia Eghbal defines four production models for open source projects, categorized by contributor and user growth: federations, clubs, toys, and stadiums. The NetBox project fits her definition of a stadium very well: + +> Stadiums are projects with low contributor growth and high user growth. While they may receive casual contributions, their regular contributor base does not grow proportionately to their users. As a result, they tend to be powered by one or a few developers. + +The bulk of NetBox's development is carried out by a handful of core maintainers, with occasional contributions from collaborators in the community. We find the stadium analogy very useful in conveying the roles and obligations of both contributors and users. + +If you're a contributor, actively working on the center stage, you have an obligation to produce quality content that will benefit the project as a whole. Conversely, if you're in the audience consuming the work being produced, you have the option of making requests and suggestions, but must also recognize that contributors are under no obligation to act on them. + +NetBox users are welcome to participate in either role, on stage or in the crowd. We ask only that you acknowledge the role you've chosen and respect the roles of others. + +### General Tips for Working on GitHub * Register for a free [GitHub account](https://github.com/signup) if you haven't already. * You can use [GitHub Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) for formatting text and adding images. * To help mitigate notification spam, please avoid "bumping" issues with no activity. (To vote an issue up or down, use a :thumbsup: or :thumbsdown: reaction.) * Please avoid pinging members with `@` unless they've previously expressed interest or involvement with that particular issue. +* Familiarize yourself with [this list of discussion anti-patterns](https://github.com/bradfitz/issue-tracker-behaviors) and make every effort to avoid them. ## :bug: Reporting Bugs diff --git a/contrib/generated_schema.json b/contrib/generated_schema.json new file mode 100644 index 000000000..8dbcb2847 --- /dev/null +++ b/contrib/generated_schema.json @@ -0,0 +1,561 @@ +{ + "type": "object", + "additionalProperties": false, + "definitions": { + "airflow": { + "type": "string", + "enum": [ + "front-to-rear", + "rear-to-front", + "left-to-right", + "right-to-left", + "side-to-rear", + "passive", + "mixed" + ] + }, + "weight-unit": { + "type": "string", + "enum": [ + "kg", + "g", + "lb", + "oz" + ] + }, + "subdevice-role": { + "type": "string", + "enum": [ + "parent", + "child" + ] + }, + "console-port": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "de-9", + "db-25", + "rj-11", + "rj-12", + "rj-45", + "mini-din-8", + "usb-a", + "usb-b", + "usb-c", + "usb-mini-a", + "usb-mini-b", + "usb-micro-a", + "usb-micro-b", + "usb-micro-ab", + "other" + ] + } + } + }, + "console-server-port": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "de-9", + "db-25", + "rj-11", + "rj-12", + "rj-45", + "mini-din-8", + "usb-a", + "usb-b", + "usb-c", + "usb-mini-a", + "usb-mini-b", + "usb-micro-a", + "usb-micro-b", + "usb-micro-ab", + "other" + ] + } + } + }, + "power-port": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "iec-60320-c6", + "iec-60320-c8", + "iec-60320-c14", + "iec-60320-c16", + "iec-60320-c20", + "iec-60320-c22", + "iec-60309-p-n-e-4h", + "iec-60309-p-n-e-6h", + "iec-60309-p-n-e-9h", + "iec-60309-2p-e-4h", + "iec-60309-2p-e-6h", + "iec-60309-2p-e-9h", + "iec-60309-3p-e-4h", + "iec-60309-3p-e-6h", + "iec-60309-3p-e-9h", + "iec-60309-3p-n-e-4h", + "iec-60309-3p-n-e-6h", + "iec-60309-3p-n-e-9h", + "iec-60906-1", + "nbr-14136-10a", + "nbr-14136-20a", + "nema-1-15p", + "nema-5-15p", + "nema-5-20p", + "nema-5-30p", + "nema-5-50p", + "nema-6-15p", + "nema-6-20p", + "nema-6-30p", + "nema-6-50p", + "nema-10-30p", + "nema-10-50p", + "nema-14-20p", + "nema-14-30p", + "nema-14-50p", + "nema-14-60p", + "nema-15-15p", + "nema-15-20p", + "nema-15-30p", + "nema-15-50p", + "nema-15-60p", + "nema-l1-15p", + "nema-l5-15p", + "nema-l5-20p", + "nema-l5-30p", + "nema-l5-50p", + "nema-l6-15p", + "nema-l6-20p", + "nema-l6-30p", + "nema-l6-50p", + "nema-l10-30p", + "nema-l14-20p", + "nema-l14-30p", + "nema-l14-50p", + "nema-l14-60p", + "nema-l15-20p", + "nema-l15-30p", + "nema-l15-50p", + "nema-l15-60p", + "nema-l21-20p", + "nema-l21-30p", + "nema-l22-30p", + "cs6361c", + "cs6365c", + "cs8165c", + "cs8265c", + "cs8365c", + "cs8465c", + "ita-c", + "ita-e", + "ita-f", + "ita-ef", + "ita-g", + "ita-h", + "ita-i", + "ita-j", + "ita-k", + "ita-l", + "ita-m", + "ita-n", + "ita-o", + "usb-a", + "usb-b", + "usb-c", + "usb-mini-a", + "usb-mini-b", + "usb-micro-a", + "usb-micro-b", + "usb-micro-ab", + "usb-3-b", + "usb-3-micro-b", + "dc-terminal", + "saf-d-grid", + "neutrik-powercon-20", + "neutrik-powercon-32", + "neutrik-powercon-true1", + "neutrik-powercon-true1-top", + "ubiquiti-smartpower", + "hardwired", + "other" + ] + } + } + }, + "power-outlet": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "iec-60320-c5", + "iec-60320-c7", + "iec-60320-c13", + "iec-60320-c15", + "iec-60320-c19", + "iec-60320-c21", + "iec-60309-p-n-e-4h", + "iec-60309-p-n-e-6h", + "iec-60309-p-n-e-9h", + "iec-60309-2p-e-4h", + "iec-60309-2p-e-6h", + "iec-60309-2p-e-9h", + "iec-60309-3p-e-4h", + "iec-60309-3p-e-6h", + "iec-60309-3p-e-9h", + "iec-60309-3p-n-e-4h", + "iec-60309-3p-n-e-6h", + "iec-60309-3p-n-e-9h", + "iec-60906-1", + "nbr-14136-10a", + "nbr-14136-20a", + "nema-1-15r", + "nema-5-15r", + "nema-5-20r", + "nema-5-30r", + "nema-5-50r", + "nema-6-15r", + "nema-6-20r", + "nema-6-30r", + "nema-6-50r", + "nema-10-30r", + "nema-10-50r", + "nema-14-20r", + "nema-14-30r", + "nema-14-50r", + "nema-14-60r", + "nema-15-15r", + "nema-15-20r", + "nema-15-30r", + "nema-15-50r", + "nema-15-60r", + "nema-l1-15r", + "nema-l5-15r", + "nema-l5-20r", + "nema-l5-30r", + "nema-l5-50r", + "nema-l6-15r", + "nema-l6-20r", + "nema-l6-30r", + "nema-l6-50r", + "nema-l10-30r", + "nema-l14-20r", + "nema-l14-30r", + "nema-l14-50r", + "nema-l14-60r", + "nema-l15-20r", + "nema-l15-30r", + "nema-l15-50r", + "nema-l15-60r", + "nema-l21-20r", + "nema-l21-30r", + "nema-l22-30r", + "CS6360C", + "CS6364C", + "CS8164C", + "CS8264C", + "CS8364C", + "CS8464C", + "ita-e", + "ita-f", + "ita-g", + "ita-h", + "ita-i", + "ita-j", + "ita-k", + "ita-l", + "ita-m", + "ita-n", + "ita-o", + "ita-multistandard", + "usb-a", + "usb-micro-b", + "usb-c", + "dc-terminal", + "hdot-cx", + "saf-d-grid", + "neutrik-powercon-20a", + "neutrik-powercon-32a", + "neutrik-powercon-true1", + "neutrik-powercon-true1-top", + "ubiquiti-smartpower", + "hardwired", + "other" + ] + }, + "feed-leg": { + "type": "string", + "enum": [ + "A", + "B", + "C" + ] + } + } + }, + "interface": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "virtual", + "bridge", + "lag", + "100base-fx", + "100base-lfx", + "100base-tx", + "100base-t1", + "1000base-t", + "2.5gbase-t", + "5gbase-t", + "10gbase-t", + "10gbase-cx4", + "1000base-x-gbic", + "1000base-x-sfp", + "10gbase-x-sfpp", + "10gbase-x-xfp", + "10gbase-x-xenpak", + "10gbase-x-x2", + "25gbase-x-sfp28", + "50gbase-x-sfp56", + "40gbase-x-qsfpp", + "50gbase-x-sfp28", + "100gbase-x-cfp", + "100gbase-x-cfp2", + "200gbase-x-cfp2", + "100gbase-x-cfp4", + "100gbase-x-cxp", + "100gbase-x-cpak", + "100gbase-x-dsfp", + "100gbase-x-sfpdd", + "100gbase-x-qsfp28", + "100gbase-x-qsfpdd", + "200gbase-x-qsfp56", + "200gbase-x-qsfpdd", + "400gbase-x-qsfpdd", + "400gbase-x-osfp", + "400gbase-x-cdfp", + "400gbase-x-cfp8", + "800gbase-x-qsfpdd", + "800gbase-x-osfp", + "1000base-kx", + "10gbase-kr", + "10gbase-kx4", + "25gbase-kr", + "40gbase-kr4", + "50gbase-kr", + "100gbase-kp4", + "100gbase-kr2", + "100gbase-kr4", + "ieee802.11a", + "ieee802.11g", + "ieee802.11n", + "ieee802.11ac", + "ieee802.11ad", + "ieee802.11ax", + "ieee802.11ay", + "ieee802.15.1", + "other-wireless", + "gsm", + "cdma", + "lte", + "sonet-oc3", + "sonet-oc12", + "sonet-oc48", + "sonet-oc192", + "sonet-oc768", + "sonet-oc1920", + "sonet-oc3840", + "1gfc-sfp", + "2gfc-sfp", + "4gfc-sfp", + "8gfc-sfpp", + "16gfc-sfpp", + "32gfc-sfp28", + "64gfc-qsfpp", + "128gfc-qsfp28", + "infiniband-sdr", + "infiniband-ddr", + "infiniband-qdr", + "infiniband-fdr10", + "infiniband-fdr", + "infiniband-edr", + "infiniband-hdr", + "infiniband-ndr", + "infiniband-xdr", + "t1", + "e1", + "t3", + "e3", + "xdsl", + "docsis", + "gpon", + "xg-pon", + "xgs-pon", + "ng-pon2", + "epon", + "10g-epon", + "cisco-stackwise", + "cisco-stackwise-plus", + "cisco-flexstack", + "cisco-flexstack-plus", + "cisco-stackwise-80", + "cisco-stackwise-160", + "cisco-stackwise-320", + "cisco-stackwise-480", + "cisco-stackwise-1t", + "juniper-vcp", + "extreme-summitstack", + "extreme-summitstack-128", + "extreme-summitstack-256", + "extreme-summitstack-512", + "other" + ] + }, + "poe_mode": { + "type": "string", + "enum": [ + "pd", + "pse" + ] + }, + "poe_type": { + "type": "string", + "enum": [ + "type1-ieee802.3af", + "type2-ieee802.3at", + "type3-ieee802.3bt", + "type4-ieee802.3bt", + "passive-24v-2pair", + "passive-24v-4pair", + "passive-48v-2pair", + "passive-48v-4pair" + ] + } + } + }, + "front-port": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "8p8c", + "8p6c", + "8p4c", + "8p2c", + "6p6c", + "6p4c", + "6p2c", + "4p4c", + "4p2c", + "gg45", + "tera-4p", + "tera-2p", + "tera-1p", + "110-punch", + "bnc", + "f", + "n", + "mrj21", + "fc", + "lc", + "lc-pc", + "lc-upc", + "lc-apc", + "lsh", + "lsh-pc", + "lsh-upc", + "lsh-apc", + "lx5", + "lx5-pc", + "lx5-upc", + "lx5-apc", + "mpo", + "mtrj", + "sc", + "sc-pc", + "sc-upc", + "sc-apc", + "st", + "cs", + "sn", + "sma-905", + "sma-906", + "urm-p2", + "urm-p4", + "urm-p8", + "splice", + "other" + ] + } + } + }, + "rear-port": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "8p8c", + "8p6c", + "8p4c", + "8p2c", + "6p6c", + "6p4c", + "6p2c", + "4p4c", + "4p2c", + "gg45", + "tera-4p", + "tera-2p", + "tera-1p", + "110-punch", + "bnc", + "f", + "n", + "mrj21", + "fc", + "lc", + "lc-pc", + "lc-upc", + "lc-apc", + "lsh", + "lsh-pc", + "lsh-upc", + "lsh-apc", + "lx5", + "lx5-pc", + "lx5-upc", + "lx5-apc", + "mpo", + "mtrj", + "sc", + "sc-pc", + "sc-upc", + "sc-apc", + "st", + "cs", + "sn", + "sma-905", + "sma-906", + "urm-p2", + "urm-p4", + "urm-p8", + "splice", + "other" + ] + } + } + } + } +} diff --git a/docs/configuration/security.md b/docs/configuration/security.md index a6dc4c5ce..2ae92285f 100644 --- a/docs/configuration/security.md +++ b/docs/configuration/security.md @@ -4,7 +4,7 @@ Default: True -If disabled, the values of API tokens will not be displayed after each token's initial creation. A user **must** record the value of a token immediately upon its creation, or it will be lost. Note that this affects _all_ users, regardless of assigned permissions. +If disabled, the values of API tokens will not be displayed after each token's initial creation. A user **must** record the value of a token prior to its creation, or it will be lost. Note that this affects _all_ users, regardless of assigned permissions. --- diff --git a/docs/development/internationalization.md b/docs/development/internationalization.md new file mode 100644 index 000000000..bdc7cbdaa --- /dev/null +++ b/docs/development/internationalization.md @@ -0,0 +1,123 @@ +# Internationalization + +Beginning with NetBox v4.0, NetBox will leverage [Django's automatic translation](https://docs.djangoproject.com/en/stable/topics/i18n/translation/) to support languages other than English. This page details the areas of the project which require special attention to ensure functioning translation support. Briefly, these include: + +* The `verbose_name` and `verbose_name_plural` Meta attributes for each model +* The `verbose_name` and (if defined) `help_text` for each model field +* The `label` for each form field +* Headers for `fieldsets` on each form class +* The `verbose_name` for each table column +* All human-readable strings within templates must be wrapped with `{% trans %}` or `{% blocktrans %}` + +The rest of this document elaborates on each of the items above. + +## General Guidance + +* Wrap human-readable strings with Django's `gettext()` or `gettext_lazy()` utility functions to enable automatic translation. Generally, `gettext_lazy()` is preferred (and sometimes required) to defer translation until the string is displayed. + +* By convention, the preferred translation function is typically imported as an underscore (`_`) to minimize boilerplate code. Thus, you will often see translation as e.g. `_("Some text")`. It is still an option to import and use alternative translation functions (e.g. `pgettext()` and `ngettext()`) normally as needed. + +* Avoid passing markup and other non-natural language where possible. Everything wrapped by a translation function gets exported to a messages file for translation by a human. + +* Where the intended meaning of the translated string may not be obvious, use `pgettext()` or `pgettext_lazy()` to include assisting context for the translator. For example: + + ```python + # Context, string + pgettext("month name", "May") + ``` + +* **Format strings do not support translation.** Avoid "f" strings for messages that must support translation. Instead, use `format()` to accomplish variable replacement: + + ```python + # Translation will not work + f"There are {count} objects" + + # Do this instead + "There are {count} objects".format(count=count) + ``` + +## Models + +1. Import `gettext_lazy` as `_`. +2. Ensure both `verbose_name` and `verbose_name_plural` are defined under the model's `Meta` class and wrapped with the `gettext_lazy()` shortcut. +3. Ensure each model field specifies a `verbose_name` wrapped with `gettext_lazy()`. +4. Ensure any `help_text` attributes on model fields are also wrapped with `gettext_lazy()`. + +```python +from django.utils.translation import gettext_lazy as _ + +class Circuit(PrimaryModel): + commit_rate = models.PositiveIntegerField( + ... + verbose_name=_('commit rate (Kbps)'), + help_text=_("Committed rate") + ) + + class Meta: + verbose_name = _('circuit') + verbose_name_plural = _('circuits') +``` + +## Forms + +1. Import `gettext_lazy` as `_`. +2. All form fields must specify a `label` wrapped with `gettext_lazy()`. +3. All headers under a form's `fieldsets` property must be wrapped with `gettext_lazy()`. + +```python +from django.utils.translation import gettext_lazy as _ + +class CircuitBulkEditForm(NetBoxModelBulkEditForm): + description = forms.CharField( + label=_('Description'), + ... + ) + + fieldsets = ( + (_('Circuit'), ('provider', 'type', 'status', 'description')), + ) +``` + +## Tables + +1. Import `gettext_lazy` as `_`. +2. All table columns must specify a `verbose_name` wrapped with `gettext_lazy()`. + +```python +from django.utils.translation import gettext_lazy as _ + +class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): + provider = tables.Column( + verbose_name=_('Provider'), + ... + ) +``` + +## Templates + +1. Ensure translation support is enabled by including `{% load i18n %}` at the top of the template. +2. Use the [`{% trans %}`](https://docs.djangoproject.com/en/stable/topics/i18n/translation/#translate-template-tag) tag (short for "translate") to wrap short strings. +3. Longer strings may be enclosed between [`{% blocktrans %}`](https://docs.djangoproject.com/en/stable/topics/i18n/translation/#blocktranslate-template-tag) and `{% endblocktrans %}` tags to improve readability and to enable variable replacement. +4. Avoid passing HTML within translated strings where possible, as this can complicate the work needed of human translators to develop message maps. + +``` +{% load i18n %} + +{# A short string #} +{stacktrace}") logger.error(f"Exception raised during report execution: {e}") job.terminate(status=JobStatusChoices.STATUS_ERRORED) - finally: - job.data = self._results - job.terminate() # Perform any post-run tasks self.post_run() diff --git a/netbox/extras/tests/test_plugins.py b/netbox/extras/tests/test_plugins.py index cb7629ad2..42dde43fd 100644 --- a/netbox/extras/tests/test_plugins.py +++ b/netbox/extras/tests/test_plugins.py @@ -5,8 +5,9 @@ from django.core.exceptions import ImproperlyConfigured from django.test import Client, TestCase, override_settings from django.urls import reverse -from extras.plugins import PluginMenu, get_plugin_config +from extras.plugins import PluginMenu from extras.tests.dummy_plugin import config as dummy_config +from extras.plugins.utils import get_plugin_config from netbox.graphql.schema import Query from netbox.registry import registry diff --git a/netbox/extras/tests/test_webhooks.py b/netbox/extras/tests/test_webhooks.py index 19264dabb..ef7637765 100644 --- a/netbox/extras/tests/test_webhooks.py +++ b/netbox/extras/tests/test_webhooks.py @@ -31,8 +31,8 @@ class WebhookTest(APITestCase): def setUpTestData(cls): site_ct = ContentType.objects.get_for_model(Site) - DUMMY_URL = "http://localhost/" - DUMMY_SECRET = "LOOKATMEIMASECRETSTRING" + DUMMY_URL = 'http://localhost:9000/' + DUMMY_SECRET = 'LOOKATMEIMASECRETSTRING' webhooks = Webhook.objects.bulk_create(( Webhook(name='Webhook 1', type_create=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET, additional_headers='X-Foo: Bar'), @@ -259,7 +259,7 @@ class WebhookTest(APITestCase): name='Conditional Webhook', type_create=True, type_update=True, - payload_url='http://localhost/', + payload_url='http://localhost:9000/', conditions={ 'and': [ { diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 193d8821b..a9636cc9e 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -1,6 +1,7 @@ from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.contenttypes.models import ContentType +from django.core.paginator import EmptyPage from django.db.models import Count, Q from django.http import HttpResponseBadRequest, HttpResponseForbidden, HttpResponse from django.shortcuts import get_object_or_404, redirect, render @@ -18,6 +19,7 @@ from netbox.config import get_config, PARAMS from netbox.views import generic from utilities.forms import ConfirmationForm, get_field_value from utilities.htmx import is_htmx +from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.rqworker import get_workers_for_queue from utilities.templatetags.builtins.filters import render_markdown from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict @@ -89,6 +91,25 @@ class CustomFieldChoiceSetListView(generic.ObjectListView): class CustomFieldChoiceSetView(generic.ObjectView): queryset = CustomFieldChoiceSet.objects.all() + def get_extra_context(self, request, instance): + + # Paginate choices list + per_page = get_paginate_count(request) + try: + page_number = request.GET.get('page', 1) + except ValueError: + page_number = 1 + paginator = EnhancedPaginator(instance.choices, per_page) + try: + choices = paginator.page(page_number) + except EmptyPage: + choices = paginator.page(paginator.num_pages) + + return { + 'paginator': paginator, + 'choices': choices, + } + @register_model_view(CustomFieldChoiceSet, 'edit') class CustomFieldChoiceSetEditView(generic.ObjectEditView): diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 16b494dd5..da6463e23 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -1,7 +1,5 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import transaction -from django.db.models import F -from django.db.models.functions import Round from django.shortcuts import get_object_or_404 from django_pglocks import advisory_lock from drf_spectacular.utils import extend_schema @@ -16,6 +14,7 @@ from circuits.models import Provider from dcim.models import Site from ipam import filtersets from ipam.models import * +from ipam.models import L2VPN, L2VPNTermination from ipam.utils import get_next_available_prefix from netbox.api.viewsets import NetBoxModelViewSet from netbox.api.viewsets.mixins import ObjectValidationMixin @@ -24,7 +23,6 @@ from netbox.constants import ADVISORY_LOCK_KEYS from utilities.api import get_serializer_for_model from utilities.utils import count_related from . import serializers -from ipam.models import L2VPN, L2VPNTermination class IPAMRootView(APIRootView): @@ -346,7 +344,11 @@ class AvailableASNsView(AvailableObjectsView): def get(self, request, pk): return super().get(request, pk) - @extend_schema(methods=["post"], responses={201: serializers.AvailableASNSerializer(many=True)}) + @extend_schema( + methods=["post"], + responses={201: serializers.ASNSerializer(many=True)}, + request=serializers.ASNSerializer(many=True), + ) def post(self, request, pk): return super().post(request, pk) @@ -395,7 +397,11 @@ class AvailablePrefixesView(AvailableObjectsView): def get(self, request, pk): return super().get(request, pk) - @extend_schema(methods=["post"], responses={201: serializers.PrefixSerializer(many=True)}) + @extend_schema( + methods=["post"], + responses={201: serializers.PrefixSerializer(many=True)}, + request=serializers.PrefixSerializer(many=True), + ) def post(self, request, pk): return super().post(request, pk) @@ -435,7 +441,11 @@ class AvailableIPAddressesView(AvailableObjectsView): def get(self, request, pk): return super().get(request, pk) - @extend_schema(methods=["post"], responses={201: serializers.IPAddressSerializer(many=True)}) + @extend_schema( + methods=["post"], + responses={201: serializers.IPAddressSerializer(many=True)}, + request=serializers.IPAddressSerializer(many=True), + ) def post(self, request, pk): return super().post(request, pk) @@ -482,6 +492,10 @@ class AvailableVLANsView(AvailableObjectsView): def get(self, request, pk): return super().get(request, pk) - @extend_schema(methods=["post"], responses={201: serializers.VLANSerializer(many=True)}) + @extend_schema( + methods=["post"], + responses={201: serializers.VLANSerializer(many=True)}, + request=serializers.VLANSerializer(many=True), + ) def post(self, request, pk): return super().post(request, pk) diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index d011472d9..9b57cb273 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -591,6 +591,10 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet): method='_assigned_to_interface', label=_('Is assigned to an interface'), ) + assigned = django_filters.BooleanFilter( + method='_assigned', + label=_('Is assigned'), + ) status = django_filters.MultipleChoiceFilter( choices=IPAddressStatusChoices, null_value=None @@ -706,6 +710,18 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet): assigned_object_id__isnull=False ) + def _assigned(self, queryset, name, value): + if value: + return queryset.exclude( + assigned_object_type__isnull=True, + assigned_object_id__isnull=True + ) + else: + return queryset.filter( + assigned_object_type__isnull=True, + assigned_object_id__isnull=True + ) + class FHRPGroupFilterSet(NetBoxModelFilterSet): protocol = django_filters.MultipleChoiceFilter( diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index 9b7c624d2..ec0812593 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -1,7 +1,6 @@ from django import forms from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError -from django.db.models import Q from django.utils.translation import gettext_lazy as _ from dcim.models import Device, Interface, Site @@ -10,7 +9,9 @@ from ipam.constants import * from ipam.models import * from netbox.forms import NetBoxModelImportForm from tenancy.models import Tenant -from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField +from utilities.forms.fields import ( + CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField +) from virtualization.models import VirtualMachine, VMInterface __all__ = ( @@ -42,10 +43,25 @@ class VRFImportForm(NetBoxModelImportForm): to_field_name='name', help_text=_('Assigned tenant') ) + import_targets = CSVModelMultipleChoiceField( + queryset=RouteTarget.objects.all(), + required=False, + to_field_name='name', + help_text=_('Import route targets') + ) + export_targets = CSVModelMultipleChoiceField( + queryset=RouteTarget.objects.all(), + required=False, + to_field_name='name', + help_text=_('Export route targets') + ) class Meta: model = VRF - fields = ('name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments', 'tags') + fields = ( + 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'comments', + 'tags', + ) class RouteTargetImportForm(NetBoxModelImportForm): diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index 299d70565..e4e967f81 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -256,7 +256,7 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = IPRange fieldsets = ( (None, ('q', 'filter_id', 'tag')), - (_('Attriubtes'), ('family', 'vrf_id', 'status', 'role_id', 'mark_utilized')), + (_('Attributes'), ('family', 'vrf_id', 'status', 'role_id', 'mark_utilized')), (_('Tenant'), ('tenant_group_id', 'tenant_id')), ) family = forms.ChoiceField( diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index 0e17a9ce2..4adff78ef 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -992,6 +992,12 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'fhrpgroup_id': [fhrp_groups[0].pk, fhrp_groups[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_assigned(self): + params = {'assigned': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) + params = {'assigned': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_assigned_to_interface(self): params = {'assigned_to_interface': 'true'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 32badd2d5..d8e4d8b47 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -216,7 +216,7 @@ class ASNRangeASNsView(generic.ObjectChildrenView): child_model = ASN table = tables.ASNTable filterset = filtersets.ASNFilterSet - template_name = 'ipam/asnrange/asns.html' + template_name = 'generic/object_children.html' tab = ViewTab( label=_('ASNs'), badge=lambda x: x.get_child_asns().count(), @@ -816,7 +816,6 @@ class IPAddressAssignView(generic.ObjectView): table = None if form.is_valid(): - addresses = self.queryset.prefetch_related('vrf', 'tenant') # Limit to 100 results addresses = filtersets.IPAddressFilterSet(request.POST, addresses).qs[:100] @@ -866,7 +865,7 @@ class IPAddressRelatedIPsView(generic.ObjectChildrenView): child_model = IPAddress table = tables.IPAddressTable filterset = filtersets.IPAddressFilterSet - template_name = 'ipam/ipaddress/ip_addresses.html' + template_name = 'generic/object_children.html' tab = ViewTab( label=_('Related IPs'), badge=lambda x: x.get_related_ips().count(), @@ -963,7 +962,6 @@ class FHRPGroupView(generic.ObjectView): queryset = FHRPGroup.objects.all() def get_extra_context(self, request, instance): - # Get assigned interfaces members_table = tables.FHRPGroupAssignmentTable( data=FHRPGroupAssignment.objects.restrict(request.user, 'view').filter(group=instance), @@ -1077,7 +1075,7 @@ class VLANInterfacesView(generic.ObjectChildrenView): child_model = Interface table = tables.VLANDevicesTable filterset = InterfaceFilterSet - template_name = 'ipam/vlan/interfaces.html' + template_name = 'generic/object_children.html' tab = ViewTab( label=_('Device Interfaces'), badge=lambda x: x.get_interfaces().count(), @@ -1095,7 +1093,7 @@ class VLANVMInterfacesView(generic.ObjectChildrenView): child_model = VMInterface table = tables.VLANVirtualMachinesTable filterset = VMInterfaceFilterSet - template_name = 'ipam/vlan/vminterfaces.html' + template_name = 'generic/object_children.html' tab = ViewTab( label=_('VM Interfaces'), badge=lambda x: x.get_vminterfaces().count(), diff --git a/netbox/netbox/api/views.py b/netbox/netbox/api/views.py index 5c55697ff..97f690762 100644 --- a/netbox/netbox/api/views.py +++ b/netbox/netbox/api/views.py @@ -11,6 +11,7 @@ from rest_framework.reverse import reverse from rest_framework.views import APIView from rq.worker import Worker +from extras.plugins.utils import get_installed_plugins from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired @@ -61,19 +62,11 @@ class StatusView(APIView): installed_apps[app_config.name] = version installed_apps = {k: v for k, v in sorted(installed_apps.items())} - # Gather installed plugins - plugins = {} - for plugin_name in settings.PLUGINS: - plugin_name = plugin_name.rsplit('.', 1)[-1] - plugin_config = apps.get_app_config(plugin_name) - plugins[plugin_name] = getattr(plugin_config, 'version', None) - plugins = {k: v for k, v in sorted(plugins.items())} - return Response({ 'django-version': DJANGO_VERSION, 'installed-apps': installed_apps, 'netbox-version': settings.VERSION, - 'plugins': plugins, + 'plugins': get_installed_plugins(), 'python-version': platform.python_version(), 'rq-workers-running': Worker.count(get_connection('default')), }) diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index f28e74eb5..c5dac90f7 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -88,7 +88,10 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm): def _get_custom_fields(self, content_type): return CustomField.objects.filter(content_types=content_type).filter( - ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE + ui_visibility__in=[ + CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE, + CustomFieldVisibilityChoices.VISIBILITY_HIDDEN_IFUNSET, + ] ) def _get_form_field(self, customfield): diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index 931d565ba..596357ea4 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -22,6 +22,7 @@ __all__ = ( class NetBoxFeatureSet( BookmarksMixin, ChangeLoggingMixin, + CloningMixin, CustomFieldsMixin, CustomLinksMixin, CustomValidationMixin, @@ -53,7 +54,7 @@ class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, WebhooksMixin abstract = True -class NetBoxModel(CloningMixin, NetBoxFeatureSet, models.Model): +class NetBoxModel(NetBoxFeatureSet, models.Model): """ Base model for most object types. Suitable for use by plugins. """ @@ -90,6 +91,10 @@ class NetBoxModel(CloningMixin, NetBoxFeatureSet, models.Model): }) +# +# NetBox internal base models +# + class PrimaryModel(NetBoxModel): """ Primary models represent real objects within the infrastructure being modeled. @@ -108,7 +113,7 @@ class PrimaryModel(NetBoxModel): abstract = True -class NestedGroupModel(CloningMixin, NetBoxFeatureSet, MPTTModel): +class NestedGroupModel(NetBoxFeatureSet, MPTTModel): """ Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest recursively using MPTT. Within each parent, each child instance must have a unique name. diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index 08693850f..cce265efc 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -491,6 +491,19 @@ class SyncedDataMixin(models.Model): return ret + def delete(self, *args, **kwargs): + from core.models import AutoSyncRecord + + # Delete AutoSyncRecord + content_type = ContentType.objects.get_for_model(self) + AutoSyncRecord.objects.filter( + datafile=self.data_file, + object_type=content_type, + object_id=self.pk + ).delete() + + return super().delete(*args, **kwargs) + def resolve_data_file(self): """ Determine the designated DataFile object identified by its parent DataSource and its path. Returns None if @@ -525,11 +538,20 @@ class SyncedDataMixin(models.Model): raise NotImplementedError(f"{self.__class__} must implement a sync_data() method.") +# +# Feature registration +# + FEATURES_MAP = { 'bookmarks': BookmarksMixin, + 'change_logging': ChangeLoggingMixin, + 'cloning': CloningMixin, + 'contacts': ContactsMixin, 'custom_fields': CustomFieldsMixin, 'custom_links': CustomLinksMixin, + 'custom_validation': CustomValidationMixin, 'export_templates': ExportTemplatesMixin, + 'image_attachments': ImageAttachmentsMixin, 'jobs': JobsMixin, 'journaling': JournalingMixin, 'synced_data': SyncedDataMixin, @@ -544,12 +566,13 @@ registry['model_features'].update({ @receiver(class_prepared) def _register_features(sender, **kwargs): + # Record each applicable feature for the model in the registry features = { feature for feature, cls in FEATURES_MAP.items() if issubclass(sender, cls) } register_features(sender, features) - # Feature view registration + # Register applicable feature views for the model if issubclass(sender, JournalingMixin): register_model_view( sender, diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 1fae55f2e..69ef8f52d 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.6-beta1' +VERSION = '3.6-beta2' # Hostname HOSTNAME = platform.node() @@ -474,8 +474,6 @@ LOGIN_REDIRECT_URL = f'/{BASE_PATH}' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -TEST_RUNNER = "django_rich.test.RichRunner" - # Exclude potentially sensitive models from wildcard view exemption. These may still be exempted # by specifying the model individually in the EXEMPT_VIEW_PERMISSIONS configuration parameter. EXEMPT_EXCLUDE_MODELS = ( diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index bc67b0b95..6d6eeb97e 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -511,9 +511,9 @@ class CustomLinkColumn(tables.Column): """ def __init__(self, customlink, *args, **kwargs): self.customlink = customlink - kwargs['accessor'] = Accessor('pk') - if 'verbose_name' not in kwargs: - kwargs['verbose_name'] = customlink.name + kwargs.setdefault('accessor', Accessor('pk')) + kwargs.setdefault('orderable', False) + kwargs.setdefault('verbose_name', customlink.name) super().__init__(*args, **kwargs) diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 673c01185..52ff69aa9 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -54,7 +54,7 @@ class BaseTable(tables.Table): # 3. Meta.fields selected_columns = None if user is not None and not isinstance(user, AnonymousUser): - selected_columns = user.config.get(f"tables.{self.__class__.__name__}.columns") + selected_columns = user.config.get(f"tables.{self.name}.columns") if not selected_columns: selected_columns = getattr(self.Meta, 'default_columns', self.Meta.fields) @@ -113,6 +113,10 @@ class BaseTable(tables.Table): columns.append((name, column.verbose_name)) return columns + @property + def name(self): + return self.__class__.__name__ + @property def available_columns(self): return self._get_columns(visible=False) @@ -138,17 +142,16 @@ class BaseTable(tables.Table): """ # Save ordering preference if request.user.is_authenticated: - table_name = self.__class__.__name__ if self.prefixed_order_by_field in request.GET: if request.GET[self.prefixed_order_by_field]: # If an ordering has been specified as a query parameter, save it as the # user's preferred ordering for this table. ordering = request.GET.getlist(self.prefixed_order_by_field) - request.user.config.set(f'tables.{table_name}.ordering', ordering, commit=True) + request.user.config.set(f'tables.{self.name}.ordering', ordering, commit=True) else: # If the ordering has been set to none (empty), clear any existing preference. - request.user.config.clear(f'tables.{table_name}.ordering', commit=True) - elif ordering := request.user.config.get(f'tables.{table_name}.ordering'): + request.user.config.clear(f'tables.{self.name}.ordering', commit=True) + elif ordering := request.user.config.get(f'tables.{self.name}.ordering'): # If no ordering has been specified, set the preferred ordering (if any). self.order_by = ordering diff --git a/netbox/netbox/views/errors.py b/netbox/netbox/views/errors.py index c74c67cef..a81d45cb5 100644 --- a/netbox/netbox/views/errors.py +++ b/netbox/netbox/views/errors.py @@ -11,6 +11,8 @@ from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found from django.views.generic import View from sentry_sdk import capture_message +from extras.plugins.utils import get_installed_plugins + __all__ = ( 'handler_404', 'handler_500', @@ -53,4 +55,5 @@ def handler_500(request, template_name=ERROR_500_TEMPLATE_NAME): 'exception': str(type_), 'netbox_version': settings.VERSION, 'python_version': platform.python_version(), + 'plugins': get_installed_plugins(), })) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 1ba789cf1..99d8ff540 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -143,9 +143,12 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin): return render(request, self.get_template_name(), { 'object': instance, 'child_model': self.child_model, + 'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html', 'table': table, + 'table_config': f'{table.name}_config', 'actions': actions, 'tab': self.tab, + 'return_url': request.get_full_path(), **self.get_extra_context(request, instance), }) diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index b62436d75..84bfecae3 100644 Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index ed3833f98..7f2400ed2 100644 Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ diff --git a/netbox/project-static/src/buttons/selectAll.ts b/netbox/project-static/src/buttons/selectAll.ts index 64b98d390..f40520e26 100644 --- a/netbox/project-static/src/buttons/selectAll.ts +++ b/netbox/project-static/src/buttons/selectAll.ts @@ -1,4 +1,4 @@ -import { getElement, getElements, findFirstAdjacent } from '../util'; +import { getElements, findFirstAdjacent } from '../util'; /** * If any PK checkbox is checked, uncheck the select all table checkbox and the select all @@ -63,29 +63,6 @@ function handleSelectAllToggle(event: Event): void { } } -/** - * Synchronize the select all confirmation checkbox state with the select all confirmation button - * disabled state. If the select all confirmation checkbox is checked, the buttons should be - * enabled. If not, the buttons should be disabled. - * - * @param event Change Event - */ -function handleSelectAll(event: Event): void { - const target = event.currentTarget as HTMLInputElement; - const selectAllBox = getElement
{% trans "If further assistance is required, please post to the" %} {% trans "NetBox discussion forum" %} {% trans "on GitHub" %}.
diff --git a/netbox/templates/dcim/device/components_base.html b/netbox/templates/dcim/device/components_base.html new file mode 100644 index 000000000..1e3d8a39d --- /dev/null +++ b/netbox/templates/dcim/device/components_base.html @@ -0,0 +1,15 @@ +{% extends 'generic/object_children.html' %} +{% load helpers %} + +{% block bulk_edit_controls %} + {{ block.super }} + {% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %} + {% if 'bulk_rename' in actions and bulk_rename_view %} + + {% endif %} + {% endwith %} +{% endblock bulk_edit_controls %} diff --git a/netbox/templates/dcim/device/consoleports.html b/netbox/templates/dcim/device/consoleports.html index 1df555cda..90f4f99ed 100644 --- a/netbox/templates/dcim/device/consoleports.html +++ b/netbox/templates/dcim/device/consoleports.html @@ -1,58 +1,28 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} +{% extends 'dcim/device/components_base.html' %} {% load helpers %} -{% load static %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsolePortTable_config" %} - - -{% endblock %} + {% endwith %} +{% endblock bulk_delete_controls %} -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% block bulk_extra_controls %} + {{ block.super }} + {% if perms.dcim.add_consoleport %} + + {% endif %} +{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/consoleserverports.html b/netbox/templates/dcim/device/consoleserverports.html index c7b818d29..39289f2e5 100644 --- a/netbox/templates/dcim/device/consoleserverports.html +++ b/netbox/templates/dcim/device/consoleserverports.html @@ -1,58 +1,28 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} +{% extends 'dcim/device/components_base.html' %} {% load helpers %} -{% load static %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsoleServerPortTable_config" %} - - -{% endblock %} + {% endwith %} +{% endblock bulk_delete_controls %} -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% block bulk_extra_controls %} + {{ block.super }} + {% if perms.dcim.add_consoleserverport %} + + {% endif %} +{% endblock bulk_extra_controls %} \ No newline at end of file diff --git a/netbox/templates/dcim/device/devicebays.html b/netbox/templates/dcim/device/devicebays.html index 2f659af9a..5b2a396d7 100644 --- a/netbox/templates/dcim/device/devicebays.html +++ b/netbox/templates/dcim/device/devicebays.html @@ -1,51 +1,14 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} -{% load helpers %} -{% load static %} +{% extends 'dcim/device/components_base.html' %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DeviceDeviceBayTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} + {% endif %} +{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/frontports.html b/netbox/templates/dcim/device/frontports.html index efc632e75..2edef9894 100644 --- a/netbox/templates/dcim/device/frontports.html +++ b/netbox/templates/dcim/device/frontports.html @@ -1,58 +1,28 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} +{% extends 'dcim/device/components_base.html' %} {% load helpers %} -{% load static %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DeviceFrontPortTable_config" %} - - -{% endblock %} + {% endwith %} +{% endblock bulk_delete_controls %} -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% block bulk_extra_controls %} + {{ block.super }} + {% if perms.dcim.add_frontport %} + + {% endif %} +{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/interfaces.html b/netbox/templates/dcim/device/interfaces.html index 8baf0786b..cab46886b 100644 --- a/netbox/templates/dcim/device/interfaces.html +++ b/netbox/templates/dcim/device/interfaces.html @@ -1,67 +1,28 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} +{% extends 'dcim/device/components_base.html' %} {% load helpers %} -{% load static %} {% load i18n %} -{% block content %} - {% include 'dcim/device/inc/interface_table_controls.html' with table_modal="DeviceInterfaceTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/inventory.html b/netbox/templates/dcim/device/inventory.html index ceebc50ee..15d70d959 100644 --- a/netbox/templates/dcim/device/inventory.html +++ b/netbox/templates/dcim/device/inventory.html @@ -1,51 +1,14 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} -{% load helpers %} -{% load static %} +{% extends 'dcim/device/components_base.html' %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DeviceInventoryItemTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} + {% endif %} +{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/modulebays.html b/netbox/templates/dcim/device/modulebays.html index b5f1df526..b218242be 100644 --- a/netbox/templates/dcim/device/modulebays.html +++ b/netbox/templates/dcim/device/modulebays.html @@ -1,47 +1,14 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} -{% load helpers %} -{% load static %} +{% extends 'dcim/device/components_base.html' %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DeviceModuleBayTable_config" %} - - - {% table_config_form table %} -{% endblock %} + {% endif %} +{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/poweroutlets.html b/netbox/templates/dcim/device/poweroutlets.html index 41960585b..34f51e389 100644 --- a/netbox/templates/dcim/device/poweroutlets.html +++ b/netbox/templates/dcim/device/poweroutlets.html @@ -1,58 +1,28 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} +{% extends 'dcim/device/components_base.html' %} {% load helpers %} -{% load static %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerOutletTable_config" %} - - -{% endblock %} + {% endwith %} +{% endblock bulk_delete_controls %} -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% block bulk_extra_controls %} + {{ block.super }} + {% if perms.dcim.add_poweroutlet %} + + {% endif %} +{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/powerports.html b/netbox/templates/dcim/device/powerports.html index 5301319da..ee9d57fd2 100644 --- a/netbox/templates/dcim/device/powerports.html +++ b/netbox/templates/dcim/device/powerports.html @@ -1,58 +1,28 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} +{% extends 'dcim/device/components_base.html' %} {% load helpers %} -{% load static %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerPortTable_config" %} - - -{% endblock %} + {% endwith %} +{% endblock bulk_delete_controls %} -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% block bulk_extra_controls %} + {{ block.super }} + {% if perms.dcim.add_powerport %} + + {% endif %} +{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/rearports.html b/netbox/templates/dcim/device/rearports.html index e81d915be..0c7781005 100644 --- a/netbox/templates/dcim/device/rearports.html +++ b/netbox/templates/dcim/device/rearports.html @@ -1,58 +1,28 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} -{% load static %} +{% extends 'dcim/device/components_base.html' %} {% load helpers %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DeviceRearPortTable_config" %} - - -{% endblock %} + {% endwith %} +{% endblock bulk_delete_controls %} -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% block bulk_extra_controls %} + {{ block.super }} + {% if perms.dcim.add_rearport %} + + {% endif %} +{% endblock bulk_extra_controls %} \ No newline at end of file diff --git a/netbox/templates/dcim/rack/non_racked_devices.html b/netbox/templates/dcim/rack/non_racked_devices.html index 700c66369..e52b8647f 100644 --- a/netbox/templates/dcim/rack/non_racked_devices.html +++ b/netbox/templates/dcim/rack/non_racked_devices.html @@ -1,5 +1,4 @@ -{% extends 'dcim/rack/base.html' %} -{% load helpers %} +{% extends 'generic/object_children.html' %} {% block extra_controls %} {% if perms.dcim.add_device %} @@ -10,42 +9,4 @@ {% endif %} -{% endblock %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %} - - -{% endblock content %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% endblock extra_controls %} diff --git a/netbox/templates/dcim/rack/reservations.html b/netbox/templates/dcim/rack/reservations.html index 1995d6174..cca8800e7 100644 --- a/netbox/templates/dcim/rack/reservations.html +++ b/netbox/templates/dcim/rack/reservations.html @@ -1,44 +1,13 @@ -{% extends 'dcim/rack/base.html' %} -{% load helpers %} +{% extends 'generic/object_children.html' %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="RackReservationTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} + {% endif %} +{% endblock extra_controls %} diff --git a/netbox/templates/extras/customfieldchoiceset.html b/netbox/templates/extras/customfieldchoiceset.html index e77a8432d..7240a88bf 100644 --- a/netbox/templates/extras/customfieldchoiceset.html +++ b/netbox/templates/extras/customfieldchoiceset.html @@ -55,18 +55,14 @@+ + {% blocktrans %}No bookmarks have been added yet.{% endblocktrans %} +
{% endif %} diff --git a/netbox/templates/extras/imageattachment.html b/netbox/templates/extras/imageattachment.html new file mode 100644 index 000000000..1968344cc --- /dev/null +++ b/netbox/templates/extras/imageattachment.html @@ -0,0 +1,4 @@ +{% extends 'generic/object.html' %} + +{% block tabs %} +{% endblock %} diff --git a/netbox/templates/extras/schema/devicetype_schema.jinja2 b/netbox/templates/extras/schema/devicetype_schema.jinja2 new file mode 100644 index 000000000..b08ab24de --- /dev/null +++ b/netbox/templates/extras/schema/devicetype_schema.jinja2 @@ -0,0 +1,93 @@ +{ + "type": "object", + "additionalProperties": false, + "definitions": { + "airflow": { + "type": "string", + "enum": {{ airflow_choices }} + }, + "weight-unit": { + "type": "string", + "enum": {{ weight_unit_choices }} + }, + "subdevice-role": { + "type": "string", + "enum": {{ subdevice_role_choices }} + }, + "console-port": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": {{ console_port_type_choices }} + } + } + }, + "console-server-port": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": {{ console_server_port_type_choices }} + } + } + }, + "power-port": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": {{ power_port_type_choices }} + } + } + }, + "power-outlet": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": {{ power_outlet_type_choices }} + }, + "feed-leg": { + "type": "string", + "enum": {{ power_outlet_feedleg_choices }} + } + } + }, + "interface": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": {{ interface_type_choices }} + }, + "poe_mode": { + "type": "string", + "enum": {{ interface_poe_mode_choices }} + }, + "poe_type": { + "type": "string", + "enum": {{ interface_poe_type_choices }} + } + } + }, + "front-port": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": {{ front_port_type_choices }} + } + } + }, + "rear-port": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": {{ rear_port_type_choices}} + } + } + } + } +} diff --git a/netbox/templates/generic/object.html b/netbox/templates/generic/object.html index efdd31db7..90a37cca5 100644 --- a/netbox/templates/generic/object.html +++ b/netbox/templates/generic/object.html @@ -60,7 +60,7 @@ Context: {# Extra buttons #} {% block extra_controls %}{% endblock %} - {% if perms.extras.add_bookmark %} + {% if perms.extras.add_bookmark and object.bookmarks %} {% bookmark_button object %} {% endif %} {% if request.user|can_add:object %} diff --git a/netbox/templates/generic/object_children.html b/netbox/templates/generic/object_children.html new file mode 100644 index 000000000..0fa59d1ec --- /dev/null +++ b/netbox/templates/generic/object_children.html @@ -0,0 +1,58 @@ +{% extends base_template %} +{% load helpers %} +{% load i18n %} + +{% block content %} + {% include 'inc/table_controls_htmx.html' with table_modal=table_config %} + +{% endblock content %} + +{% block modals %} + {{ block.super }} + {% table_config_form table %} +{% endblock modals %} diff --git a/netbox/templates/ipam/aggregate/prefixes.html b/netbox/templates/ipam/aggregate/prefixes.html index 37084f9f3..e1e53962d 100644 --- a/netbox/templates/ipam/aggregate/prefixes.html +++ b/netbox/templates/ipam/aggregate/prefixes.html @@ -1,5 +1,4 @@ -{% extends 'ipam/aggregate/base.html' %} -{% load helpers %} +{% extends 'generic/object_children.html' %} {% load i18n %} {% block extra_controls %} @@ -10,38 +9,4 @@ {% endif %} {{ block.super }} -{% endblock %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% endblock extra_controls %} diff --git a/netbox/templates/ipam/asnrange/asns.html b/netbox/templates/ipam/asnrange/asns.html deleted file mode 100644 index c0f2a15ba..000000000 --- a/netbox/templates/ipam/asnrange/asns.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends 'ipam/asnrange/base.html' %} -{% load helpers %} -{% load i18n %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="ASNTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} diff --git a/netbox/templates/ipam/ipaddress/ip_addresses.html b/netbox/templates/ipam/ipaddress/ip_addresses.html deleted file mode 100644 index b82ec2375..000000000 --- a/netbox/templates/ipam/ipaddress/ip_addresses.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends 'ipam/ipaddress/base.html' %} -{% load helpers %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %} - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} diff --git a/netbox/templates/ipam/iprange/ip_addresses.html b/netbox/templates/ipam/iprange/ip_addresses.html index f3bc0666f..a9baccc39 100644 --- a/netbox/templates/ipam/iprange/ip_addresses.html +++ b/netbox/templates/ipam/iprange/ip_addresses.html @@ -1,45 +1,10 @@ -{% extends 'ipam/iprange/base.html' %} -{% load helpers %} +{% extends 'generic/object_children.html' %} {% load i18n %} {% block extra_controls %} - {% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and object.first_available_ip %} + {% if perms.ipam.add_ipaddress and object.first_available_ip %} {% trans "Add IP Address" %} {% endif %} -{% endblock %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% endblock extra_controls %} diff --git a/netbox/templates/ipam/prefix/ip_addresses.html b/netbox/templates/ipam/prefix/ip_addresses.html index ac02590f6..51ebcc730 100644 --- a/netbox/templates/ipam/prefix/ip_addresses.html +++ b/netbox/templates/ipam/prefix/ip_addresses.html @@ -1,5 +1,4 @@ -{% extends 'ipam/prefix/base.html' %} -{% load helpers %} +{% extends 'generic/object_children.html' %} {% load i18n %} {% block extra_controls %} @@ -8,38 +7,4 @@ {% trans "Add IP Address" %} {% endif %} -{% endblock %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% endblock extra_controls %} diff --git a/netbox/templates/ipam/prefix/ip_ranges.html b/netbox/templates/ipam/prefix/ip_ranges.html index 6d2910c64..3bfba921f 100644 --- a/netbox/templates/ipam/prefix/ip_ranges.html +++ b/netbox/templates/ipam/prefix/ip_ranges.html @@ -1,5 +1,4 @@ -{% extends 'ipam/prefix/base.html' %} -{% load helpers %} +{% extends 'generic/object_children.html' %} {% load i18n %} {% block extra_controls %} @@ -8,38 +7,4 @@ {% trans "Add IP Range" %} {% endif %} -{% endblock %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="IPRangeTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% endblock extra_controls %} diff --git a/netbox/templates/ipam/prefix/prefixes.html b/netbox/templates/ipam/prefix/prefixes.html index 189a714e1..b8c2fd31c 100644 --- a/netbox/templates/ipam/prefix/prefixes.html +++ b/netbox/templates/ipam/prefix/prefixes.html @@ -1,5 +1,4 @@ -{% extends 'ipam/prefix/base.html' %} -{% load helpers %} +{% extends 'generic/object_children.html' %} {% load i18n %} {% block extra_controls %} @@ -9,39 +8,4 @@ {% trans "Add Prefix" %} {% endif %} - {{ block.super }} -{% endblock %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% endblock extra_controls %} diff --git a/netbox/templates/ipam/vlan/interfaces.html b/netbox/templates/ipam/vlan/interfaces.html deleted file mode 100644 index f7bcc8563..000000000 --- a/netbox/templates/ipam/vlan/interfaces.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends 'ipam/vlan/base.html' %} -{% load helpers %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="VLANDevicesTable_config" %} - - -{% endblock content %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} diff --git a/netbox/templates/ipam/vlan/vminterfaces.html b/netbox/templates/ipam/vlan/vminterfaces.html deleted file mode 100644 index a485b33eb..000000000 --- a/netbox/templates/ipam/vlan/vminterfaces.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends 'ipam/vlan/base.html' %} -{% load helpers %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="VLANVirtualMachinesTable_config" %} - - -{% endblock content %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} diff --git a/netbox/templates/tenancy/object_contacts.html b/netbox/templates/tenancy/object_contacts.html index 47f9f57bc..6e4814d8e 100644 --- a/netbox/templates/tenancy/object_contacts.html +++ b/netbox/templates/tenancy/object_contacts.html @@ -1,4 +1,4 @@ -{% extends base_template %} +{% extends 'generic/object_children.html' %} {% load helpers %} {% load i18n %} @@ -11,20 +11,3 @@ {% endwith %} {% endif %} {% endblock %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="ContactAssignmentTable_config" %} - -{% endblock content %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} diff --git a/netbox/templates/virtualization/cluster/devices.html b/netbox/templates/virtualization/cluster/devices.html index 077611e24..caf0af0de 100644 --- a/netbox/templates/virtualization/cluster/devices.html +++ b/netbox/templates/virtualization/cluster/devices.html @@ -1,31 +1,13 @@ -{% extends 'virtualization/cluster/base.html' %} -{% load helpers %} -{% load render_table from django_tables2 %} +{% extends 'generic/object_children.html' %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %} - - -{% endblock content %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% block bulk_delete_controls %} + {{ block.super }} + {% if 'bulk_remove_devices' in actions %} + + {% endif %} +{% endblock bulk_delete_controls %} diff --git a/netbox/templates/virtualization/cluster/virtual_machines.html b/netbox/templates/virtualization/cluster/virtual_machines.html deleted file mode 100644 index 2c83382fc..000000000 --- a/netbox/templates/virtualization/cluster/virtual_machines.html +++ /dev/null @@ -1,36 +0,0 @@ -{% extends 'virtualization/cluster/base.html' %} -{% load helpers %} -{% load render_table from django_tables2 %} -{% load i18n %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineTable_config" %} - - -{% endblock content %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 04e038b92..27f5ea114 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -43,6 +43,10 @@ {{ object.tenant|linkify|placeholder }} +{% trans "Config Template" %} | +{{ config_template|linkify|placeholder }} | +
---|---|
{% trans "Data Source" %} | +{{ config_template.data_file.source|linkify|placeholder }} | +
{% trans "Data File" %} | +{{ config_template.data_file|linkify|placeholder }} | +
{{ context_data|pprint }}+
{{ rendered_config }}+ {% else %} +