How to add image cropping functionality to django administration site.
Cropping images is a fairly common use case in a web application. For example some applications let you upload a profile picture. In addition it let you crop and resize image for a better result. When dealing with image processing we need to install a few dependencies both on the frontend and backend.
In this tutorial I will demonstrate how to crop images in Django administration using two methods.
I will start by creating a new virtual environment where I am going to install django and other dependencies.
% mkdir django-cropping
% python3 -m venv venv
After creating a virtual environment I will activate my virtual environment using the following command.
% source ./venv/bin/activate
After activating my virtual environment I go on to install Django
% pip install Django
After Django finishes installing I can now create a new django project using the following command.
% django-admin startproject django_cropping
Create a super user
% python manage.py migrate
% python manage.py createsuperuser
Now the django project is setup it's time to create the application we will be using for this tutorial. I will just call the application gallery. This gallery application will consist of two models.
- Gallery
- Picture
These two models have a one to many relationship. A gallery has many pictures. The Picture model will have a ForeignKey field that points to the Gallery model. You will see this in the code snippet. For now let's go ahead and create the gallery app using the command below.
% cd django_cropping
% python manage.py startapp gallery
After creating the gallery app, go ahead and open the settings.py file in django_cropping/django_cropping and add the gallery app to INSTALLED_APPS list.
INSTALLED_APPS = [
...
'gallery'
...
]
Update MEDIA_URL and MEDIA_ROOT in django_cropping/django_cropping/settings.py
MEDIA_URL = '/media/
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')'
Add the following lines in the django_cropping/django_cropping/urls.py
from django.conf import setting
from django.conf.urls.static import static
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)s
Go ahead and run the development server.
% python manage.py runserver
After running the command above go to http://localhost:8000 to verify your development server is running correctly.
Now the django development server is running and gallery application has been created. It is time to write code for our two models we discussed above. Open django_cropping/gallery/models.py using the editor of your choice and add the following lines of code.
class Gallery(models.Model)
name = models.CharField(max_length=255)
date_created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class Meta:
verbose_name = 'Gallery'
verbose_name_plural = 'Galleries'
class Picture(models.Model):
image = models.ImageField(upload_to='pictures')
gallery = models.ForeignKey(Gallery, on_delete=models.CASCADE, related_name='pictures')
date_uploaded = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.image.name:
After adding this code if you go to the terminal where your development server is running you will notice this error.
Recommended by LinkedIn
As you can see in this error message django is complaining about a missing package called Pillow. Pillow is a Python imaging library for manipulating images. That's the library we are going to use to crop our images. Let's go ahead and install this library.
% pip install Pillow
Once Pillow is installed we can go forward to create our database migrations and migrate. This will create new tables in the database for Gallery and Picture models.
% python manage.py makemigrations
% python manage.py migrate
The next step is to create the form that we will be using to add and modify pictures. This form is called PictureAdminForm . In order to crop to the image we need four pieces of information X coordinate, Y coordinate, height and width of the cropping box the user will eventually interact with in the browser.
Create a new file under django_cropping/gallery called forms.py and copy the following the following code. I advise you to write the code line by line so as to understand it much better.
from django import forms
from .models import Picture
class PictureAdminForm(forms.ModelForm):
x = forms.FloatField(widget=forms.HiddenInput(), required=False)
y = forms.FloatField(widget=forms.HiddenInput(), required=False)
width = forms.FloatField(widget=forms.HiddenInput(), required=False)
height = forms.FloatField(widget=forms.HiddenInput(), required=False)
class Meta:
model = Picture
fields = ('image', 'x', 'y', 'width', 'height')
Before we dive into the code I just want to explain what the code does. The following code allows us to use our PictureAdminForm in Django administration form. We are replacing the form that is generated by default for our Picture model. The form that is generated by default doesn't have x,y,width and height attributes which we want to use when cropping an image. So we will set the form attribute of PictureAdmin class to PictureAdminForm so that our custom form is used instead.
We will also override the save_model method of our PictureAdmin and write code for cropping images in that method. It allows us to cropped the picture after the picture has been saved. Documentation for this method is found here
Now it's time to implement the code. Open django_cropping/gallery/admin.py and copy the following code.
class GalleryAdmin(admin.ModelAdmin)
pass
admin.site.register(Gallery, GalleryAdmin)
class PictureAdmin(admin.ModelAdmin):
form = PictureAdminForm
def save_model(self, request, obj, form, change):
obj.save()
x = form.cleaned_data.get('x')
y = form.cleaned_data.get('y')
width = form.cleaned_data.get('width')
height = form.cleaned_data.get('height')
# Only crop image when x,y, width and height have been provided
if x and y and width and height:
image = Image.open(obj.image)
cropped_image = image.crop((x,y,width + x, height + y))
cropped_image.save(obj.image.path)
admin.site.register(Picture, PictureAdmin):
We are done with all the back-end logic and now it's time to work on the front-end side. We are going to start by overriding the template rendered on the picture add or modify view. To do this we are going to follow the following steps
- In django_cropping/gallery create a new folder called templates
- Under templates folder create another folder admin
- Under admin folder create another folder called gallery
- Under gallery folder create another folder called picture
- Under picture folder create a file called change_form.html
Quit and start your development server and goto http://localhost:8000/admin/gallery/picture/add/. If you see an empty white page that means you have successfully overridden the default template rendered by Django. Now copy the code found here and paste it in you change_form.html file you .n the template search for the following code
{% block extrastyle %}
Paste in the following code just before the corresponding `{% endblock %}` statement
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropper/4.1.0/cropper.min.css">
It should look like this
{% block extrastyle %
{{ block.super }}
<link rel="stylesheet" href="{% static "admin/css/forms.css" %}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropper/4.1.0/cropper.min.css">
{% endblock %}}
In the template search for the following code
{% block field_sets %
{% for fieldset in adminform %}
{% include "admin/includes/fieldset.html" %}
{% endfor %}
{% endblock %}}
In the template, after the code snippet above pasted the following code
<div id="image-box" class="mt-3"></div>
The last step is to paste in the following code just before the last `{% endblock %}` statement in the template.
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropper/4.1.0/cropper.min.js"></script>
<script>
const imageBox = document.getElementById('image-box');
const imageInput = document.getElementById('id_image');
imageInput.addEventListener('change', () => {
const img_data = imageInput.files[0]
const url = URL.createObjectURL(img_data)
imageBox.innerHTML = `<img src="${url}" id="image" width="700px">`;
var $image = $('#image');
console.log($image);
$image.cropper({
aspectRatio: 9 / 3,
crop: function (event) {
console.log("X::", event.detail.x);
$('#id_x').val(event.detail.x);
console.log("Y::", event.detail.y);
$('#id_y').val(event.detail.y);
console.log("WIDTH::", event.detail.width);
$('#id_width').val(event.detail.width);
console.log("HEIGHT::", event.detail.height);
$('#id_height').val(event.detail.height);
console.log(event.detail.rotate);
console.log(event.detail.scaleX);
console.log(event.detail.scaleY);
}
});
})
</script>
The code listens for the change event that is fired when a user selects a file from the file system. On detecting the event it then creates a new cropping user interface to crop the image selected. The cropper instance is created with aspect ratio of 9:3. The crop function executes whenever a new cropping event occurs to updated the form fields. By the time the user clicks the save button the form fields have been updated and they are submitted along.
I am currently using Django 4.1.5 and here is my final template
{% extends "admin/base_site.html" %
{% load i18n admin_urls static admin_modify %}
{% block extrahead %}{{ block.super }}
<script src="{% url 'admin:jsi18n' %}"></script>
{{ media }}
{% endblock %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" href="{% static "admin/css/forms.css" %}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropper/4.1.0/cropper.min.css">
{% endblock %}
{% block coltype %}colM{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-form{% endblock %}
{% if not is_popup %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
› {% if has_view_permission %}<a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %}
› {% if add %}{% blocktranslate with name=opts.verbose_name %}Add {{ name }}{% endblocktranslate %}{% else %}{{ original|truncatewords:"18" }}{% endif %}
</div>
{% endblock %}
{% endif %}
{% block content %}<div id="content-main">
{% block object-tools %}
{% if change and not is_popup %}
<ul class="object-tools">
{% block object-tools-items %}
{% change_form_object_tools %}
{% endblock %}
</ul>
{% endif %}
{% endblock %}
<form {% if has_file_field %}enctype="multipart/form-data" {% endif %}{% if form_url %}action="{{ form_url }}" {% endif %}method="post" id="{{ opts.model_name }}_form" novalidate>{% csrf_token %}{% block form_top %}{% endblock %}
<div>
{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1">{% endif %}
{% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}">{% endif %}
{% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %}
{% if errors %}
<p class="errornote">
{% blocktranslate count counter=errors|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktranslate %}
</p>
{{ adminform.form.non_field_errors }}
{% endif %}
{% block field_sets %}
{% for fieldset in adminform %}
{% include "admin/includes/fieldset.html" %}
{% endfor %}
{% endblock %}
<div id="image-box" class="mt-3"></div>
{% block after_field_sets %}{% endblock %}
{% block inline_field_sets %}
{% for inline_admin_formset in inline_admin_formsets %}
{% include inline_admin_formset.opts.template %}
{% endfor %}
{% endblock %}
{% block after_related_objects %}{% endblock %}
{% block submit_buttons_bottom %}{% submit_row %}{% endblock %}
{% block admin_change_form_document_ready %}
<script id="django-admin-form-add-constants"
src="{% static 'admin/js/change_form.js' %}"
{% if adminform and add %}
data-model-name="{{ opts.model_name }}"
{% endif %}
async>
</script>
{% endblock %}
{# JavaScript for prepopulated fields #}
{% prepopulated_fields_js %}
</div>
</form></div>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropper/4.1.0/cropper.min.js"></script>
<script>
const imageBox = document.getElementById('image-box');
const imageInput = document.getElementById('id_image');
imageInput.addEventListener('change', () => {
const img_data = imageInput.files[0]
const url = URL.createObjectURL(img_data)
imageBox.innerHTML = `<img src="${url}" id="image" width="700px">`;
var $image = $('#image');
console.log($image);
$image.cropper({
aspectRatio: 9 / 3,
crop: function (event) {
console.log("X::", event.detail.x);
$('#id_x').val(event.detail.x);
console.log("Y::", event.detail.y);
$('#id_y').val(event.detail.y);
console.log("WIDTH::", event.detail.width);
$('#id_width').val(event.detail.width);
console.log("HEIGHT::", event.detail.height);
$('#id_height').val(event.detail.height);
console.log(event.detail.rotate);
console.log(event.detail.scaleX);
console.log(event.detail.scaleY);
}
});
})
</script>
{% endblock %}}
The source code is found here