Repository: encode/django-rest-framework

Issue:
Limit select dropdowns.
Related fields with many possible values will render into select dropdowns with potentially 1000's+++ rows. This will stop the page from rendering and leave the end user at a loss. We should be more graceful here, and also consider ajax-loaded widgets.



Discussion:
A default setting of 500 or 1000 would go a long way towards addressing this. On large tables, a dropdown is kind of useful over a few hundred anyways. Thanks!

Yup, that'd certainly be a good start!...

Another option is to implement something similar to Django's raw_id_field:
https://docs.djangoproject.com/en/1.8/ref/contrib/admin/#django.contrib.admin.ModelAdmin.raw_id_fields

Or django-extensions' ForeignKeyAutocompleteAdmin:
https://django-extensions.readthedocs.org/en/latest/admin_extensions.html#current-admin-extensions

Ideally, `raw_id` would be the default to avoid this problems from the start :) Same goes for the Django Admin.

I'm going to toss the idea of using [Select2](http://select2.github.io/) out there.
- It's designed to be a replacement for `<select>` boxes that supports searching
- It has support for pulling results from remote data sources

There are a few Django integrations that may be useful to build from
- https://github.com/asyncee/django-easy-select2
  - Pros
    - Uses the newer version of Select2 (4.0.0)
  - Cons
    - Not designed (out of the box) to pull results from AJAX
- https://github.com/applegrew/django-select2
  - Pros
    - Has views for populating results using AJAX
  - Cons
    - Still uses the older version of Select2 (3.5.2)

Considering we already have the API set up, and we aren't tied to Django forms, it would probably be easier to not use a third party app for integrating it.

_**Disclaimer:** I'm the active maintainer of Select2._


Resolving patch:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
--- 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=None, cutoff_text=None):
     """
     Helper function for options and option groups in templates.
     """
@@ -175,18 +175,32 @@ class Option(object):
         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 = None
+    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 @@ def iter_options(self):
         """
         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
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -54,9 +54,13 @@ def __init__(self, pk):
 
 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 @@ def grouped_choices(self):
         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 @@ def grouped_choices(self):
         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
+        )


Test patch: