Merge branch 'netbox-community:develop' into develop

This commit is contained in:
rmanyari 2023-03-09 14:32:47 -07:00 committed by GitHub
commit d9a1f5d06a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 153 additions and 50 deletions

View File

@ -13,9 +13,9 @@ NetBox provides the ideal "source of truth" to power network automation.
Available as open source software under the Apache 2.0 license, NetBox serves Available as open source software under the Apache 2.0 license, NetBox serves
as the cornerstone for network automation in thousands of organizations. 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. * **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. * **Power tracking:** Map the distribution of power from upstream sources to individual feeds and outlets.
* **Organization:** Manage tenant and contact assignments natively. * **Organization:** Manage tenant and contact assignments natively.
* **Powerful search:** Easily find anything you need using a single global search function. * **Powerful search:** Easily find anything you need using a single global search function.

View File

@ -6,14 +6,20 @@
* [#10058](https://github.com/netbox-community/netbox/issues/10058) - Enable searching for devices/VMs by primary IP address * [#10058](https://github.com/netbox-community/netbox/issues/10058) - Enable searching for devices/VMs by primary IP address
* [#11011](https://github.com/netbox-community/netbox/issues/11011) - Add ability to toggle visibility of virtual interfaces under device view * [#11011](https://github.com/netbox-community/netbox/issues/11011) - Add ability to toggle visibility of virtual interfaces under device view
* [#11294](https://github.com/netbox-community/netbox/issues/11294) - Enable live preview of Markdown content
* [#11807](https://github.com/netbox-community/netbox/issues/11807) - Restore default page size when navigating between views * [#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 * [#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 ### 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 * [#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 * [#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 * [#11796](https://github.com/netbox-community/netbox/issues/11796) - When importing devices, restrict rack by location only if the location field is specified
* [#11819](https://github.com/netbox-community/netbox/issues/11819) - Fix filtering of cable terminations by object type
* [#11903](https://github.com/netbox-community/netbox/issues/11903) - Fix escaping of return URL values for action buttons in tables
--- ---

View File

@ -7,7 +7,7 @@ from ipam.models import ASN
from netbox.forms import NetBoxModelBulkEditForm from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
add_blank_choice, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, add_blank_choice, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
StaticSelect, StaticSelect,
) )
@ -35,7 +35,6 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label=_('Comments') label=_('Comments')
) )
@ -63,7 +62,6 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label=_('Comments') label=_('Comments')
) )
@ -125,7 +123,6 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label=_('Comments') label=_('Comments')
) )

View File

@ -902,6 +902,7 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_STACKWISE160 = 'cisco-stackwise-160' TYPE_STACKWISE160 = 'cisco-stackwise-160'
TYPE_STACKWISE320 = 'cisco-stackwise-320' TYPE_STACKWISE320 = 'cisco-stackwise-320'
TYPE_STACKWISE480 = 'cisco-stackwise-480' TYPE_STACKWISE480 = 'cisco-stackwise-480'
TYPE_STACKWISE1T = 'cisco-stackwise-1t'
TYPE_JUNIPER_VCP = 'juniper-vcp' TYPE_JUNIPER_VCP = 'juniper-vcp'
TYPE_SUMMITSTACK = 'extreme-summitstack' TYPE_SUMMITSTACK = 'extreme-summitstack'
TYPE_SUMMITSTACK128 = 'extreme-summitstack-128' TYPE_SUMMITSTACK128 = 'extreme-summitstack-128'
@ -1078,6 +1079,7 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_STACKWISE160, 'Cisco StackWise-160'), (TYPE_STACKWISE160, 'Cisco StackWise-160'),
(TYPE_STACKWISE320, 'Cisco StackWise-320'), (TYPE_STACKWISE320, 'Cisco StackWise-320'),
(TYPE_STACKWISE480, 'Cisco StackWise-480'), (TYPE_STACKWISE480, 'Cisco StackWise-480'),
(TYPE_STACKWISE1T, 'Cisco StackWise-1T'),
(TYPE_JUNIPER_VCP, 'Juniper VCP'), (TYPE_JUNIPER_VCP, 'Juniper VCP'),
(TYPE_SUMMITSTACK, 'Extreme SummitStack'), (TYPE_SUMMITSTACK, 'Extreme SummitStack'),
(TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'), (TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'),
@ -1135,6 +1137,7 @@ class InterfacePoETypeChoices(ChoiceSet):
TYPE_1_8023AF = 'type1-ieee802.3af' TYPE_1_8023AF = 'type1-ieee802.3af'
TYPE_2_8023AT = 'type2-ieee802.3at' TYPE_2_8023AT = 'type2-ieee802.3at'
TYPE_2_8023AZ = 'type2-ieee802.3az'
TYPE_3_8023BT = 'type3-ieee802.3bt' TYPE_3_8023BT = 'type3-ieee802.3bt'
TYPE_4_8023BT = 'type4-ieee802.3bt' TYPE_4_8023BT = 'type4-ieee802.3bt'
@ -1149,6 +1152,7 @@ class InterfacePoETypeChoices(ChoiceSet):
( (
(TYPE_1_8023AF, '802.3af (Type 1)'), (TYPE_1_8023AF, '802.3af (Type 1)'),
(TYPE_2_8023AT, '802.3at (Type 2)'), (TYPE_2_8023AT, '802.3at (Type 2)'),
(TYPE_2_8023AZ, '802.3az (Type 2)'),
(TYPE_3_8023BT, '802.3bt (Type 3)'), (TYPE_3_8023BT, '802.3bt (Type 3)'),
(TYPE_4_8023BT, '802.3bt (Type 4)'), (TYPE_4_8023BT, '802.3bt (Type 4)'),
) )

