mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-24 09:28:38 -06:00
Merge branch 'develop' into develop-2.9
This commit is contained in:
commit
5fd5dbab7b
@ -1,5 +1,14 @@
|
|||||||
# NetBox v2.8
|
# NetBox v2.8
|
||||||
|
|
||||||
|
## v2.8.7 (FUTURE)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#4821](https://github.com/netbox-community/netbox/issues/4821) - Restrict group options by selected site when bulk editing VLANs
|
||||||
|
* [#4835](https://github.com/netbox-community/netbox/issues/4835) - Support passing multiple initial values for multiple choice fields
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v2.8.7 (2020-07-02)
|
## v2.8.7 (2020-07-02)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
@ -17,6 +17,7 @@ from extras.models import (
|
|||||||
from extras.reports import get_report, get_reports, run_report
|
from extras.reports import get_report, get_reports, run_report
|
||||||
from extras.scripts import get_script, get_scripts, run_script
|
from extras.scripts import get_script, get_scripts, run_script
|
||||||
from utilities.api import IsAuthenticatedOrLoginNotRequired, ModelViewSet
|
from utilities.api import IsAuthenticatedOrLoginNotRequired, ModelViewSet
|
||||||
|
from utilities.metadata import ContentTypeMetadata
|
||||||
from utilities.utils import copy_safe_request
|
from utilities.utils import copy_safe_request
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
@ -90,6 +91,7 @@ class CustomFieldModelViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class GraphViewSet(ModelViewSet):
|
class GraphViewSet(ModelViewSet):
|
||||||
|
metadata_class = ContentTypeMetadata
|
||||||
queryset = Graph.objects.all()
|
queryset = Graph.objects.all()
|
||||||
serializer_class = serializers.GraphSerializer
|
serializer_class = serializers.GraphSerializer
|
||||||
filterset_class = filters.GraphFilterSet
|
filterset_class = filters.GraphFilterSet
|
||||||
@ -100,6 +102,7 @@ class GraphViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ExportTemplateViewSet(ModelViewSet):
|
class ExportTemplateViewSet(ModelViewSet):
|
||||||
|
metadata_class = ContentTypeMetadata
|
||||||
queryset = ExportTemplate.objects.all()
|
queryset = ExportTemplate.objects.all()
|
||||||
serializer_class = serializers.ExportTemplateSerializer
|
serializer_class = serializers.ExportTemplateSerializer
|
||||||
filterset_class = filters.ExportTemplateFilterSet
|
filterset_class = filters.ExportTemplateFilterSet
|
||||||
@ -122,6 +125,7 @@ class TagViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ImageAttachmentViewSet(ModelViewSet):
|
class ImageAttachmentViewSet(ModelViewSet):
|
||||||
|
metadata_class = ContentTypeMetadata
|
||||||
queryset = ImageAttachment.objects.all()
|
queryset = ImageAttachment.objects.all()
|
||||||
serializer_class = serializers.ImageAttachmentSerializer
|
serializer_class = serializers.ImageAttachmentSerializer
|
||||||
|
|
||||||
@ -325,6 +329,7 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet):
|
|||||||
"""
|
"""
|
||||||
Retrieve a list of recent changes.
|
Retrieve a list of recent changes.
|
||||||
"""
|
"""
|
||||||
|
metadata_class = ContentTypeMetadata
|
||||||
queryset = ObjectChange.objects.prefetch_related('user')
|
queryset = ObjectChange.objects.prefetch_related('user')
|
||||||
serializer_class = serializers.ObjectChangeSerializer
|
serializer_class = serializers.ObjectChangeSerializer
|
||||||
filterset_class = filters.ObjectChangeFilterSet
|
filterset_class = filters.ObjectChangeFilterSet
|
||||||
|
@ -1102,7 +1102,12 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
|||||||
)
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
filter_for={
|
||||||
|
'group': 'site_id'
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
group = DynamicModelChoiceField(
|
group = DynamicModelChoiceField(
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
|
@ -611,21 +611,24 @@ class DynamicModelChoiceMixin:
|
|||||||
filter = django_filters.ModelChoiceFilter
|
filter = django_filters.ModelChoiceFilter
|
||||||
widget = APISelect
|
widget = APISelect
|
||||||
|
|
||||||
def _get_initial_value(self, initial_data, field_name):
|
def filter_queryset(self, data):
|
||||||
return initial_data.get(field_name)
|
field_name = getattr(self, 'to_field_name') or 'pk'
|
||||||
|
# If multiple values have been provided, use only the last.
|
||||||
|
if type(data) in (list, tuple):
|
||||||
|
data = data[-1]
|
||||||
|
filter = self.filter(
|
||||||
|
field_name=field_name
|
||||||
|
)
|
||||||
|
return filter.filter(self.queryset, data)
|
||||||
|
|
||||||
def get_bound_field(self, form, field_name):
|
def get_bound_field(self, form, field_name):
|
||||||
bound_field = BoundField(form, self, field_name)
|
bound_field = BoundField(form, self, field_name)
|
||||||
|
|
||||||
# Override initial() to allow passing multiple values
|
|
||||||
bound_field.initial = self._get_initial_value(form.initial, field_name)
|
|
||||||
|
|
||||||
# Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
|
# Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
|
||||||
# will be populated on-demand via the APISelect widget.
|
# will be populated on-demand via the APISelect widget.
|
||||||
data = bound_field.value()
|
data = bound_field.value()
|
||||||
if data:
|
if data:
|
||||||
filter = self.filter(field_name=self.to_field_name or 'pk', queryset=self.queryset)
|
self.queryset = self.filter_queryset(data)
|
||||||
self.queryset = filter.filter(self.queryset, data)
|
|
||||||
else:
|
else:
|
||||||
self.queryset = self.queryset.none()
|
self.queryset = self.queryset.none()
|
||||||
|
|
||||||
@ -655,11 +658,16 @@ class DynamicModelMultipleChoiceField(DynamicModelChoiceMixin, forms.ModelMultip
|
|||||||
filter = django_filters.ModelMultipleChoiceFilter
|
filter = django_filters.ModelMultipleChoiceFilter
|
||||||
widget = APISelectMultiple
|
widget = APISelectMultiple
|
||||||
|
|
||||||
def _get_initial_value(self, initial_data, field_name):
|
def filter_queryset(self, data):
|
||||||
# If a QueryDict has been passed as initial form data, get *all* listed values
|
field_name = getattr(self, 'to_field_name') or 'pk'
|
||||||
if hasattr(initial_data, 'getlist'):
|
# Normalize data to a list
|
||||||
return initial_data.getlist(field_name)
|
if type(data) not in (list, tuple):
|
||||||
return initial_data.get(field_name)
|
data = [data]
|
||||||
|
filter = self.filter(
|
||||||
|
field_name=field_name,
|
||||||
|
lookup_expr='in'
|
||||||
|
)
|
||||||
|
return filter.filter(self.queryset, data)
|
||||||
|
|
||||||
|
|
||||||
class LaxURLField(forms.URLField):
|
class LaxURLField(forms.URLField):
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
|
from django.http import QueryDict
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from utilities.utils import deepmerge, dict_to_filter_params
|
from utilities.utils import deepmerge, dict_to_filter_params, normalize_querydict
|
||||||
|
|
||||||
|
|
||||||
class DictToFilterParamsTest(TestCase):
|
class DictToFilterParamsTest(TestCase):
|
||||||
"""
|
"""
|
||||||
Validate the operation of dict_to_filter_params().
|
Validate the operation of dict_to_filter_params().
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
|
||||||
return
|
|
||||||
|
|
||||||
def test_dict_to_filter_params(self):
|
def test_dict_to_filter_params(self):
|
||||||
|
|
||||||
input = {
|
input = {
|
||||||
@ -39,13 +37,21 @@ class DictToFilterParamsTest(TestCase):
|
|||||||
self.assertNotEqual(dict_to_filter_params(input), output)
|
self.assertNotEqual(dict_to_filter_params(input), output)
|
||||||
|
|
||||||
|
|
||||||
|
class NormalizeQueryDictTest(TestCase):
|
||||||
|
"""
|
||||||
|
Validate normalize_querydict() utility function.
|
||||||
|
"""
|
||||||
|
def test_normalize_querydict(self):
|
||||||
|
self.assertDictEqual(
|
||||||
|
normalize_querydict(QueryDict('foo=1&bar=2&bar=3&baz=')),
|
||||||
|
{'foo': '1', 'bar': ['2', '3'], 'baz': ''}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeepMergeTest(TestCase):
|
class DeepMergeTest(TestCase):
|
||||||
"""
|
"""
|
||||||
Validate the behavior of the deepmerge() utility.
|
Validate the behavior of the deepmerge() utility.
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
|
||||||
return
|
|
||||||
|
|
||||||
def test_deepmerge(self):
|
def test_deepmerge(self):
|
||||||
|
|
||||||
dict1 = {
|
dict1 = {
|
||||||
|
@ -152,6 +152,24 @@ def dict_to_filter_params(d, prefix=''):
|
|||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_querydict(querydict):
|
||||||
|
"""
|
||||||
|
Convert a QueryDict to a normal, mutable dictionary, preserving list values. For example,
|
||||||
|
|
||||||
|
QueryDict('foo=1&bar=2&bar=3&baz=')
|
||||||
|
|
||||||
|
becomes:
|
||||||
|
|
||||||
|
{'foo': '1', 'bar': ['2', '3'], 'baz': ''}
|
||||||
|
|
||||||
|
This function is necessary because QueryDict does not provide any built-in mechanism which preserves multiple
|
||||||
|
values.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
k: v if len(v) > 1 else v[0] for k, v in querydict.lists()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def deepmerge(original, new):
|
def deepmerge(original, new):
|
||||||
"""
|
"""
|
||||||
Deep merge two dictionaries (new into original) and return a new dict
|
Deep merge two dictionaries (new into original) and return a new dict
|
||||||
|
@ -31,7 +31,7 @@ from extras.querysets import CustomFieldQueryset
|
|||||||
from utilities.exceptions import AbortTransaction
|
from utilities.exceptions import AbortTransaction
|
||||||
from utilities.forms import BootstrapMixin, BulkRenameForm, CSVDataField, TableConfigForm, restrict_form_fields
|
from utilities.forms import BootstrapMixin, BulkRenameForm, CSVDataField, TableConfigForm, restrict_form_fields
|
||||||
from utilities.permissions import get_permission_for_model, resolve_permission
|
from utilities.permissions import get_permission_for_model, resolve_permission
|
||||||
from utilities.utils import csv_format, prepare_cloned_fields
|
from utilities.utils import csv_format, normalize_querydict, prepare_cloned_fields
|
||||||
from .error_handlers import handle_protectederror
|
from .error_handlers import handle_protectederror
|
||||||
from .forms import ConfirmationForm, ImportForm
|
from .forms import ConfirmationForm, ImportForm
|
||||||
from .paginator import EnhancedPaginator, get_paginate_count
|
from .paginator import EnhancedPaginator, get_paginate_count
|
||||||
@ -392,8 +392,7 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
|
obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
|
||||||
|
|
||||||
# Parse initial data manually to avoid setting field values as lists
|
initial_data = normalize_querydict(request.GET)
|
||||||
initial_data = {k: request.GET[k] for k in request.GET}
|
|
||||||
form = self.model_form(instance=obj, initial=initial_data)
|
form = self.model_form(instance=obj, initial=initial_data)
|
||||||
restrict_form_fields(form, request.user)
|
restrict_form_fields(form, request.user)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user