Skip to content

Commit f601c6c

Browse files
committed
Merge pull request #3313 from tomchristie/limit-selects
Limit rendering of relational selects to max 1000 items by default.
2 parents 490f0c9 + c271568 commit f601c6c

File tree

10 files changed

+75
-13
lines changed

10 files changed

+75
-13
lines changed

docs/api-guide/fields.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ Two options are currently used in HTML form generation, `'input_type'` and `'bas
100100
style = {'base_template': 'radio.html'}
101101
}
102102

103-
**Note**: The `style` argument replaces the old-style version 2.x `widget` keyword argument. Because REST framework 3 now uses templated HTML form generation, the `widget` option that was used to support Django built-in widgets can no longer be supported. Version 3.1 is planned to include public API support for customizing HTML form generation.
103+
**Note**: The `style` argument replaces the old-style version 2.x `widget` keyword argument. Because REST framework 3 now uses templated HTML form generation, the `widget` option that was used to support Django built-in widgets can no longer be supported. Version 3.3 is planned to include public API support for customizing HTML form generation.
104104

105105
---
106106

@@ -364,6 +364,8 @@ Used by `ModelSerializer` to automatically generate fields if the corresponding
364364

365365
- `choices` - A list of valid values, or a list of `(key, display_name)` tuples.
366366
- `allow_blank` - If set to `True` then the empty string should be considered a valid value. If set to `False` then the empty string is considered invalid and will raise a validation error. Defaults to `False`.
367+
- `html_cutoff` - If set this will be the maximum number of choices that will be displayed by a HTML select drop down. Can be used to ensure that automatically generated ChoiceFields with very large possible selections do not prevent a template from rendering. Defaults to `None`.
368+
- `html_cutoff_text` - If set this will display a textual indicator if the maximum number of items have been cutoff in an HTML select drop down. Defaults to `"More than {count} items…"`
367369

368370
Both the `allow_blank` and `allow_null` are valid options on `ChoiceField`, although it is highly recommended that you only use one and not both. `allow_blank` should be preferred for textual choices, and `allow_null` should be preferred for numeric or other non-textual choices.
369371

@@ -375,6 +377,8 @@ A field that can accept a set of zero, one or many values, chosen from a limited
375377

376378
- `choices` - A list of valid values, or a list of `(key, display_name)` tuples.
377379
- `allow_blank` - If set to `True` then the empty string should be considered a valid value. If set to `False` then the empty string is considered invalid and will raise a validation error. Defaults to `False`.
380+
- `html_cutoff` - If set this will be the maximum number of choices that will be displayed by a HTML select drop down. Can be used to ensure that automatically generated ChoiceFields with very large possible selections do not prevent a template from rendering. Defaults to `None`.
381+
- `html_cutoff_text` - If set this will display a textual indicator if the maximum number of items have been cutoff in an HTML select drop down. Defaults to `"More than {count} items…"`
378382

379383
As with `ChoiceField`, both the `allow_blank` and `allow_null` options are valid, although it is highly recommended that you only use one and not both. `allow_blank` should be preferred for textual choices, and `allow_null` should be preferred for numeric or other non-textual choices.
380384

docs/api-guide/relations.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Relational fields are used to represent model relationships. They can be applie
1616

1717
---
1818

19-
#### Inspecting automatically generated relationships.
19+
#### Inspecting relationships.
2020

2121
When using the `ModelSerializer` class, serializer fields and relationships will be automatically generated for you. Inspecting these automatically generated fields can be a useful tool for determining how to customize the relationship style.
2222

@@ -442,6 +442,25 @@ To provide customized representations for such inputs, override `display_value()
442442
def display_value(self, instance):
443443
return 'Track: %s' % (instance.title)
444444

445+
## Select field cutoffs
446+
447+
When rendered in the browsable API relational fields will default to only displaying a maximum of 1000 selectable items. If more items are present then a disabled option with "More than 1000 items…" will be displayed.
448+
449+
This behavior is intended to prevent a template from being unable to render in an acceptable timespan due to a very large number of relationships being displayed.
450+
451+
There are two keyword arguments you can use to control this behavior:
452+
453+
- `html_cutoff` - If set this will be the maximum number of choices that will be displayed by a HTML select drop down. Set to `None` to disable any limiting. Defaults to `1000`.
454+
- `html_cutoff_text` - If set this will display a textual indicator if the maximum number of items have been cutoff in an HTML select drop down. Defaults to `"More than {count} items…"`
455+
456+
In cases where the cutoff is being enforced you may want to instead use a plain input field in the HTML form. You can do so using the `style` keyword argument. For example:
457+
458+
assigned_to = serializers.SlugRelatedField(
459+
queryset=User.objects.all(),
460+
slug field='username',
461+
style={'base_template': 'input.html'}
462+
)
463+
445464
## Reverse relations
446465

