mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-24 17:38:37 -06:00
commit
80c8c4c4b2
20
CHANGELOG.md
20
CHANGELOG.md
@ -1,3 +1,23 @@
|
|||||||
|
v2.6.1 (2019-06-25)
|
||||||
|
|
||||||
|
## Enhancements
|
||||||
|
|
||||||
|
* [#3154](https://github.com/digitalocean/netbox/issues/3154) - Add `virtual_chassis_member` device filter
|
||||||
|
* [#3277](https://github.com/digitalocean/netbox/issues/3277) - Add cable trace buttons for console and power ports
|
||||||
|
* [#3281](https://github.com/digitalocean/netbox/issues/3281) - Hide custom links which render as empty text
|
||||||
|
|
||||||
|
## Bug Fixes
|
||||||
|
|
||||||
|
* [#3229](https://github.com/digitalocean/netbox/issues/3229) - Limit rack group selection by parent site on racks list
|
||||||
|
* [#3269](https://github.com/digitalocean/netbox/issues/3269) - Raise validation error when specifying non-existent cable terminations
|
||||||
|
* [#3275](https://github.com/digitalocean/netbox/issues/3275) - Fix error when adding power outlets to a device type
|
||||||
|
* [#3279](https://github.com/digitalocean/netbox/issues/3279) - Reset the PostgreSQL sequence for Tag and TaggedItem IDs
|
||||||
|
* [#3283](https://github.com/digitalocean/netbox/issues/3283) - Fix rack group assignment on PowerFeed CSV import
|
||||||
|
* [#3290](https://github.com/digitalocean/netbox/issues/3290) - Fix server error when viewing cascaded PDUs
|
||||||
|
* [#3292](https://github.com/digitalocean/netbox/issues/3292) - Ignore empty URL query parameters
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
v2.6.0 (2019-06-20)
|
v2.6.0 (2019-06-20)
|
||||||
|
|
||||||
## New Features
|
## New Features
|
||||||
|
@ -527,6 +527,10 @@ class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet):
|
|||||||
queryset=VirtualChassis.objects.all(),
|
queryset=VirtualChassis.objects.all(),
|
||||||
label='Virtual chassis (ID)',
|
label='Virtual chassis (ID)',
|
||||||
)
|
)
|
||||||
|
virtual_chassis_member = django_filters.BooleanFilter(
|
||||||
|
method='_virtual_chassis_member',
|
||||||
|
label='Is a virtual chassis member'
|
||||||
|
)
|
||||||
console_ports = django_filters.BooleanFilter(
|
console_ports = django_filters.BooleanFilter(
|
||||||
method='_console_ports',
|
method='_console_ports',
|
||||||
label='Has console ports',
|
label='Has console ports',
|
||||||
@ -590,6 +594,9 @@ class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet):
|
|||||||
Q(primary_ip6__isnull=False)
|
Q(primary_ip6__isnull=False)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _virtual_chassis_member(self, queryset, name, value):
|
||||||
|
return queryset.exclude(virtual_chassis__isnull=value)
|
||||||
|
|
||||||
def _console_ports(self, queryset, name, value):
|
def _console_ports(self, queryset, name, value):
|
||||||
return queryset.exclude(consoleports__isnull=value)
|
return queryset.exclude(consoleports__isnull=value)
|
||||||
|
|
||||||
|
@ -601,12 +601,18 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
|||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/dcim/sites/",
|
api_url="/api/dcim/sites/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
|
filter_for={
|
||||||
|
'group_id': 'site'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
group_id = FilterChoiceField(
|
group_id = ChainedModelChoiceField(
|
||||||
queryset=RackGroup.objects.select_related('site'),
|
|
||||||
label='Rack group',
|
label='Rack group',
|
||||||
null_label='-- None --',
|
queryset=RackGroup.objects.select_related('site'),
|
||||||
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
),
|
||||||
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/dcim/rack-groups/",
|
api_url="/api/dcim/rack-groups/",
|
||||||
null_option=True,
|
null_option=True,
|
||||||
@ -951,10 +957,6 @@ class PowerPortTemplateCreateForm(ComponentForm):
|
|||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
power_port = forms.ModelChoiceField(
|
|
||||||
queryset=PowerPortTemplate.objects.all(),
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutletTemplate
|
model = PowerOutletTemplate
|
||||||
@ -965,6 +967,21 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
'device_type': forms.HiddenInput(),
|
'device_type': forms.HiddenInput(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PowerOutletTemplateCreateForm(ComponentForm):
|
||||||
|
name_pattern = ExpandableNameField(
|
||||||
|
label='Name'
|
||||||
|
)
|
||||||
|
power_port = forms.ModelChoiceField(
|
||||||
|
queryset=PowerPortTemplate.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
feed_leg = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(POWERFEED_LEG_CHOICES),
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2()
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -975,12 +992,6 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateCreateForm(ComponentForm):
|
|
||||||
name_pattern = ExpandableNameField(
|
|
||||||
label='Name'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -1739,6 +1750,13 @@ class DeviceFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
|||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
virtual_chassis_member = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
label='Virtual chassis member',
|
||||||
|
widget=StaticSelect2(
|
||||||
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
|
)
|
||||||
|
)
|
||||||
console_ports = forms.NullBooleanField(
|
console_ports = forms.NullBooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
label='Has console ports',
|
label='Has console ports',
|
||||||
@ -3580,7 +3598,7 @@ class PowerFeedCSVForm(forms.ModelForm):
|
|||||||
# Validate rack
|
# Validate rack
|
||||||
if rack_name:
|
if rack_name:
|
||||||
try:
|
try:
|
||||||
self.instance.rack = Rack.objects.get(site=site, rack_group=rack_group, name=rack_name)
|
self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name)
|
||||||
except Rack.DoesNotExist:
|
except Rack.DoesNotExist:
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
"Rack {} not found in site {}, group {}".format(rack_name, site, rack_group)
|
"Rack {} not found in site {}, group {}".format(rack_name, site, rack_group)
|
||||||
|
@ -2747,55 +2747,69 @@ class Cable(ChangeLoggedModel):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
if self.termination_a and self.termination_b:
|
# Validate that termination A exists
|
||||||
|
try:
|
||||||
|
self.termination_a_type.model_class().objects.get(pk=self.termination_a_id)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise ValidationError({
|
||||||
|
'termination_a': 'Invalid ID for type {}'.format(self.termination_a_type)
|
||||||
|
})
|
||||||
|
|
||||||
type_a = self.termination_a_type.model
|
# Validate that termination B exists
|
||||||
type_b = self.termination_b_type.model
|
try:
|
||||||
|
self.termination_b_type.model_class().objects.get(pk=self.termination_b_id)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise ValidationError({
|
||||||
|
'termination_b': 'Invalid ID for type {}'.format(self.termination_b_type)
|
||||||
|
})
|
||||||
|
|
||||||
# Check that termination types are compatible
|
type_a = self.termination_a_type.model
|
||||||
if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a):
|
type_b = self.termination_b_type.model
|
||||||
raise ValidationError("Incompatible termination types: {} and {}".format(
|
|
||||||
self.termination_a_type, self.termination_b_type
|
|
||||||
))
|
|
||||||
|
|
||||||
# A termination point cannot be connected to itself
|
# Check that termination types are compatible
|
||||||
if self.termination_a == self.termination_b:
|
if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a):
|
||||||
raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type))
|
raise ValidationError("Incompatible termination types: {} and {}".format(
|
||||||
|
self.termination_a_type, self.termination_b_type
|
||||||
|
))
|
||||||
|
|
||||||
# A front port cannot be connected to its corresponding rear port
|
# A termination point cannot be connected to itself
|
||||||
if (
|
if self.termination_a == self.termination_b:
|
||||||
type_a in ['frontport', 'rearport'] and
|
raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type))
|
||||||
type_b in ['frontport', 'rearport'] and
|
|
||||||
(
|
|
||||||
getattr(self.termination_a, 'rear_port', None) == self.termination_b or
|
|
||||||
getattr(self.termination_b, 'rear_port', None) == self.termination_a
|
|
||||||
)
|
|
||||||
):
|
|
||||||
raise ValidationError("A front port cannot be connected to it corresponding rear port")
|
|
||||||
|
|
||||||
# Check for an existing Cable connected to either termination object
|
# A front port cannot be connected to its corresponding rear port
|
||||||
if self.termination_a.cable not in (None, self):
|
if (
|
||||||
raise ValidationError("{} already has a cable attached (#{})".format(
|
type_a in ['frontport', 'rearport'] and
|
||||||
self.termination_a, self.termination_a.cable_id
|
type_b in ['frontport', 'rearport'] and
|
||||||
))
|
(
|
||||||
if self.termination_b.cable not in (None, self):
|
getattr(self.termination_a, 'rear_port', None) == self.termination_b or
|
||||||
raise ValidationError("{} already has a cable attached (#{})".format(
|
getattr(self.termination_b, 'rear_port', None) == self.termination_a
|
||||||
self.termination_b, self.termination_b.cable_id
|
)
|
||||||
))
|
):
|
||||||
|
raise ValidationError("A front port cannot be connected to it corresponding rear port")
|
||||||
|
|
||||||
# Virtual interfaces cannot be connected
|
# Check for an existing Cable connected to either termination object
|
||||||
endpoint_a, endpoint_b, _ = self.get_path_endpoints()
|
if self.termination_a.cable not in (None, self):
|
||||||
if (
|
raise ValidationError("{} already has a cable attached (#{})".format(
|
||||||
(
|
self.termination_a, self.termination_a.cable_id
|
||||||
isinstance(endpoint_a, Interface) and
|
))
|
||||||
endpoint_a.type == IFACE_TYPE_VIRTUAL
|
if self.termination_b.cable not in (None, self):
|
||||||
) or
|
raise ValidationError("{} already has a cable attached (#{})".format(
|
||||||
(
|
self.termination_b, self.termination_b.cable_id
|
||||||
isinstance(endpoint_b, Interface) and
|
))
|
||||||
endpoint_b.type == IFACE_TYPE_VIRTUAL
|
|
||||||
)
|
# Virtual interfaces cannot be connected
|
||||||
):
|
endpoint_a, endpoint_b, _ = self.get_path_endpoints()
|
||||||
raise ValidationError("Cannot connect to a virtual interface")
|
if (
|
||||||
|
(
|
||||||
|
isinstance(endpoint_a, Interface) and
|
||||||
|
endpoint_a.type == IFACE_TYPE_VIRTUAL
|
||||||
|
) or
|
||||||
|
(
|
||||||
|
isinstance(endpoint_b, Interface) and
|
||||||
|
endpoint_b.type == IFACE_TYPE_VIRTUAL
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise ValidationError("Cannot connect to a virtual interface")
|
||||||
|
|
||||||
# Validate length and length_unit
|
# Validate length and length_unit
|
||||||
if self.length is not None and self.length_unit is None:
|
if self.length is not None and self.length_unit is None:
|
||||||
|
@ -433,7 +433,7 @@ class PowerOutletTemplateTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = PowerOutletTemplate
|
model = PowerOutletTemplate
|
||||||
fields = ('pk', 'name')
|
fields = ('pk', 'name', 'power_port', 'feed_leg')
|
||||||
empty_text = "None"
|
empty_text = "None"
|
||||||
|
|
||||||
|
|
||||||
|
@ -87,7 +87,8 @@ class CustomLinkForm(forms.ModelForm):
|
|||||||
model = CustomLink
|
model = CustomLink
|
||||||
exclude = []
|
exclude = []
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'text': 'Jinja2 template code for the link text. Reference the object as <code>{{ obj }}</code>.',
|
'text': 'Jinja2 template code for the link text. Reference the object as <code>{{ obj }}</code>. Links '
|
||||||
|
'which render as empty text will not be displayed.',
|
||||||
'url': 'Jinja2 template code for the link URL. Reference the object as <code>{{ obj }}</code>.',
|
'url': 'Jinja2 template code for the link URL. Reference the object as <code>{{ obj }}</code>.',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
14
netbox/extras/migrations/0023_fix_tag_sequences.py
Normal file
14
netbox/extras/migrations/0023_fix_tag_sequences.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0022_custom_links'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Update the last_value for tag Tag and TaggedItem ID sequences
|
||||||
|
migrations.RunSQL("SELECT setval('extras_tag_id_seq', (SELECT id FROM extras_tag ORDER BY id DESC LIMIT 1) + 1)"),
|
||||||
|
migrations.RunSQL("SELECT setval('extras_taggeditem_id_seq', (SELECT id FROM extras_taggeditem ORDER BY id DESC LIMIT 1) + 1)"),
|
||||||
|
]
|
@ -15,7 +15,8 @@ GROUP_BUTTON = '<div class="btn-group">\n' \
|
|||||||
'<button type="button" class="btn btn-sm btn-{} dropdown-toggle" data-toggle="dropdown">\n' \
|
'<button type="button" class="btn btn-sm btn-{} dropdown-toggle" data-toggle="dropdown">\n' \
|
||||||
'{} <span class="caret"></span>\n' \
|
'{} <span class="caret"></span>\n' \
|
||||||
'</button>\n' \
|
'</button>\n' \
|
||||||
'<ul class="dropdown-menu pull-right">\n'
|
'<ul class="dropdown-menu pull-right">\n' \
|
||||||
|
'{}</ul></div>'
|
||||||
GROUP_LINK = '<li><a href="{}"{}>{}</a></li>\n'
|
GROUP_LINK = '<li><a href="{}"{}>{}</a></li>\n'
|
||||||
|
|
||||||
|
|
||||||
@ -35,32 +36,40 @@ def custom_links(obj):
|
|||||||
template_code = ''
|
template_code = ''
|
||||||
group_names = OrderedDict()
|
group_names = OrderedDict()
|
||||||
|
|
||||||
# Organize custom links by group
|
|
||||||
for cl in custom_links:
|
for cl in custom_links:
|
||||||
|
|
||||||
|
# Organize custom links by group
|
||||||
if cl.group_name and cl.group_name in group_names:
|
if cl.group_name and cl.group_name in group_names:
|
||||||
group_names[cl.group_name].append(cl)
|
group_names[cl.group_name].append(cl)
|
||||||
elif cl.group_name:
|
elif cl.group_name:
|
||||||
group_names[cl.group_name] = [cl]
|
group_names[cl.group_name] = [cl]
|
||||||
|
|
||||||
# Add non-grouped links
|
# Add non-grouped links
|
||||||
for cl in custom_links:
|
else:
|
||||||
if not cl.group_name:
|
text_rendered = Environment().from_string(source=cl.text).render(**context)
|
||||||
link_target = ' target="_blank"' if cl.new_window else ''
|
if text_rendered:
|
||||||
template_code += LINK_BUTTON.format(
|
link_target = ' target="_blank"' if cl.new_window else ''
|
||||||
cl.url, link_target, cl.button_class, cl.text
|
template_code += LINK_BUTTON.format(
|
||||||
)
|
cl.url, link_target, cl.button_class, text_rendered
|
||||||
|
)
|
||||||
|
|
||||||
# Add grouped links to template
|
# Add grouped links to template
|
||||||
for group, links in group_names.items():
|
for group, links in group_names.items():
|
||||||
template_code += GROUP_BUTTON.format(
|
|
||||||
links[0].button_class, group
|
links_rendered = []
|
||||||
)
|
|
||||||
for cl in links:
|
for cl in links:
|
||||||
link_target = ' target="_blank"' if cl.new_window else ''
|
text_rendered = Environment().from_string(source=cl.text).render(**context)
|
||||||
template_code += GROUP_LINK.format(
|
if text_rendered:
|
||||||
cl.url, link_target, cl.text
|
link_target = ' target="_blank"' if cl.new_window else ''
|
||||||
|
links_rendered.append(
|
||||||
|
GROUP_LINK.format(cl.url, link_target, cl.text)
|
||||||
|
)
|
||||||
|
|
||||||
|
if links_rendered:
|
||||||
|
template_code += GROUP_BUTTON.format(
|
||||||
|
links[0].button_class, group, ''.join(links_rendered)
|
||||||
)
|
)
|
||||||
template_code += '</ul>\n</div>\n'
|
|
||||||
|
|
||||||
# Render template
|
# Render template
|
||||||
rendered = Environment().from_string(source=template_code).render(**context)
|
rendered = Environment().from_string(source=template_code).render(**context)
|
||||||
|
@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '2.6.0'
|
VERSION = '2.6.1'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
@ -346,7 +346,7 @@ else:
|
|||||||
REDIS_CACHE_CON_STRING = 'redis://'
|
REDIS_CACHE_CON_STRING = 'redis://'
|
||||||
|
|
||||||
if REDIS_PASSWORD:
|
if REDIS_PASSWORD:
|
||||||
REDIS_CACHE_CON_STRING = '{}{}@'.format(REDIS_CACHE_CON_STRING, REDIS_PASSWORD)
|
REDIS_CACHE_CON_STRING = '{}:{}@'.format(REDIS_CACHE_CON_STRING, REDIS_PASSWORD)
|
||||||
|
|
||||||
REDIS_CACHE_CON_STRING = '{}{}:{}/{}'.format(REDIS_CACHE_CON_STRING, REDIS_HOST, REDIS_PORT, REDIS_CACHE_DATABASE)
|
REDIS_CACHE_CON_STRING = '{}{}:{}/{}'.format(REDIS_CACHE_CON_STRING, REDIS_HOST, REDIS_PORT, REDIS_CACHE_DATABASE)
|
||||||
|
|
||||||
|
@ -359,7 +359,7 @@
|
|||||||
<td>{{ pp }}</td>
|
<td>{{ pp }}</td>
|
||||||
<td>{{ utilization.outlet_count }}</td>
|
<td>{{ utilization.outlet_count }}</td>
|
||||||
<td>{{ utilization.allocated }}VA</td>
|
<td>{{ utilization.allocated }}VA</td>
|
||||||
{% if powerfeed %}
|
{% if powerfeed.available_power %}
|
||||||
<td>{{ powerfeed.available_power }}VA</td>
|
<td>{{ powerfeed.available_power }}VA</td>
|
||||||
<td>{% utilization_graph utilization.allocated|percentage:powerfeed.available_power %}</td>
|
<td>{% utilization_graph utilization.allocated|percentage:powerfeed.available_power %}</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -15,6 +15,9 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if cp.cable %}
|
{% if cp.cable %}
|
||||||
<a href="{{ cp.cable.get_absolute_url }}">{{ cp.cable }}</a>
|
<a href="{{ cp.cable.get_absolute_url }}">{{ cp.cable }}</a>
|
||||||
|
<a href="{% url 'dcim:consoleport_trace' pk=cp.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||||
|
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
—
|
—
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -23,6 +23,9 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if pp.cable %}
|
{% if pp.cable %}
|
||||||
<a href="{{ pp.cable.get_absolute_url }}">{{ pp.cable }}</a>
|
<a href="{{ pp.cable.get_absolute_url }}">{{ pp.cable }}</a>
|
||||||
|
<a href="{% url 'dcim:powerport_trace' pk=pp.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||||
|
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
—
|
—
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -9,7 +9,7 @@ from extras.models import Tag
|
|||||||
def multivalue_field_factory(field_class):
|
def multivalue_field_factory(field_class):
|
||||||
"""
|
"""
|
||||||
Given a form field class, return a subclass capable of accepting multiple values. This allows us to OR on multiple
|
Given a form field class, return a subclass capable of accepting multiple values. This allows us to OR on multiple
|
||||||
filter values while maintaining the field's built-in vlaidation. Example: GET /api/dcim/devices/?name=foo&name=bar
|
filter values while maintaining the field's built-in validation. Example: GET /api/dcim/devices/?name=foo&name=bar
|
||||||
"""
|
"""
|
||||||
class NewField(field_class):
|
class NewField(field_class):
|
||||||
widget = forms.SelectMultiple
|
widget = forms.SelectMultiple
|
||||||
@ -17,7 +17,10 @@ def multivalue_field_factory(field_class):
|
|||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
if not value:
|
if not value:
|
||||||
return []
|
return []
|
||||||
return [super(field_class, self).to_python(v) for v in value]
|
return [
|
||||||
|
# Only append non-empty values (this avoids e.g. trying to cast '' as an integer)
|
||||||
|
super(field_class, self).to_python(v) for v in value if v
|
||||||
|
]
|
||||||
|
|
||||||
return type('MultiValue{}'.format(field_class.__name__), (NewField,), dict())
|
return type('MultiValue{}'.format(field_class.__name__), (NewField,), dict())
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user