A new Django widget

A new Django widget

Category : Python

I’m building websites with Bootstrap and Django. I’ve already created a form on one of them with a file input. The standard file input can’t really be styled with CSS apparently, so I’ve searched for a way to have a nice Bootstrap file input and found this. In short, it uses a button and a text input for display, and a transparent file input above them to use the browser access to local files. Some Javascript makes those two parts communicate.

First I’ve used this manually in a form directly in the template. Today I discovered that you can write your own Django widgets, so I tried to reorganize this file input.

Creating the widget

First I created a widgets directory in my app, with the following files : __init__.py (empty) and styledfileinput.py.

styledfileinput.py contains :

# StyledFileInput
# coding: utf-8

from django.forms.widgets import FileInput
from django.utils.html import format_html
from django.forms.utils import flatatt

class StyledFileInput(FileInput):

class Media:
css = { 'all': ('styledfileinput.css', ) }
js = ('styledfileinput.js', )

def render(self, name, value, attrs=None):
if value is None:
value = ''
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
if value != '':
# Only add the 'value' attribute if a value is non-empty.
final_attrs['value'] = force_text(self._format_value(value))
return format_html(
'<div class="input-group">\n'
+ '<span class="input-group-btn">\n'
+ '<span class="btn btn-default btn-file">\n'
+ 'Browse... \n'
+ '<input multiple="" {} >\n'
+ '</span>\n'
+ '</span>\n'
+ '<input class="form-control" readonly="" type="text">\n'
+ '</div>\n', flatatt(final_attrs)
)

I’ve created a StyledFileInput class inheriting from FileInput, and redefined the render function with the whole HTML block. I’ve kept the attributes and value handling, so that Django can use it properly.

There is an inner Media class to define the required static files. This two files are placed in the static directory of my app.

Here is the content of styledfileinput.css :

/* file buttons should be styled */
.btn-file {
position: relative;
overflow: hidden;
}

.btn-file input[type=file] {
position: absolute;
top: 0;
right: 0;
min-width: 100%;
min-height: 100%;
font-size: 999px;
text-align: right;
filter: alpha(opacity=0);
opacity: 0;
background: red;
cursor: inherit;
display: block;
}

Then styledfileinput.js :

(function($){
$(document)
.on('change', '.btn-file :file', function() {
var input = $(this),
numFiles = input.get(0).files ? input.get(0).files.length : 1,
label = input.val().replace(/\\/g, '/').replace(/.*\//, '');
input.trigger('fileselect', [numFiles, label]);
});

$(document).ready( function() {
$('.btn-file :file').on('fileselect', function(event, numFiles, label) {

var input = $(this).parents('.input-group').find(':text'),
log = numFiles > 1 ? numFiles + ' files selected' : label;

if( input.length ) {
input.val(log);
} else {
if( log ) alert(log);
}

});
});
})(django.jQuery);

You can see that this code is wrapped in a function called with django.jQuery for $. This is necessary as I also use the AdminDateWidget, which uses the following JavaScript :

/* Puts the included jQuery into our own namespace using noConflict and passing
* it 'true'. This ensures that the included jQuery doesn't pollute the global
* namespace (i.e. this preserves pre-existing values for both window.$ and
* window.jQuery).
*/
var django = django || {};
django.jQuery = jQuery.noConflict(true);

Using the widget

Now I can use it in a Django form :

from app.widgets.styledfileinput import StyledFileInput

class DocumentForm(forms.ModelForm):
class Meta:
model = Document
fields = "__all__"
widgets = {
'date': admin.widgets.AdminDateWidget(),
'filename' : StyledFileInput(),
}

In the template, nothing changes in the form itself :

<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="panel panel-default">
<div class="panel-body">
{{ doc_form|bootstrap_horizontal }}
<button class="btn btn-default" type="submit">Envoyer</button>
</div>
</div>
</form>

You just have to be sure to put somewhere in your template {{ doc_form.media }} so that the required static files are included.

Conclusion

Now that the Bootstrap file input is redefined as a widget, it’s easy to attach to any classic Django forms, and it’s easy to reuse in any project.

If you have any suggestion to improve it, feel free to comment.


Log out of this account

Leave a Reply