447466
Note that reverse relationships are not automatically included by the `ModelSerializer` and `HyperlinkedModelSerializer` classes. To include a reverse relationship, you must explicitly add it to the fields list. For example:

rest_framework/fields.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def flatten_choices_dict(choices):
156156
return ret
157157

158158

159-
def iter_options(grouped_choices):
159+
def iter_options(grouped_choices, cutoff=None, cutoff_text=None):
160160
"""
161161
Helper function for options and option groups in templates.
162162
"""
@@ -175,18 +175,32 @@ class Option(object):
175175
start_option_group = False
176176
end_option_group = False
177177

178-
def __init__(self, value, display_text):
178+
def __init__(self, value, display_text, disabled=False):
179179
self.value = value
180180
self.display_text = display_text
181+
self.disabled = disabled
182+
183+
count = 0
181184

182185
for key, value in grouped_choices.items():
186+
if cutoff and count >= cutoff:
187+
break
188+
183189
if isinstance(value, dict):
184190
yield StartOptionGroup(label=key)
185191
for sub_key, sub_value in value.items():
192+
if cutoff and count >= cutoff:
193+
break
186194
yield Option(value=sub_key, display_text=sub_value)
195+
count += 1
187196
yield EndOptionGroup()
188197
else:
189198
yield Option(value=key, display_text=value)
199+
count += 1
200+
201+
if cutoff and count >= cutoff and cutoff_text:
202+
cutoff_text = cutoff_text.format(count=cutoff)
203+
yield Option(value='n/a', display_text=cutoff_text, disabled=True)
190204

191205

192206
class CreateOnlyDefault(object):
@@ -1188,10 +1202,14 @@ class ChoiceField(Field):
11881202
default_error_messages = {
11891203
'invalid_choice': _('"{input}" is not a valid choice.')
11901204
}
1205+
html_cutoff = None
1206+
html_cutoff_text = _('More than {count} items...')
11911207

11921208
def __init__(self, choices, **kwargs):
11931209
self.grouped_choices = to_choices_dict(choices)
11941210
self.choices = flatten_choices_dict(self.grouped_choices)
1211+
self.html_cutoff = kwargs.pop('html_cutoff', self.html_cutoff)
1212+
self.html_cutoff_text = kwargs.pop('html_cutoff_text', self.html_cutoff_text)
11951213

11961214
# Map the string representation of choices to the underlying value.
11971215
# Allows us to deal with eg. integer choices while supporting either
@@ -1222,7 +1240,11 @@ def iter_options(self):
12221240
"""
12231241
Helper method for use with templates rendering select widgets.
12241242
"""
1225-
return iter_options(self.grouped_choices)
1243+
return iter_options(
1244+
self.grouped_choices,
1245+
cutoff=self.html_cutoff,
1246+
cutoff_text=self.html_cutoff_text
1247+
)
12261248

12271249

12281250
class MultipleChoiceField(ChoiceField):

rest_framework/relations.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,13 @@ def __init__(self, pk):
5454

5555
class RelatedField(Field):
5656
queryset = None
57+
html_cutoff = 1000
58+
html_cutoff_text = _('More than {count} items...')
5759

5860
def __init__(self, **kwargs):
5961
self.queryset = kwargs.pop('queryset', self.queryset)
62+
self.html_cutoff = kwargs.pop('html_cutoff', self.html_cutoff)
63+
self.html_cutoff_text = kwargs.pop('html_cutoff_text', self.html_cutoff_text)
6064
assert self.queryset is not None or kwargs.get('read_only', None), (
6165
'Relational field must provide a `queryset` argument, '
6266
'or set read_only=`True`.'
@@ -158,7 +162,11 @@ def grouped_choices(self):
158162
return self.choices
159163

160164
def iter_options(self):
161-
return iter_options(self.grouped_choices)
165+
return iter_options(
166+
self.grouped_choices,
167+
cutoff=self.html_cutoff,
168+
cutoff_text=self.html_cutoff_text
169+
)
162170

163171
def display_value(self, instance):
164172
return six.text_type(instance)
@@ -415,10 +423,15 @@ class ManyRelatedField(Field):
415423
'not_a_list': _('Expected a list of items but got type "{input_type}".'),
416424
'empty': _('This list may not be empty.')
417425
}
426+
html_cutoff = 1000
427+
html_cutoff_text = _('More than {count} items...')
418428

