In this tutorial, we are going to create a nice crispy range slider using django-crispy-forms for an integer filter provided by django-filters. The tutorial can be split into four sections. In the first or prerequisite section, we will install the required packages and initiate the Django project. In the next section, we will create a simple app with a model, view, and template. In the third section, we will create the simple range filter using django-filters package without a slider. In the fourth and last section, we will describe how to create a range-slider and integrate it into our Django app created in the third section.
Prerequisite
First things first, let’s create a directory with the working environment:
$ mkdir myproject
$ cd myproject
$ pipenv shellThen, install packages that are required in this tutorial using pip:
$ pip install Django
$ pip install django-crispy-forms
$ pip install django-filterNext, create a new Django project called myproject:
$ django-admin startproject myproject
$ mv myproject srcSimilarly, create a new Django app called myapp:
$ python manage.py startapp myappIn the following sections, you are going to need to generate sample data for our model. Hence, let’s create a new Django admin super-user using the following command:
$ python manage.py createsuperuserTo enable packages in Django project, add the following lines to the 
INSTALLED_APPS/src/myproject/settings.pyINSTALLED_APPS = [
    ...
    'django.forms', # Required in the last section.
    'django_filters', # Django-filter
    'crispy_forms',
    'myapp'
]Then, add the following line to 
TEMPLATES/src/myproject/settings.pyTEMPLATES = [
    {
        ...
        'DIRS': [BASE_DIR / 'templates'],
        ...
    },
]Next, add the path 
/src/myproject/staticSTATICFILES_DIRS/src/myproject/settings.py...
STATICFILES_DIRS = [ BASE_DIR / 'static']
...Finally, add the following line of code to 
/src/myproject/settings.py...
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
...Getting Ready
In this section, we will create a model called People, a view for this model, and a template for that view.
The Model
Create a model called 
PeopleIntegerFieldclass People(models.Model):
    name = models.CharField(null=True,blank=True,max_length=50)
    surname = models.CharField(null=True,blank=True,max_length=50)
    age = models.IntegerField()Run 
makemigrationsmigrate$ python manage.py makemigrations
$ python manage.py migrateThen, register the model in Django admin by adding the following code to file 
/src/myapp/admin.pyfrom django.contrib import admin
from .models import People
class PeopleAdmin(admin.ModelAdmin):
    pass
admin.site.register(People, PeopleAdmin)NOTE: Add some items to the database from the Django admin page at http://127.0.0.1:8000/admin/.
The View
Now let’s create a simple view that will print all instances of 
People/src/myapp/views.pyfrom django.shortcuts import render
from .models import People
def index(request):
    all_people = People.objects.all()
    return render(request, 'index.html', {'all_people':all_people})URLs
Create a URL path by adding the following line to the file 
/src/myproject/urls.py...
from myapp.views import index
urlpatterns = [
    ...
    path('', index),
]The Template
In order to create the simplest template to render all 
People/src/templates/index.html<table border='1' style="width:100%; text-align:center">
  <thead>
    <tr>
      <th> Name </th>
      <th> Surname </th>
      <th> Age </th>
    </tr>
  </thead>
  <tbody>
    {% for person in all_people %}
    <tr>
      <td> {{ person.name }} </td>
      <td> {{ person.surname }} </td>
      <td> {{ person.age }} </td>
    </tr>
    {% endfor %}
  </tbody>
</table>Recap
In this section, we created a simple view and template to print database records for 
People$ python manage.py runserverNaive Range Filter
In order to ensure coherence let’s first create a simple(or naive) 
RangeFilterThe Filter
Create a new file 
/src/myapp/filters.pyimport django_filters
from .models import People
class PeopleFilter(django_filters.FilterSet):
  age = django_filters.AllValuesFilter()
  class Meta:
      model = People
      fields = ['age']This will create a simple range-filter with minimum and maximum value requirements for 
agePeopleThe View
Now that filtering logic is ready, let’s add the filtering feature to the main view in 
/src/myapp/views.pyfrom django.shortcuts import render
from .filters import PeopleFilter
def index(request):
    people_filter = RangeFilter(request.GET)
    return render(request, 'index.html', {'people_filter':people_filter})In the above code, 
RangeFilterrequest.GETThe Template
With our filter ready, we can add filter controls in the front. Once again change the primary template file 
/src/template/index.html<form method="get">
    {{ people_filter.form.as_p }}
    <input type="submit" />
