From 189668fbfb51b5cf071e16662e53bff18c7aae64 Mon Sep 17 00:00:00 2001 From: "Daniel W. Anner" Date: Wed, 1 Mar 2023 18:20:37 +0000 Subject: [PATCH 01/10] Implemented PoE choice for IEEE 802.3az --- netbox/dcim/choices.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index f1485b67f..8c1888cc2 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1135,6 +1135,7 @@ class InterfacePoETypeChoices(ChoiceSet): TYPE_1_8023AF = 'type1-ieee802.3af' TYPE_2_8023AT = 'type2-ieee802.3at' + TYPE_2_8023AZ = 'type2-ieee802.3az' TYPE_3_8023BT = 'type3-ieee802.3bt' TYPE_4_8023BT = 'type4-ieee802.3bt' @@ -1149,6 +1150,7 @@ class InterfacePoETypeChoices(ChoiceSet): ( (TYPE_1_8023AF, '802.3af (Type 1)'), (TYPE_2_8023AT, '802.3at (Type 2)'), + (TYPE_2_8023AZ, '802.3az (Type 2)'), (TYPE_3_8023BT, '802.3bt (Type 3)'), (TYPE_4_8023BT, '802.3bt (Type 4)'), ) From 6640fc9eb73731bc2a1731e80ada853198f13db5 Mon Sep 17 00:00:00 2001 From: rmanyari Date: Wed, 1 Mar 2023 14:49:40 -0700 Subject: [PATCH 02/10] Fixes #11470: Validation and user friendly message on invalid address query param (#11858) * Fixes #11470: Validation and user friendly message on invalid address query param * Update invalid input handling to return empty set instead of raising exception --- netbox/ipam/filtersets.py | 27 +++++++++++++++++++++++++++ netbox/ipam/tests/test_filtersets.py | 21 +++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 2e9f56bbc..dbda8811f 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -16,6 +16,7 @@ from virtualization.models import VirtualMachine, VMInterface from .choices import * from .models import * +from rest_framework import serializers __all__ = ( 'AggregateFilterSet', @@ -599,7 +600,33 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet): return queryset.none() return queryset.filter(q) + def parse_inet_addresses(self, value): + ''' + Parse networks or IP addresses and cast to a format + acceptable by the Postgres inet type. + + Skips invalid values. + ''' + parsed = [] + for addr in value: + if netaddr.valid_ipv4(addr) or netaddr.valid_ipv6(addr): + parsed.append(addr) + continue + try: + network = netaddr.IPNetwork(addr) + parsed.append(str(network)) + except (AddrFormatError, ValueError): + continue + return parsed + def filter_address(self, queryset, name, value): + # Let's first parse the addresses passed + # as argument. If they are all invalid, + # we return an empty queryset + value = self.parse_inet_addresses(value) + if (len(value) == 0): + return queryset.none() + try: return queryset.filter(address__net_in=value) except ValidationError: diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index 13b3ae163..c53522d7a 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -10,6 +10,7 @@ from ipam.models import * from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from tenancy.models import Tenant, TenantGroup +from rest_framework import serializers class ASNTestCase(TestCase, ChangeLoggedFilterSetTests): @@ -851,6 +852,26 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'address': ['2001:db8::1/64', '2001:db8::1/65']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + # Check for valid edge cases. Note that Postgres inet type + # only accepts netmasks in the int form, so the filterset + # casts netmasks in the xxx.xxx.xxx.xxx format. + params = {'address': ['24']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0) + params = {'address': ['10.0.0.1/255.255.255.0']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'address': ['10.0.0.1/255.255.255.0', '10.0.0.1/25']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + # Check for invalid input. + params = {'address': ['/24']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0) + params = {'address': ['10.0.0.1/255.255.999.0']} # Invalid netmask + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0) + + # Check for partially invalid input. + params = {'address': ['10.0.0.1', '/24', '10.0.0.10/24']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_mask_length(self): params = {'mask_length': '24'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) From e270cb20ba40552947003f1c6779ef8c0559cf4f Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 1 Mar 2023 17:34:57 -0500 Subject: [PATCH 03/10] Changelog for #11470, #11871 --- docs/release-notes/version-3.4.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 3049853ea..b708947d6 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -8,9 +8,11 @@ * [#11011](https://github.com/netbox-community/netbox/issues/11011) - Add ability to toggle visibility of virtual interfaces under device view * [#11807](https://github.com/netbox-community/netbox/issues/11807) - Restore default page size when navigating between views * [#11817](https://github.com/netbox-community/netbox/issues/11817) - Add `connected_endpoints` field to GraphQL API for cabled objects +* [#11871](https://github.com/netbox-community/netbox/issues/11871) - Add IEEE 802.3az PoE type for interfaces ### Bug Fixes +* [#11470](https://github.com/netbox-community/netbox/issues/11470) - Avoid raising exception when filtering IPs by an invalid address * [#11565](https://github.com/netbox-community/netbox/issues/11565) - Apply custom field defaults to IP address created during FHRP group creation * [#11758](https://github.com/netbox-community/netbox/issues/11758) - Support non-URL-safe characters in plugin menu titles * [#11796](https://github.com/netbox-community/netbox/issues/11796) - When importing devices, restrict rack by location only if the location field is specified From 07b39fe44ad47148d786e1c17df8bc51ce930c27 Mon Sep 17 00:00:00 2001 From: Ximalas Date: Thu, 2 Mar 2023 14:59:08 +0100 Subject: [PATCH 04/10] Update choices.py: Adding Cisco StackWise-1T (#11886) Cisco Catalyst 9300X Series adds Cisco StackWise-1T. https://www.cisco.com/c/en/us/products/collateral/switches/catalyst-9300-series-switches/nb-06-cat9300-ser-data-sheet-cte-en.html --- netbox/dcim/choices.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 8c1888cc2..c495c42ec 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -902,6 +902,7 @@ class InterfaceTypeChoices(ChoiceSet): TYPE_STACKWISE160 = 'cisco-stackwise-160' TYPE_STACKWISE320 = 'cisco-stackwise-320' TYPE_STACKWISE480 = 'cisco-stackwise-480' + TYPE_STACKWISE1T = 'cisco-stackwise-1t' TYPE_JUNIPER_VCP = 'juniper-vcp' TYPE_SUMMITSTACK = 'extreme-summitstack' TYPE_SUMMITSTACK128 = 'extreme-summitstack-128' @@ -1078,6 +1079,7 @@ class InterfaceTypeChoices(ChoiceSet): (TYPE_STACKWISE160, 'Cisco StackWise-160'), (TYPE_STACKWISE320, 'Cisco StackWise-320'), (TYPE_STACKWISE480, 'Cisco StackWise-480'), + (TYPE_STACKWISE1T, 'Cisco StackWise-1T'), (TYPE_JUNIPER_VCP, 'Juniper VCP'), (TYPE_SUMMITSTACK, 'Extreme SummitStack'), (TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'), From d29a4a60f9e5d40698d48be452f4ba19ba8c8812 Mon Sep 17 00:00:00 2001 From: jose_d Date: Fri, 3 Mar 2023 17:22:38 +0100 Subject: [PATCH 05/10] README.md: typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 053aa8461..22e53f0da 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ as the cornerstone for network automation in thousands of organizations. * **Physical infrasucture:** Accurately model the physical world, from global regions down to individual racks of gear. Then connect everything - network, console, and power! * **Modern IPAM:** All the standard IPAM functionality you expect, plus VRF import/export tracking, VLAN management, and overlay support. -* **Data circuits:** Confidently manage the delivery of crtical circuits from various service providers, modeled seamlessly alongside your own infrastructure. +* **Data circuits:** Confidently manage the delivery of critical circuits from various service providers, modeled seamlessly alongside your own infrastructure. * **Power tracking:** Map the distribution of power from upstream sources to individual feeds and outlets. * **Organization:** Manage tenant and contact assignments natively. * **Powerful search:** Easily find anything you need using a single global search function. From ee5b707e688f62e3b08242fe2ea39e26b42d1534 Mon Sep 17 00:00:00 2001 From: Charly Forot <97433029+charlyforot@users.noreply.github.com> Date: Mon, 6 Mar 2023 16:46:33 +0100 Subject: [PATCH 06/10] README.md: typo infrasucture -> infrastructure --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22e53f0da..e3c9611c0 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ NetBox provides the ideal "source of truth" to power network automation. Available as open source software under the Apache 2.0 license, NetBox serves as the cornerstone for network automation in thousands of organizations. -* **Physical infrasucture:** Accurately model the physical world, from global regions down to individual racks of gear. Then connect everything - network, console, and power! +* **Physical infrastructure:** Accurately model the physical world, from global regions down to individual racks of gear. Then connect everything - network, console, and power! * **Modern IPAM:** All the standard IPAM functionality you expect, plus VRF import/export tracking, VLAN management, and overlay support. * **Data circuits:** Confidently manage the delivery of critical circuits from various service providers, modeled seamlessly alongside your own infrastructure. * **Power tracking:** Map the distribution of power from upstream sources to individual feeds and outlets. From d48a8770de78abf7f46004b3330f1e9f14f83755 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 7 Mar 2023 09:34:25 -0500 Subject: [PATCH 07/10] Fixes #11903: Fix escaping of return URL values for action buttons in tables --- docs/release-notes/version-3.4.md | 2 ++ netbox/netbox/tables/columns.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index b708947d6..171e5813d 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -8,6 +8,7 @@ * [#11011](https://github.com/netbox-community/netbox/issues/11011) - Add ability to toggle visibility of virtual interfaces under device view * [#11807](https://github.com/netbox-community/netbox/issues/11807) - Restore default page size when navigating between views * [#11817](https://github.com/netbox-community/netbox/issues/11817) - Add `connected_endpoints` field to GraphQL API for cabled objects +* [#11862](https://github.com/netbox-community/netbox/issues/11862) - Add Cisco StackWise 1T interface type * [#11871](https://github.com/netbox-community/netbox/issues/11871) - Add IEEE 802.3az PoE type for interfaces ### Bug Fixes @@ -16,6 +17,7 @@ * [#11565](https://github.com/netbox-community/netbox/issues/11565) - Apply custom field defaults to IP address created during FHRP group creation * [#11758](https://github.com/netbox-community/netbox/issues/11758) - Support non-URL-safe characters in plugin menu titles * [#11796](https://github.com/netbox-community/netbox/issues/11796) - When importing devices, restrict rack by location only if the location field is specified +* [#11903](https://github.com/netbox-community/netbox/issues/11903) - Fix escaping of return URL values for action buttons in tables --- diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index 519f6021e..66ee787a8 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from typing import Optional +from urllib.parse import quote import django_tables2 as tables from django.conf import settings @@ -8,7 +9,6 @@ from django.db.models import DateField, DateTimeField from django.template import Context, Template from django.urls import reverse from django.utils.dateparse import parse_date -from django.utils.encoding import escape_uri_path from django.utils.html import escape from django.utils.formats import date_format from django.utils.safestring import mark_safe @@ -235,7 +235,7 @@ class ActionsColumn(tables.Column): model = table.Meta.model request = getattr(table, 'context', {}).get('request') - url_appendix = f'?return_url={escape_uri_path(request.get_full_path())}' if request else '' + url_appendix = f'?return_url={quote(request.get_full_path())}' if request else '' html = '' # Compile actions menu From 33286aad391de51a460189649c5187ddbd8ed4ce Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 3 Mar 2023 00:41:38 +0530 Subject: [PATCH 08/10] added the missing filterset --- netbox/dcim/filtersets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 1ea56b3ef..493ccbbea 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1727,6 +1727,7 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet): class CableTerminationFilterSet(BaseFilterSet): + termination_type = ContentTypeFilter() class Meta: model = CableTermination From fa60f9d2a882df13cc8e8b47a4f227455897f815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aron=20Bergur=20J=C3=B3hannsson?= Date: Thu, 9 Mar 2023 13:21:13 +0000 Subject: [PATCH 09/10] Closes #11294: Markdown Preview (#11894) * MarkdownWidget * Change border and color of active markdown tab * Fix template name typo * Add render markdown endpoint * Static assets for markdown widget * widget style fix and unique ids based on name * Replace SmallTextArea with SmallMarkdownWidget * Clear innerHTML before swapping * render markdown directly in template * change render markdown view path * remove small markdown widget * Simplify rendering logic * Use a form to clean input Markdown data --------- Co-authored-by: Jeremy Stretch --- netbox/circuits/forms/bulk_edit.py | 5 +- netbox/dcim/forms/bulk_edit.py | 13 +---- netbox/extras/forms/__init__.py | 1 + netbox/extras/forms/misc.py | 14 ++++++ netbox/extras/urls.py | 2 + netbox/extras/views.py | 18 ++++++- netbox/ipam/forms/bulk_edit.py | 13 +---- netbox/project-static/dist/netbox-dark.css | Bin 374883 -> 375160 bytes netbox/project-static/dist/netbox-light.css | Bin 232430 -> 232605 bytes netbox/project-static/dist/netbox-print.css | Bin 726343 -> 726930 bytes netbox/project-static/dist/netbox.js | Bin 380966 -> 381466 bytes netbox/project-static/dist/netbox.js.map | Bin 353776 -> 354201 bytes netbox/project-static/src/buttons/index.ts | 2 + .../src/buttons/markdownPreview.ts | 45 ++++++++++++++++++ netbox/project-static/styles/netbox.scss | 29 ++++++++--- netbox/tenancy/forms/bulk_edit.py | 3 +- netbox/utilities/forms/fields/fields.py | 2 +- netbox/utilities/forms/widgets.py | 5 ++ .../templates/form_helpers/render_field.html | 2 +- .../templates/widgets/markdown_input.html | 22 +++++++++ netbox/virtualization/forms/bulk_edit.py | 4 +- netbox/wireless/forms/bulk_edit.py | 4 +- 22 files changed, 138 insertions(+), 46 deletions(-) create mode 100644 netbox/extras/forms/misc.py create mode 100644 netbox/project-static/src/buttons/markdownPreview.ts create mode 100644 netbox/utilities/templates/widgets/markdown_input.html diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index e1fe6338d..0449f8e99 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, SmallTextarea, + add_blank_choice, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect, ) @@ -35,7 +35,6 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label=_('Comments') ) @@ -63,7 +62,6 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label=_('Comments') ) @@ -125,7 +123,6 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label=_('Comments') ) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 38fa55738..bd466ca48 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,6 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -309,7 +308,6 @@ class RackBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -345,7 +343,6 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -406,7 +403,6 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -441,7 +437,6 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -551,7 +546,6 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -594,7 +588,6 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -644,7 +637,6 @@ class CableBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -668,7 +660,6 @@ class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -714,7 +705,6 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -776,7 +766,6 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label=_('Comments') ) diff --git a/netbox/extras/forms/__init__.py b/netbox/extras/forms/__init__.py index af0f7cf43..0825c9ca7 100644 --- a/netbox/extras/forms/__init__.py +++ b/netbox/extras/forms/__init__.py @@ -2,6 +2,7 @@ from .model_forms import * from .filtersets import * from .bulk_edit import * from .bulk_import import * +from .misc import * from .mixins import * from .config import * from .scripts import * diff --git a/netbox/extras/forms/misc.py b/netbox/extras/forms/misc.py new file mode 100644 index 000000000..b52338e76 --- /dev/null +++ b/netbox/extras/forms/misc.py @@ -0,0 +1,14 @@ +from django import forms + +__all__ = ( + 'RenderMarkdownForm', +) + + +class RenderMarkdownForm(forms.Form): + """ + Provides basic validation for markup to be rendered. + """ + text = forms.CharField( + required=False + ) diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index f41a45f5a..304e5b9ea 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -92,4 +92,6 @@ urlpatterns = [ path('scripts/results//', views.ScriptResultView.as_view(), name='script_result'), re_path(r'^scripts/(?P.([^.]+)).(?P.(.+))/', views.ScriptView.as_view(), name='script'), + # Markdown + path('render/markdown/', views.RenderMarkdownView.as_view(), name="render_markdown") ] diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 2d2608ae8..91d3b5c58 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -1,7 +1,7 @@ from django.contrib import messages 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, HttpResponseBadRequest, HttpResponseForbidden, HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.views.generic import View @@ -10,6 +10,7 @@ from rq import Worker from netbox.views import generic from utilities.htmx import is_htmx +from utilities.templatetags.builtins.filters import render_markdown from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict from utilities.views import ContentTypePermissionRequiredMixin, register_model_view from . import filtersets, forms, tables @@ -885,3 +886,18 @@ class JobResultBulkDeleteView(generic.BulkDeleteView): queryset = JobResult.objects.all() filterset = filtersets.JobResultFilterSet table = tables.JobResultTable + + +# +# Markdown +# + +class RenderMarkdownView(View): + + def post(self, request): + form = forms.RenderMarkdownForm(request.POST) + if not form.is_valid(): + HttpResponseBadRequest() + rendered = render_markdown(form.cleaned_data['text']) + + return HttpResponse(rendered) diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index d0af43975..63352698b 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,6 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -69,7 +68,6 @@ class RouteTargetBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -116,7 +114,6 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -145,7 +142,6 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -227,7 +223,6 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -266,7 +261,6 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -314,7 +308,6 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -359,7 +352,6 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -442,7 +434,6 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -474,7 +465,6 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -504,7 +494,6 @@ class L2VPNBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) diff --git a/netbox/project-static/dist/netbox-dark.css b/netbox/project-static/dist/netbox-dark.css index 8b6b37a4cc545892c5ab1d46c60ebe75f8bc5a40..74ab785a5794f756ce265e7c39fb9175b729c616 100644 GIT binary patch delta 139 zcmaF-LF~sTv4$4L7N!>F7M3ln+FKR%aubWPQ}WC6bjveS(o;(m^zstRbaOKEva6Hw zi&9dHrVF?+>GGqgn*Q36NqPE)HLP6hN%<+2x=HEN8ILfj@u8YEU3vqn3|JLgK~XAD Q(PYCZD%-WUvRW_$0C(my4*&oF delta 25 hcmezIN$l|lv4$4L7N!>F7M3ln+FQ3fY-6=x1^}5m3JL%K diff --git a/netbox/project-static/dist/netbox-light.css b/netbox/project-static/dist/netbox-light.css index 27e804c3084267eba5a70d9352ea5fb676a6d35f..f12291e44d1dc368699f6c51307d5d63d40dd267 100644 GIT binary patch delta 139 zcmaDiop0_;zJ?aY7N#xCOFFIfaubWPQ}WC6bjveS(o;(m^zstRbaOKEva6Hwi&9dH zbd&N+O7e593~JGpPT$(btUT>86PHp_eoCcoQo1IF5{PLfiAmERYBC9f)J}if$;`fe JaVN7k69A~~G`#=- delta 21 dcmbO`lkeShzJ?aY7N#xCOFFj)cQJc00RUrE2r2*o diff --git a/netbox/project-static/dist/netbox-print.css b/netbox/project-static/dist/netbox-print.css index f0220c0500b66f04fbdffb8e04f83bc86900c527..5d7f7342be9c1ee67fd170bc98a19cebc26f3aec 100644 GIT binary patch delta 302 zcmX@UN@vn~orV_17N!>F7M2#)7Pc1l7LFFq7OpMa|9J)UaubWPQ}WC6bjveS(o;*O ze_YO`$B!;J(UL{q9!*d|FE6o7HzzYMyE-YqC?&N>Hz~iQBtO^6pf;l~bS+R&eY(L~R!&J&iy$s8 gNlYrPo-SC!Da}?;l&YJQK3!3PQFQx%UTzO|02dEy?EnA( delta 37 tcmbQVUg!8KorV_17N!>F7M2#)7Pc1l7LFFq7OpMa|9Q8232=L`0|5V+3?Kji diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index ed6e127477872c2c111e2c18a555bdc54769df5f..f430604f967c52fa5ed13c20a60064e1cdff88ac 100644 GIT binary patch delta 17996 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 MAfWlv^U4VRe}M}_w*UYD delta 17547 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 M1lM}!Q8tMGA3BYxr2qf` diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index dd1ec47c90c838edcfb2385e1f8e8a65c560d54a..753576bc313c4c012b2bb99d14f14c2fd9eda6dc 100644 GIT binary patch delta 465 zcmYjNtxm&G6waUs6l!Q>6OLP;j^^A$S(>JlrdgKCWi+fCFjkUvU@8zOFo=b>z)d4i z2qZ6o;1M|Iv>_AsC*R-s?*092zkapfYvu z+FaSL^?q8-PdWxt#6tsJ#0vsK>Kw~G1fKzgGZ-Qc;m#mABwSy%22GI`3ij5(48vOK zb08|62%SJE$+6sc(oLmQC?XH~O>hDwPdd=ijUf^P`@0ZfD+kNd>4*D@$x)n_C?`JWeZhMNEoiSzmm423^ F*8vBm6S4pR diff --git a/netbox/project-static/src/buttons/index.ts b/netbox/project-static/src/buttons/index.ts index e677ff599..fe2ccaaef 100644 --- a/netbox/project-static/src/buttons/index.ts +++ b/netbox/project-static/src/buttons/index.ts @@ -4,6 +4,7 @@ import { initMoveButtons } from './moveOptions'; import { initReslug } from './reslug'; import { initSelectAll } from './selectAll'; import { initSelectMultiple } from './selectMultiple'; +import { initMarkdownPreviews } from './markdownPreview'; export function initButtons(): void { for (const func of [ @@ -13,6 +14,7 @@ export function initButtons(): void { initSelectAll, initSelectMultiple, initMoveButtons, + initMarkdownPreviews, ]) { func(); } diff --git a/netbox/project-static/src/buttons/markdownPreview.ts b/netbox/project-static/src/buttons/markdownPreview.ts new file mode 100644 index 000000000..224b2beab --- /dev/null +++ b/netbox/project-static/src/buttons/markdownPreview.ts @@ -0,0 +1,45 @@ +import { isTruthy } from 'src/util'; + +/** + * interface for htmx configRequest event + */ +declare global { + interface HTMLElementEventMap { + 'htmx:configRequest': CustomEvent<{ + parameters: Record; + headers: Record; + }>; + } +} + +function initMarkdownPreview(markdownWidget: HTMLDivElement) { + const previewButton = markdownWidget.querySelector('button.preview-button') as HTMLButtonElement; + const textarea = markdownWidget.querySelector('textarea') as HTMLTextAreaElement; + const preview = markdownWidget.querySelector('div.preview') as HTMLDivElement; + + /** + * Make sure the textarea has style attribute height + * So that it can be copied over to preview div. + */ + if (!isTruthy(textarea.style.height)) { + const { height } = textarea.getBoundingClientRect(); + textarea.style.height = `${height}px`; + } + + /** + * Add the value of the textarea to the body of the htmx request + * and copy the height of text are to the preview div + */ + previewButton.addEventListener('htmx:configRequest', e => { + e.detail.parameters = { text: textarea.value || '' }; + e.detail.headers['X-CSRFToken'] = window.CSRF_TOKEN; + preview.style.minHeight = textarea.style.height; + preview.innerHTML = ''; + }); +} + +export function initMarkdownPreviews(): void { + for (const markdownWidget of document.querySelectorAll('.markdown-widget')) { + initMarkdownPreview(markdownWidget); + } +} diff --git a/netbox/project-static/styles/netbox.scss b/netbox/project-static/styles/netbox.scss index e486bc7db..37f6c21c4 100644 --- a/netbox/project-static/styles/netbox.scss +++ b/netbox/project-static/styles/netbox.scss @@ -236,12 +236,12 @@ table { } th.asc > a::after { - content: "\f0140"; + content: '\f0140'; font-family: 'Material Design Icons'; } th.desc > a::after { - content: "\f0143"; + content: '\f0143'; font-family: 'Material Design Icons'; } @@ -416,18 +416,18 @@ nav.search { } } -// Styles for the quicksearch and its clear button; +// Styles for the quicksearch and its clear button; // Overrides input-group styles and adds transition effects .quicksearch { - input[type="search"] { - border-radius: $border-radius !important; + input[type='search'] { + border-radius: $border-radius !important; } button { margin-left: -32px !important; z-index: 100 !important; outline: none !important; - border-radius: $border-radius !important; + border-radius: $border-radius !important; transition: visibility 0s, opacity 0.2s linear; } @@ -998,9 +998,24 @@ div.card-overlay { padding: 8px; } +/* Markdown widget */ +.markdown-widget { + .nav-link { + border-bottom: 0; + + &.active { + background-color: var(--nbx-body-bg); + } + } + + .nav-tabs { + background-color: var(--nbx-pre-bg); + } +} + // Preformatted text blocks td pre { - margin-bottom: 0 + margin-bottom: 0; } pre.block { padding: $spacer; diff --git a/netbox/tenancy/forms/bulk_edit.py b/netbox/tenancy/forms/bulk_edit.py index 183a8e851..ab882fe7e 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,6 @@ class ContactBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) diff --git a/netbox/utilities/forms/fields/fields.py b/netbox/utilities/forms/fields/fields.py index bb6c3f73b..ee9543452 100644 --- a/netbox/utilities/forms/fields/fields.py +++ b/netbox/utilities/forms/fields/fields.py @@ -27,7 +27,7 @@ class CommentField(forms.CharField): """ A textarea with support for Markdown rendering. Exists mostly just to add a standard `help_text`. """ - widget = forms.Textarea + widget = widgets.MarkdownWidget help_text = f""" diff --git a/netbox/utilities/forms/widgets.py b/netbox/utilities/forms/widgets.py index 1802306f1..bd828bb8f 100644 --- a/netbox/utilities/forms/widgets.py +++ b/netbox/utilities/forms/widgets.py @@ -16,6 +16,7 @@ __all__ = ( 'ColorSelect', 'DatePicker', 'DateTimePicker', + 'MarkdownWidget', 'NumericArrayField', 'SelectDurationWidget', 'SelectSpeedWidget', @@ -116,6 +117,10 @@ class SelectDurationWidget(forms.NumberInput): template_name = 'widgets/select_duration.html' +class MarkdownWidget(forms.Textarea): + template_name = 'widgets/markdown_input.html' + + class NumericArrayField(SimpleArrayField): def clean(self, value): diff --git a/netbox/utilities/templates/form_helpers/render_field.html b/netbox/utilities/templates/form_helpers/render_field.html index ec9ceb09a..85c04df92 100644 --- a/netbox/utilities/templates/form_helpers/render_field.html +++ b/netbox/utilities/templates/form_helpers/render_field.html @@ -6,7 +6,7 @@ {# Render the field label, except for: #} {# 1. Checkboxes (label appears to the right of the field #} {# 2. Textareas with no label set (will expand across entire row) #} - {% if field|widget_type == 'checkboxinput' or field|widget_type == 'textarea' and not label %} + {% if field|widget_type == 'checkboxinput' or field|widget_type == 'textarea' or field|widget_type == 'markdownwidget' and not label %} {% else %}