419429
def __init__(self, child_relation=None, *args, **kwargs):
420430
self.child_relation = child_relation
421431
self.allow_empty = kwargs.pop('allow_empty', True)
432+
self.html_cutoff = kwargs.pop('html_cutoff', self.html_cutoff)
433+
self.html_cutoff_text = kwargs.pop('html_cutoff_text', self.html_cutoff_text)
434+
422435
assert child_relation is not None, '`child_relation` is a required argument.'
423436
super(ManyRelatedField, self).__init__(*args, **kwargs)
424437
self.child_relation.bind(field_name='', parent=self)
@@ -469,4 +482,8 @@ def grouped_choices(self):
469482
return self.choices
470483

471484
def iter_options(self):
472-
return iter_options(self.grouped_choices)
485+
return iter_options(
486+
self.grouped_choices,
487+
cutoff=self.html_cutoff,
488+
cutoff_text=self.html_cutoff_text
489+
)

rest_framework/templates/rest_framework/horizontal/select.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
{% elif select.end_option_group %}
1717
</optgroup>
1818
{% else %}
19-
<option value="{{ select.value }}" {% if select.value == field.value %}selected{% endif %}>{{ select.display_text }}</option>
19+
<option value="{{ select.value }}" {% if select.value == field.value %}selected{% endif %} {% if select.disabled %}disabled{% endif %}>{{ select.display_text }}</option>
2020
{% endif %}
2121
{% endfor %}
2222
</select>

rest_framework/templates/rest_framework/horizontal/select_multiple.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
{% elif select.end_option_group %}
1717
</optgroup>
1818
{% else %}
19-
<option value="{{ select.value }}" {% if select.value in field.value %}selected{% endif %}>{{ select.display_text }}</option>
19+
<option value="{{ select.value }}" {% if select.value in field.value %}selected{% endif %} {% if select.disabled %}disabled{% endif %}>{{ select.display_text }}</option>
2020
{% endif %}
2121
{% empty %}
2222
<option>{{ no_items }}</option>

rest_framework/templates/rest_framework/inline/select.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
{% elif select.end_option_group %}
1616
</optgroup>
1717
{% else %}
18-
<option value="{{ select.value }}" {% if select.value == field.value %}selected{% endif %}>{{ select.display_text }}</option>
18+
<option value="{{ select.value }}" {% if select.value == field.value %}selected{% endif %} {% if select.disabled %}disabled{% endif %}>{{ select.display_text }}</option>
1919
{% endif %}
2020
{% endfor %}
2121
</select>

rest_framework/templates/rest_framework/inline/select_multiple.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
{% elif select.end_option_group %}
1616
</optgroup>
1717
{% else %}
18-
<option value="{{ select.value }}" {% if select.value in field.value %}selected{% endif %}>{{ select.display_text }}</option>
18+
<option value="{{ select.value }}" {% if select.value in field.value %}selected{% endif %} {% if select.disabled %}disabled{% endif %}>{{ select.display_text }}</option>
1919
{% endif %}
2020
{% empty %}
2121
<option>{{ no_items }}</option>

rest_framework/templates/rest_framework/vertical/select.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
{% elif select.end_option_group %}
1616
</optgroup>
1717
{% else %}
18-
<option value="{{ select.value }}" {% if select.value == field.value %}selected{% endif %}>{{ select.display_text }}</option>
18+
<option value="{{ select.value }}" {% if select.value == field.value %}selected{% endif %} {% if select.disabled %}disabled{% endif %}>{{ select.display_text }}</option>
1919
{% endif %}
2020
{% endfor %}
2121
</select>

rest_framework/templates/rest_framework/vertical/select_multiple.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
{% elif select.end_option_group %}
1616
</optgroup>
1717
{% else %}
18-
<option value="{{ select.value }}" {% if select.value in field.value %}selected{% endif %}>{{ select.display_text }}</option>
18+
<option value="{{ select.value }}" {% if select.value in field.value %}selected{% endif %} {% if select.disabled %}disabled{% endif %}>{{ select.display_text }}</option>
1919
{% endif %}
2020
{% empty %}
2121
<option>{{ no_items }}</option>

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy