Files
netbox/netbox/utilities/forms/widgets/misc.py
Jason Novinger d01d7b4156 Fixes #20551: Support quick-add form prefix in automatic slug generation (#20624)
* Fixes #20551: Support quick-add form prefix in automatic slug generation

The slug generation logic in `reslug.ts` looks for form fields using hard-coded ID selectors like `#id_slug` and `#id_name`. In quick-add modals, Django applies a `quickadd` prefix to form fields (introduced in #20542), resulting in IDs like `#id_quickadd-slug` and `#id_quickadd-name`. The logic couldn't find these prefixed fields, so automatic slug generation failed silently in quick-add modals. This fix updates the field selectors to try both unprefixed and prefixed patterns using the nullish coalescing operator (`??`), checking for the standard field ID first and falling back to the quickadd-prefixed ID if the standard one isn't found.

* Address PR feedback

The slug generation logic required updates to support form prefixes like `quickadd`. Python-side changes
ensure `SlugField.get_bound_field()` updates the `slug-source` attribute to include the form prefix when
present, so JavaScript receives the correct prefixed field ID. `SlugWidget.__init__()` now adds a
`slug-field` class to enable selector-based field discovery. On the frontend, `reslug.ts` now uses class
selectors (`button.reslug` and `input.slug-field`) instead of ID-based lookups, eliminating the need for
fallback logic. The template was updated to use `class="reslug"` instead of `id="reslug"` on the button to
avoid ID duplication issues.
2025-10-21 08:33:10 -04:00

88 lines
2.3 KiB
Python

from django import forms
__all__ = (
'ArrayWidget',
'ChoicesWidget',
'ClearableFileInput',
'MarkdownWidget',
'NumberWithOptions',
'SlugWidget',
)
class ClearableFileInput(forms.ClearableFileInput):
"""
Override Django's stock ClearableFileInput with a custom template.
"""
template_name = 'widgets/clearable_file_input.html'
class MarkdownWidget(forms.Textarea):
"""
Provide a live preview for Markdown-formatted content.
"""
template_name = 'widgets/markdown_input.html'
def __init__(self, attrs=None):
# Markdown fields should use monospace font
default_attrs = {
"class": "font-monospace",
}
if attrs:
default_attrs.update(attrs)
super().__init__(default_attrs)
class NumberWithOptions(forms.NumberInput):
"""
Number field with a dropdown pre-populated with common values for convenience.
"""
template_name = 'widgets/number_with_options.html'
def __init__(self, options, attrs=None):
self.options = options
super().__init__(attrs)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
context['widget']['options'] = self.options
return context
class SlugWidget(forms.TextInput):
"""
Subclass TextInput and add a slug regeneration button next to the form field.
"""
template_name = 'widgets/sluginput.html'
def __init__(self, attrs=None):
local_attrs = {} if attrs is None else attrs.copy()
if 'class' in local_attrs:
local_attrs['class'] = f"{local_attrs['class']} slug-field"
else:
local_attrs['class'] = 'slug-field'
super().__init__(local_attrs)
class ArrayWidget(forms.Textarea):
"""
Render each item of an array on a new line within a textarea for easy editing/
"""
def format_value(self, value):
if value is None or not len(value):
return None
return '\n'.join(value)
class ChoicesWidget(forms.Textarea):
"""
Render each key-value pair of a dictionary on a new line within a textarea for easy editing.
"""
def format_value(self, value):
if not value:
return None
if type(value) is list:
return '\n'.join([f'{k}:{v}' for k, v in value])
return value