View File

@ -1727,6 +1727,7 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
class CableTerminationFilterSet(BaseFilterSet): class CableTerminationFilterSet(BaseFilterSet):
termination_type = ContentTypeFilter()
class Meta: class Meta:
model = CableTermination model = CableTermination

View File

@ -11,7 +11,7 @@ from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField, add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, form_from_model, SmallTextarea, StaticSelect, SelectSpeedWidget, DynamicModelMultipleChoiceField, form_from_model, StaticSelect, SelectSpeedWidget
) )
__all__ = ( __all__ = (
@ -138,7 +138,6 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -309,7 +308,6 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -345,7 +343,6 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -406,7 +403,6 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -441,7 +437,6 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -551,7 +546,6 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -594,7 +588,6 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -644,7 +637,6 @@ class CableBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -668,7 +660,6 @@ class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -714,7 +705,6 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -776,7 +766,6 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label=_('Comments') label=_('Comments')
) )

View File

@ -2,6 +2,7 @@ from .model_forms import *
from .filtersets import * from .filtersets import *
from .bulk_edit import * from .bulk_edit import *
from .bulk_import import * from .bulk_import import *
from .misc import *
from .mixins import * from .mixins import *
from .config import * from .config import *
from .scripts import * from .scripts import *

View File

@ -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
)

View File

@ -92,4 +92,6 @@ urlpatterns = [
path('scripts/results/<int:job_result_pk>/', views.ScriptResultView.as_view(), name='script_result'), path('scripts/results/<int:job_result_pk>/', views.ScriptResultView.as_view(), name='script_result'),
re_path(r'^scripts/(?P<module>.([^.]+)).(?P<name>.(.+))/', views.ScriptView.as_view(), name='script'), re_path(r'^scripts/(?P<module>.([^.]+)).(?P<name>.(.+))/', views.ScriptView.as_view(), name='script'),
# Markdown
path('render/markdown/', views.RenderMarkdownView.as_view(), name="render_markdown")
] ]

View File

@ -1,7 +1,7 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Count, Q 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.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.views.generic import View from django.views.generic import View
@ -10,6 +10,7 @@ from rq import Worker
from netbox.views import generic from netbox.views import generic
from utilities.htmx import is_htmx 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.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
@ -885,3 +886,18 @@ class JobResultBulkDeleteView(generic.BulkDeleteView):
queryset = JobResult.objects.all() queryset = JobResult.objects.all()
filterset = filtersets.JobResultFilterSet filterset = filtersets.JobResultFilterSet
table = tables.JobResultTable 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)

View File