</form>
<table border='1' style="width:100%; text-align:center">
  <thead>
    <tr>
      <th> Name </th>
      <th> Surname </th>
      <th> Age </th>
    </tr>
  </thead>
  <tbody>
    {% for person in people_filter.qs %}
    <tr>
      <td> {{ person.name }} </td>
      <td> {{ person.surname }} </td>
      <td> {{ person.age }} </td>
    </tr>
    {% endfor %}
  </tbody>
</table>Notice that we now have an additional filtering form provided by django-filters. In addition, observe that for statement loops over 
people_filter.qsall_people.qsRecap
In this section, we created the simplest or naive filter. The final result should look like this:
Crispy Range-Slider
To create a working crispy range-slider we need the following:
- Front-end Backbone: Actual range-slider with HTML, CSS and JavaScript.
- Django Widget: Custom Django widget for the actual range-slider backbone.
- Crispy Form: Crispy form with a layout template for the custom widget.
- Range Filter: Custom filter from django-filters package that utilizes the range-slider widget with the Crispy form.
Each point will be described in details so let’s move step-by-step:
The Front End
The first and obvious step is to create an actual slider. Since range-slider is fancy filtering “thing” and not a real HTML form element, let’s use a popular trick to make such a fancy “thing” act like an HTML form element. Particularly, we use jQuery’s range slider feature to make our range-slider work. Here is a sample HTML blueprint for our slider:
<div id="my-numeric-slider"
     class="form-group numeric-slider"
     data-range_min="[Min. Possible Value]"
     data-range_max="[Max. Possible Value]"
     data-cur_min="[Current Min. Value]"
     data-cur_max="[Current Max. Value]">
<div class="numeric-slider-range ui-slider ui-slider-horizontal ui-slider-range"></div>
<span class="numeric-slider-range_text" id='my-numeric-slider_text'>[Lower Value] - [Upper Value]</span>
<input type='hidden' id='my-numeric-slider_min' name='slider-min'/>
<input type='hidden' id='my-numeric-slider_max' name='slider-max'/>
</div>The above HTML markup is comprised of outer Div element where the first two data- attributes represent possible minimum and maximum values of the range filter, last two data- attributes represent current lower and upper values of the range filter that were established when the page is loaded. Likewise, the first inner elements Div with the class 
numeric-slider-range/src/templates/index.html{% load static %}
{% load crispy_forms_tags %}
<head>
  <link rel="stylesheet" href="{% static 'custom_slider.css' %}"> # CSS of our range-slider.
  <link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
  <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
</head>
<body>
  {% crispy people_filter.form %}
  <table border='1' style="width:100%; text-align:center">
    <thead>
      <tr>
        <th> Name </th>
        <th> Surname </th>
        <th> Age </th>
      </tr>
    </thead>
    <tbody>
      {% for person in people_filter.qs %}
      <tr>
        <td> {{ person.name }} </td>
        <td> {{ person.surname }} </td>
        <td> {{ person.age }} </td>
      </tr>
      {% endfor %}
    </tbody>
  </table>
  <script src="{% static 'custom_slider.js' %}"></script> # JS of our range-slider.
</body>Notice that in the HTML above we load the 
crispy_forms_tags{% crispy people_filter.form %}.as_pNOTE: The django-crispy-forms package provides two ways to render crispy forms. Common way is to use 
|crispy<form>...</form>{% crispy %}FormHelperThe Django Widget
Django uses widgets to render form-fields and present them as final HTML markup. Therefore, to let Django handle the rendering of our front-end backbone, we need a working widget. In Django, a widget consists of two parts:
- a widget-template that represents the final HTML
- a class that inherits Django’s 
 class withWidget
 method.render()
Widget Class
The django-filters package provides working 
RangeFilterRangeWidgetRangeWidgetCustomRangeWidgetfrom django.forms.widgets import HiddenInput
from django_filters.widgets import RangeWidget
class CustomRangeWidget(RangeWidget):
    template_name = 'forms/widgets/range-slider.html'
    def __init__(self, attrs=None):
        widgets = (HiddenInput(), HiddenInput())
        super(RangeWidget, self).__init__(widgets, attrs)
    def get_context(self, name, value, attrs):
        ctx = super().get_context(name, value, attrs)
        cur_min, cur_max = value
        if cur_min is None:
            cur_min = ctx['widget']['attrs']['data-range_min']
        if cur_max is None:
            cur_max = ctx['widget']['attrs']['data-range_max']
        ctx['widget']['attrs'].update({'data-cur_min':cur_min,
                                        'data-cur_max':cur_max})
        base_id = ctx['widget']['attrs']['id']
        for swx, subwidget in enumerate(ctx['widget']['subwidgets']):
            subwidget['attrs']['id'] = base_id + "_" + self.suffixes[swx]
        ctx['widget']['value_text'] = "{} - {}".format(cur_min,cur_max)
        return ctxWidget Template
