From f4412d08275f74e71237cd948749cef616b7e85d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Aug 2015 10:52:44 +0100 Subject: [PATCH 1/4] Docs for select cutoffs --- docs/api-guide/fields.md | 6 +++- docs/api-guide/relations.md | 21 +++++++++++++- rest_framework/fields.py | 28 +++++++++++++++++-- rest_framework/relations.py | 21 ++++++++++++-- .../rest_framework/filters/ordering.html | 0 .../rest_framework/horizontal/select.html | 2 +- .../horizontal/select_multiple.html | 2 +- .../rest_framework/inline/select.html | 2 +- .../inline/select_multiple.html | 2 +- .../rest_framework/vertical/select.html | 2 +- .../vertical/select_multiple.html | 2 +- 11 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 rest_framework/templates/rest_framework/filters/ordering.html diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 55d73fbcb..2b9a6bba6 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -100,7 +100,7 @@ Two options are currently used in HTML form generation, `'input_type'` and `'bas style = {'base_template': 'radio.html'} } -**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. +**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. --- @@ -364,6 +364,8 @@ Used by `ModelSerializer` to automatically generate fields if the corresponding - `choices` - A list of valid values, or a list of `(key, display_name)` tuples. - `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`. +- `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`. +- `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…"` 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. @@ -375,6 +377,8 @@ A field that can accept a set of zero, one or many values, chosen from a limited - `choices` - A list of valid values, or a list of `(key, display_name)` tuples. - `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`. +- `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`. +- `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…"` 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. diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 61685e3b1..979c3326c 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -16,7 +16,7 @@ Relational fields are used to represent model relationships. They can be applie --- -#### Inspecting automatically generated relationships. +#### Inspecting relationships. 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. @@ -442,6 +442,25 @@ To provide customized representations for such inputs, override `display_value() def display_value(self, instance): return 'Track: %s' % (instance.title) +## Select field cutoffs + +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. + +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. + +There are two keyword arguments you can use to control this behavior: + +- `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`. +- `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…"` + +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: + + assigned_to = serializers.SlugRelatedField( + queryset=User.objects.all(), + slug field='username', + style={'base_template': 'input.html'} + ) + ## Reverse relations 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: diff --git a/rest_framework/fields.py b/rest_framework/fields.py index e2081abd4..a362c9d22 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -156,7 +156,7 @@ def flatten_choices_dict(choices): return ret -def iter_options(grouped_choices): +def iter_options(grouped_choices, cutoff=1000, cutoff_text=None): """ Helper function for options and option groups in templates. """ @@ -175,18 +175,32 @@ def iter_options(grouped_choices): start_option_group = False end_option_group = False - def __init__(self, value, display_text): + def __init__(self, value, display_text, disabled=False): self.value = value self.display_text = display_text + self.disabled = disabled + + count = 0 for key, value in grouped_choices.items(): + if cutoff and count >= cutoff: + break + if isinstance(value, dict): yield StartOptionGroup(label=key) for sub_key, sub_value in value.items(): + if cutoff and count >= cutoff: + break yield Option(value=sub_key, display_text=sub_value) + count += 1 yield EndOptionGroup() else: yield Option(value=key, display_text=value) + count += 1 + + if cutoff and count >= cutoff and cutoff_text: + cutoff_text = cutoff_text.format(count=cutoff) + yield Option(value='n/a', display_text=cutoff_text, disabled=True) class CreateOnlyDefault(object): @@ -1188,10 +1202,14 @@ class ChoiceField(Field): default_error_messages = { 'invalid_choice': _('"{input}" is not a valid choice.') } + html_cutoff = 1000 + html_cutoff_text = _('More than {count} items...') def __init__(self, choices, **kwargs): self.grouped_choices = to_choices_dict(choices) self.choices = flatten_choices_dict(self.grouped_choices) + self.html_cutoff = kwargs.pop('html_cutoff', self.html_cutoff) + self.html_cutoff_text = kwargs.pop('html_cutoff_text', self.html_cutoff_text) # Map the string representation of choices to the underlying value. # Allows us to deal with eg. integer choices while supporting either @@ -1222,7 +1240,11 @@ class ChoiceField(Field): """ Helper method for use with templates rendering select widgets. """ - return iter_options(self.grouped_choices) + return iter_options( + self.grouped_choices, + cutoff=self.html_cutoff, + cutoff_text=self.html_cutoff_text + ) class MultipleChoiceField(ChoiceField): diff --git a/rest_framework/relations.py b/rest_framework/relations.py index e0edc645f..56af657d9 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -54,9 +54,13 @@ MANY_RELATION_KWARGS = ( class RelatedField(Field): queryset = None + html_cutoff = 1000 + html_cutoff_text = _('More than {count} items...') def __init__(self, **kwargs): self.queryset = kwargs.pop('queryset', self.queryset) + self.html_cutoff = kwargs.pop('html_cutoff', self.html_cutoff) + self.html_cutoff_text = kwargs.pop('html_cutoff_text', self.html_cutoff_text) assert self.queryset is not None or kwargs.get('read_only', None), ( 'Relational field must provide a `queryset` argument, ' 'or set read_only=`True`.' @@ -158,7 +162,11 @@ class RelatedField(Field): return self.choices def iter_options(self): - return iter_options(self.grouped_choices) + return iter_options( + self.grouped_choices, + cutoff=self.html_cutoff, + cutoff_text=self.html_cutoff_text + ) def display_value(self, instance): return six.text_type(instance) @@ -415,10 +423,15 @@ class ManyRelatedField(Field): 'not_a_list': _('Expected a list of items but got type "{input_type}".'), 'empty': _('This list may not be empty.') } + html_cutoff = 1000 + html_cutoff_text = _('More than {count} items...') def __init__(self, child_relation=None, *args, **kwargs): self.child_relation = child_relation self.allow_empty = kwargs.pop('allow_empty', True) + self.html_cutoff = kwargs.pop('html_cutoff', self.html_cutoff) + self.html_cutoff_text = kwargs.pop('html_cutoff_text', self.html_cutoff_text) + assert child_relation is not None, '`child_relation` is a required argument.' super(ManyRelatedField, self).__init__(*args, **kwargs) self.child_relation.bind(field_name='', parent=self) @@ -469,4 +482,8 @@ class ManyRelatedField(Field): return self.choices def iter_options(self): - return iter_options(self.grouped_choices) + return iter_options( + self.grouped_choices, + cutoff=self.html_cutoff, + cutoff_text=self.html_cutoff_text + ) diff --git a/rest_framework/templates/rest_framework/filters/ordering.html b/rest_framework/templates/rest_framework/filters/ordering.html new file mode 100644 index 000000000..e69de29bb diff --git a/rest_framework/templates/rest_framework/horizontal/select.html b/rest_framework/templates/rest_framework/horizontal/select.html index 4aa3db3de..1ea5c16e2 100644 --- a/rest_framework/templates/rest_framework/horizontal/select.html +++ b/rest_framework/templates/rest_framework/horizontal/select.html @@ -16,7 +16,7 @@ {% elif select.end_option_group %} {% else %} - + {% endif %} {% endfor %} diff --git a/rest_framework/templates/rest_framework/horizontal/select_multiple.html b/rest_framework/templates/rest_framework/horizontal/select_multiple.html index f8763ce72..b00a63fbe 100644 --- a/rest_framework/templates/rest_framework/horizontal/select_multiple.html +++ b/rest_framework/templates/rest_framework/horizontal/select_multiple.html @@ -16,7 +16,7 @@ {% elif select.end_option_group %} {% else %} - + {% endif %} {% empty %} diff --git a/rest_framework/templates/rest_framework/inline/select.html b/rest_framework/templates/rest_framework/inline/select.html index d5204bb82..99f10ae71 100644 --- a/rest_framework/templates/rest_framework/inline/select.html +++ b/rest_framework/templates/rest_framework/inline/select.html @@ -15,7 +15,7 @@ {% elif select.end_option_group %} {% else %} - + {% endif %} {% endfor %} diff --git a/rest_framework/templates/rest_framework/inline/select_multiple.html b/rest_framework/templates/rest_framework/inline/select_multiple.html index a2e94d211..1c0e96b3c 100644 --- a/rest_framework/templates/rest_framework/inline/select_multiple.html +++ b/rest_framework/templates/rest_framework/inline/select_multiple.html @@ -15,7 +15,7 @@ {% elif select.end_option_group %} {% else %} - + {% endif %} {% empty %} diff --git a/rest_framework/templates/rest_framework/vertical/select.html b/rest_framework/templates/rest_framework/vertical/select.html index ce30022d8..973efb679 100644 --- a/rest_framework/templates/rest_framework/vertical/select.html +++ b/rest_framework/templates/rest_framework/vertical/select.html @@ -15,7 +15,7 @@ {% elif select.end_option_group %} {% else %} - + {% endif %} {% endfor %} diff --git a/rest_framework/templates/rest_framework/vertical/select_multiple.html b/rest_framework/templates/rest_framework/vertical/select_multiple.html index b5effe03f..dcbee2eda 100644 --- a/rest_framework/templates/rest_framework/vertical/select_multiple.html +++ b/rest_framework/templates/rest_framework/vertical/select_multiple.html @@ -15,7 +15,7 @@ {% elif select.end_option_group %} {% else %} - + {% endif %} {% empty %} From 99beeb80e1819dc16f02079fa70b65f4f2b212be Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Aug 2015 10:53:10 +0100 Subject: [PATCH 2/4] No cutoff for default ChoiceField, only for relationships --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index a362c9d22..0bf442f5d 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1202,7 +1202,7 @@ class ChoiceField(Field): default_error_messages = { 'invalid_choice': _('"{input}" is not a valid choice.') } - html_cutoff = 1000 + html_cutoff = None html_cutoff_text = _('More than {count} items...') def __init__(self, choices, **kwargs): From 314c0095aabc118275ef5b374c9fbfd2123a22e6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Aug 2015 10:56:22 +0100 Subject: [PATCH 3/4] iter_options function should have no cutoff by deault --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 0bf442f5d..10a310280 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -156,7 +156,7 @@ def flatten_choices_dict(choices): return ret -def iter_options(grouped_choices, cutoff=1000, cutoff_text=None): +def iter_options(grouped_choices, cutoff=None, cutoff_text=None): """ Helper function for options and option groups in templates. """ From c271568bf55f5fb9053a78f20ac2bc84951d709b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Aug 2015 10:57:46 +0100 Subject: [PATCH 4/4] Remove erronous checkin --- rest_framework/templates/rest_framework/filters/ordering.html | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 rest_framework/templates/rest_framework/filters/ordering.html diff --git a/rest_framework/templates/rest_framework/filters/ordering.html b/rest_framework/templates/rest_framework/filters/ordering.html deleted file mode 100644 index e69de29bb..000000000