Skip to content

Commit f601c6c

Browse files
committed
Merge pull request encode#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

+5-1
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

+20-1
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

+25-3
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

+19-2
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

+1-1
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

+1-1
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

+1-1
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

+1-1
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

+1-1
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

+1-1
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)