@ -10,7 +10,7 @@ from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
add_blank_choice, BulkEditNullBooleanSelect, CommentField, DynamicModelChoiceField, NumericArrayField, add_blank_choice, BulkEditNullBooleanSelect, CommentField, DynamicModelChoiceField, NumericArrayField,
SmallTextarea, StaticSelect, DynamicModelMultipleChoiceField, StaticSelect, DynamicModelMultipleChoiceField
) )
__all__ = ( __all__ = (
@ -48,7 +48,6 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -69,7 +68,6 @@ class RouteTargetBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -116,7 +114,6 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -145,7 +142,6 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -227,7 +223,6 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -266,7 +261,6 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -314,7 +308,6 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -359,7 +352,6 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -442,7 +434,6 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -474,7 +465,6 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -504,7 +494,6 @@ class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )

View File

@ -1,5 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
from urllib.parse import quote
import django_tables2 as tables import django_tables2 as tables
from django.conf import settings from django.conf import settings
@ -8,7 +9,6 @@ from django.db.models import DateField, DateTimeField
from django.template import Context, Template from django.template import Context, Template
from django.urls import reverse from django.urls import reverse
from django.utils.dateparse import parse_date from django.utils.dateparse import parse_date
from django.utils.encoding import escape_uri_path
from django.utils.html import escape from django.utils.html import escape
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -235,7 +235,7 @@ class ActionsColumn(tables.Column):
model = table.Meta.model model = table.Meta.model
request = getattr(table, 'context', {}).get('request') 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 = '' html = ''
# Compile actions menu # Compile actions menu

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4,6 +4,7 @@ import { initMoveButtons } from './moveOptions';
import { initReslug } from './reslug'; import { initReslug } from './reslug';
import { initSelectAll } from './selectAll'; import { initSelectAll } from './selectAll';
import { initSelectMultiple } from './selectMultiple'; import { initSelectMultiple } from './selectMultiple';
import { initMarkdownPreviews } from './markdownPreview';
export function initButtons(): void { export function initButtons(): void {
for (const func of [ for (const func of [
@ -13,6 +14,7 @@ export function initButtons(): void {
initSelectAll, initSelectAll,
initSelectMultiple, initSelectMultiple,
initMoveButtons, initMoveButtons,
initMarkdownPreviews,
]) { ]) {
func(); func();
} }

View File

@ -0,0 +1,45 @@
import { isTruthy } from 'src/util';
/**
* interface for htmx configRequest event
*/
declare global {
interface HTMLElementEventMap {
'htmx:configRequest': CustomEvent<{
parameters: Record<string, string>;
headers: Record<string, string>;
}>;
}
}
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<HTMLDivElement>('.markdown-widget')) {
initMarkdownPreview(markdownWidget);
}
}

View File

@ -236,12 +236,12 @@ table {
} }
th.asc > a::after { th.asc > a::after {
content: "\f0140"; content: '\f0140';
font-family: 'Material Design Icons'; font-family: 'Material Design Icons';
} }
th.desc > a::after { th.desc > a::after {
content: "\f0143"; content: '\f0143';
font-family: 'Material Design Icons'; font-family: 'Material Design Icons';
} }
@ -419,7 +419,7 @@ 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 // Overrides input-group styles and adds transition effects
.quicksearch { .quicksearch {
input[type="search"] { input[type='search'] {
border-radius: $border-radius !important; border-radius: $border-radius !important;
} }
@ -998,9 +998,24 @@ div.card-overlay {
padding: 8px; 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 // Preformatted text blocks
td pre { td pre {
margin-bottom: 0 margin-bottom: 0;
} }
pre.block { pre.block {
padding: $spacer; padding: $spacer;

View File

@ -2,7 +2,7 @@ from django import forms
from netbox.forms import NetBoxModelBulkEditForm from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import * from tenancy.models import *
from utilities.forms import CommentField, DynamicModelChoiceField, SmallTextarea from utilities.forms import CommentField, DynamicModelChoiceField
__all__ = ( __all__ = (
'ContactBulkEditForm', 'ContactBulkEditForm',
@ -106,7 +106,6 @@ class ContactBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )

View File

@ -27,7 +27,7 @@ class CommentField(forms.CharField):
""" """
A textarea with support for Markdown rendering. Exists mostly just to add a standard `help_text`. 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""" help_text = f"""
<i class="mdi mdi-information-outline"></i> <i class="mdi mdi-information-outline"></i>
<a href="{static('docs/reference/markdown/')}" target="_blank" tabindex="-1"> <a href="{static('docs/reference/markdown/')}" target="_blank" tabindex="-1">

View File

@ -16,6 +16,7 @@ __all__ = (
'ColorSelect', 'ColorSelect',
'DatePicker', 'DatePicker',
'DateTimePicker', 'DateTimePicker',
'MarkdownWidget',
'NumericArrayField', 'NumericArrayField',
'SelectDurationWidget', 'SelectDurationWidget',
'SelectSpeedWidget', 'SelectSpeedWidget',
@ -116,6 +117,10 @@ class SelectDurationWidget(forms.NumberInput):
template_name = 'widgets/select_duration.html' template_name = 'widgets/select_duration.html'
class MarkdownWidget(forms.Textarea):
template_name = 'widgets/markdown_input.html'
class NumericArrayField(SimpleArrayField): class NumericArrayField(SimpleArrayField):
def clean(self, value): def clean(self, value):

View File

@ -6,7 +6,7 @@
{# Render the field label, except for: #} {# Render the field label, except for: #}
{# 1. Checkboxes (label appears to the right of the field #} {# 1. Checkboxes (label appears to the right of the field #}
{# 2. Textareas with no label set (will expand across entire row) #} {# 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 %} {% else %}
<label for="{{ field.id_for_label }}" class="col-sm-3 col-form-label text-lg-end{% if field.field.required %} required{% endif %}"> <label for="{{ field.id_for_label }}" class="col-sm-3 col-form-label text-lg-end{% if field.field.required %} required{% endif %}">
{{ label }} {{ label }}

View File

@ -0,0 +1,22 @@
<div class="border rounded markdown-widget">
<ul class="nav nav-tabs px-3 pt-2 rounded-top border-0">
<li class="nav-item" role="presentation">
<button class="nav-link active " id="{{ widget.name }}-input-tab" data-bs-toggle="tab" data-bs-target="#{{ widget.name }}-input" type="button" role="tab" aria-controls="{{ widget.name }}-input" aria-selected="true">
Write
</button>
</li>
<li class="nav-item" role="presentation">
<button hx-target="#{{ widget.name }}-preview" hx-swap="innerHTML" hx-post="{% url 'extras:render_markdown' %}" class="nav-link preview-button" id="{{ widget.name }}-markdown-preview-tab" data-bs-toggle="tab" data-bs-target="#{{ widget.name }}-markdown-preview" type="button" role="tab" aria-controls="{{ widget.name }}-markdown-preview" aria-selected="false">
Preview
</button>
</li>
</ul>
<div class="tab-content bg-body rounded-bottom border-top">
<div class="tab-pane show active" id="{{ widget.name }}-input" role="tabpanel" aria-labelledby="{{ widget.name }}-input-tab">
{% include "django/forms/widgets/textarea.html" %}
</div>
<div class="tab-pane show" id="{{ widget.name }}-markdown-preview" role="tabpanel" aria-labelledby="{{ widget.name }}-markdown-preview-tab">
<div id="{{ widget.name }}-preview" class="preview px-3 py-2">Testing</div>
</div>
</div>
</div>

View File

@ -9,7 +9,7 @@ from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
add_blank_choice, BulkEditNullBooleanSelect, BulkRenameForm, CommentField, DynamicModelChoiceField, add_blank_choice, BulkEditNullBooleanSelect, BulkRenameForm, CommentField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, SmallTextarea, StaticSelect DynamicModelMultipleChoiceField, StaticSelect
) )
from virtualization.choices import * from virtualization.choices import *
from virtualization.models import * from virtualization.models import *
@ -90,7 +90,6 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label=_('Comments') label=_('Comments')
) )
@ -163,7 +162,6 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label=_('Comments') label=_('Comments')
) )

View File

@ -5,7 +5,7 @@ from dcim.choices import LinkStatusChoices
from ipam.models import VLAN from ipam.models import VLAN
from netbox.forms import NetBoxModelBulkEditForm from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import add_blank_choice, CommentField, DynamicModelChoiceField, SmallTextarea from utilities.forms import add_blank_choice, CommentField, DynamicModelChoiceField
from wireless.choices import * from wireless.choices import *
from wireless.constants import SSID_MAX_LENGTH from wireless.constants import SSID_MAX_LENGTH
from wireless.models import * from wireless.models import *
@ -74,7 +74,6 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )
@ -119,7 +118,6 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea,
label='Comments' label='Comments'
) )