The widget also requires an associated template. Let’s create a file in 
/src/templates/forms/widgets/range-slider.html<div class="form-group numeric-slider" {% include "django/forms/widgets/attrs.html" %}>
  <div class="numeric-slider-range ui-slider ui-slider-horizontal ui-slider-range"></div>
  <span class="numeric-slider-range_text" id='{{ widget.attrs.id }}_text'>
    {{ widget.value_text }}
  </span>
  {% for widget in widget.subwidgets %}
    {% include widget.template_name %}
  {% endfor %}
</div>In the above widget-template, we use 
{% include "django/forms/widgets/attrs.html" %}ctx['widget']['attrs']forHiddenInputThe Crispy Form
At last, we have our actual widget ready and now we can create a crispy-form with a special template for our slider. This crispy layout template basically helps our widget to fit the Bootstrap markup logic. In other words, it makes it crispy.
Crispy Template
Create a new file 
/src/templates/forms/fields/range-slider.html{% load crispy_forms_field %}
<div class="form-group{% if 'form-horizontal' in form_class %} row{% endif %}">
  <label for="{{ field.id_for_label }}" class="{% if 'form-horizontal' in form_class %}col-form-label {% endif %}{{ label_class }}{% if field.field.required %} requiredField{% endif %}">
    {{ field.label|safe }}
    {% if field.field.required %}
      <span class="asteriskField">*</span>
    {% endif %}
  </label>
  {% crispy_field field  %}
</div>NOTE: the above code is based on django-crispy-forms’s bootstrap4 templates and was not tested in bootstrap3 or other crispy template-engine.
Crispy Form Helper
Once the crispy template is ready we need a form where the template will be utilized. Create a file 
/src/myapp/forms.pyfrom crispy_forms.helper import FormHelper
from crispy_forms.bootstrap import StrictButton
from crispy_forms.layout import Field, Layout
from django import forms
from django_filters.fields import RangeField
class PeopleFilterFormHelper(forms.Form):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper(self)
        self.helper.form_method = 'get'
        layout_fields = []
        for field_name, field in self.fields.items():
            if isinstance(field, RangeField):
                layout_field = Field(field_name, template="forms/fields/range-slider.html")
            else:
                layout_field = Field(field_name)
            layout_fields.append(layout_field)
        layout_fields.append(StrictButton("Submit", name='submit', type='submit', css_class='btn btn-fill-out btn-block mt-1'))
        self.helper.layout = Layout(*layout_fields)In the code above the class 
PeopleFilterFormHelperFormHelperNOTE: the 
FormHelperRange Filter
At last, we have everything ready except the actual filtering logic.
Custom Range Filter
Insert following final filter code to the file 
/src/myapp/filters.pyfrom django_filters import FilterSet
from django_filters.filters import RangeFilter
from .models import People
from .forms import PeopleFilterFormHelper
from .widgets import CustomRangeWidget
class AllRangeFilter(RangeFilter):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        values = [p.age for p in People.objects.all()]
        min_value = min(values)
        max_value = max(values)
        self.extra['widget'] = CustomRangeWidget(attrs={'data-range_min':min_value,'data-range_max':max_value})
class PeopleFilter(FilterSet):
  age = AllRangeFilter()
  class Meta:
      model = People
      fields = ['age']
      form = PeopleFilterFormHelperIn the code above, 
PeopleFilterPeopleform = PeopleFilterFormHelperPeopleFilterFormHelperAllRangeFilterRangeFilter__init__CustomRangeWidgetPeopleageNOTE: I totally agree that list comprehension is far not the best way to get the min. and max. values but this is a tutorial and its for only for educational purpose.
Recap
At last, we created our range filter with a fancy slider that should look like this:
Summary
In this tutorial, you learned how to create a fancy jQuery range slider with the custom widget for the custom range filter provided by django-filters package. Moreover, you learned how to use render the custom widget using django-crispy-forms the package. The source code for this tutorial can be found on my GitHub repo. I hope this tutorial was helpful to the reader and eased his suffering while learning these amazing Django packages.
Previously published at https://www.mmtechslv.com/tutorials/django-crispy-range-slider/
