diff --git a/rest_framework/filters.py b/rest_framework/filters.py index e05d31ae3..265dfae17 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -9,6 +9,7 @@ from functools import reduce from django.core.exceptions import ImproperlyConfigured from django.db import models +from django.template import Context, Template, loader from django.utils import six from rest_framework.compat import ( @@ -36,6 +37,7 @@ class DjangoFilterBackend(BaseFilterBackend): A filter backend that uses django-filter. """ default_filter_set = FilterSet + template = 'rest_framework/filters/django_filter.html' def __init__(self): assert django_filters, 'Using DjangoFilterBackend, but django-filter is not installed' @@ -57,11 +59,33 @@ class DjangoFilterBackend(BaseFilterBackend): return filter_class if filter_fields: + from crispy_forms.helper import FormHelper + from crispy_forms.layout import Field, Fieldset, Layout, Submit + class AutoFilterSet(self.default_filter_set): class Meta: model = queryset.model fields = filter_fields + @property + def form(self): + self._form = super(AutoFilterSet, self).form + for field in self._form.fields.values(): + field.help_text = None + layout_components = filter_fields + [ + Submit('', 'Apply', css_class='btn-default'), + ] + + helper = FormHelper() + helper.form_method = 'get' + helper.form_action = '.' + helper.template_pack = 'bootstrap3' + + helper.layout = Layout(*layout_components) + + self._form.helper = helper + return self._form + return AutoFilterSet return None @@ -74,6 +98,15 @@ class DjangoFilterBackend(BaseFilterBackend): return queryset + def to_html(self, request, queryset, view): + cls = self.get_filter_class(view, queryset) + filter_instance = cls(request.query_params, queryset=queryset) + context = Context({ + 'filter': filter_instance + }) + template = loader.get_template(self.template) + return template.render(context) + class SearchFilter(BaseFilterBackend): # The URL query parameter used for the search. @@ -127,6 +160,7 @@ class OrderingFilter(BaseFilterBackend): # The URL query parameter used for the ordering. ordering_param = api_settings.ORDERING_PARAM ordering_fields = None + template = 'rest_framework/filters/ordering.html' def get_ordering(self, request, queryset, view): """ @@ -152,7 +186,7 @@ class OrderingFilter(BaseFilterBackend): return (ordering,) return ordering - def remove_invalid_fields(self, queryset, fields, view): + def get_valid_fields(self, queryset, view): valid_fields = getattr(view, 'ordering_fields', self.ordering_fields) if valid_fields is None: @@ -172,6 +206,10 @@ class OrderingFilter(BaseFilterBackend): valid_fields = [field.name for field in queryset.model._meta.fields] valid_fields += queryset.query.aggregates.keys() + return valid_fields + + def remove_invalid_fields(self, queryset, fields, view): + valid_fields = self.get_valid_fields(queryset, view) return [term for term in fields if term.lstrip('-') in valid_fields] def filter_queryset(self, request, queryset, view): @@ -182,6 +220,18 @@ class OrderingFilter(BaseFilterBackend): return queryset + def get_template_context(self, request, queryset, view): + #default_tuple = self.get_default_ordering() + #default = None if default_tuple is None else default_tuple[0] + { + 'options': self.get_valid_fields(queryset, view), + } + + def to_html(self, request, queryset, view): + template = loader.get_template(self.template) + context = Context(self.get_template_context(request, queryset, view)) + return template.render(context) + class DjangoObjectPermissionsFilter(BaseFilterBackend): """ diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index fb0dfcbd8..826a4062a 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -374,6 +374,7 @@ class BrowsableAPIRenderer(BaseRenderer): media_type = 'text/html' format = 'api' template = 'rest_framework/api.html' + filter_template = 'rest_framework/filters/base.html' charset = 'utf-8' form_renderer_class = HTMLFormRenderer @@ -600,6 +601,24 @@ class BrowsableAPIRenderer(BaseRenderer): def get_breadcrumbs(self, request): return get_breadcrumbs(request.path, request) + def get_filter_form(self, view, request): + if not hasattr(view, 'get_queryset') or not hasattr(view, 'filter_backends'): + return + + queryset = view.get_queryset() + elements = [] + for backend in view.filter_backends: + if hasattr(backend, 'to_html'): + html = backend().to_html(request, queryset, view) + elements.append(html) + + if not elements: + return + + template = loader.get_template(self.filter_template) + context = Context({'elements': elements}) + return template.render(context) + def get_context(self, data, accepted_media_type, renderer_context): """ Returns the context used to render. @@ -647,6 +666,8 @@ class BrowsableAPIRenderer(BaseRenderer): 'delete_form': self.get_rendered_html_form(data, view, 'DELETE', request), 'options_form': self.get_rendered_html_form(data, view, 'OPTIONS', request), + 'filter_form': self.get_filter_form(view, request), + 'raw_data_put_form': raw_data_put_form, 'raw_data_post_form': raw_data_post_form, 'raw_data_patch_form': raw_data_patch_form, diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css index f6c675462..f4e645ca0 100644 --- a/rest_framework/static/rest_framework/css/default.css +++ b/rest_framework/static/rest_framework/css/default.css @@ -73,3 +73,11 @@ pre { border-bottom: none; padding-bottom: 0px; } + +#filtersModal form input[type=submit] { + width: auto; +} + +#filtersModal .modal-body h2 { + margin-top: 0 +} diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index abbc75952..71075ad89 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -109,6 +109,13 @@ {% endif %} + {% if filter_form %} + + {% endif %} +