How to do it...

Let's refine the form for inspirational quotes so that it can support Ajax uploads, using the following steps:

  1. First of all, add the following to your settings:
# settings.py or config/base.py
# ...
UPLOAD_URL = f'{MEDIA_URL}upload/'
UPLOAD_ROOT = os.path.join(MEDIA_ROOT, 'upload')
If you want, you can also update the .gitignore file under the MEDIA_ROOT to avoid committing anything to the UPLOAD_ROOT, just by adding /upload/ as a new line.
  1. Then, in the quotes app, we will define a custom file storage system for uploads using the new setting:
# quotes/storages.py
from django.conf import settings
from django.core.files.storage import FileSystemStorage


upload_storage = FileSystemStorage(location=settings.UPLOAD_ROOT)
  1. Getting to the form, we'll update it to add a hidden picture_path field dynamically:
# quotes/forms.py
# ...
class InspirationalQuoteForm(forms.ModelForm):
# ...
picture_path = forms.CharField(max_length=255,
widget=forms.HiddenInput(),
required=False)
  1. Then, we will override the save() method in the form, as follows:
# quotes/forms.py
class InspirationalQuoteForm(forms.ModelForm):
# ...
def save(self, commit=True):
instance = super().save(commit=commit)
picture = self.cleaned_data["picture"]
path = self.cleaned_data["picture_path"]
if not picture and path:
try:
picture = upload_storage.open(path)
instance.picture.save(path, picture, False)
os.remove(path)
except FileNotFoundError:
pass
instance.save()
return instance
  1. In addition to the previously defined views in the quotes app, we'll add an upload_quote_picture view, as shown in the following code:
# quotes/views.py
from datetime import datetime
import os

from django.core.files.base import ContentFile
from django.http import HttpResponse
from django.shortcuts import render, redirect
from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.csrf import csrf_protect
from django.views.generic import DetailView, ListView

from .models import InspirationalQuote
from .forms import InspirationalQuoteForm
from .storages import upload_storage

# ...

def _upload_to(request, filename):
user = (f"user-{request.user.pk}"
if request.user.is_authenticated
else "anonymous")

return os.path.join("quotes",
user,
f"{datetime.now():%Y/%m/%d}",
filename)

@csrf_protect
def upload_quote_picture(request):
status_code = 400
data = {
"files": [],
"error": _("Bad request"),
}
if request.method == "POST"
and request.is_ajax()
and "picture" in request.FILES:
image_types = [f"image/{x}" for x in [
"gif", "jpg", "jpeg", "pjpeg", "png"
]]
picture = request.FILES["picture"]
if picture.content_type not in image_types:
status_code = 405
data["error"] = _("Invalid image format")
else:
upload_to = _upload_to(request, picture.name)
name = upload_storage.save(upload_to,
ContentFile(picture.read()))
picture = upload_storage.open(name)
status_code = 200
del data["error"]
picture.filename = os.path.basename(picture.name)
data["files"].append(picture)

json_data = render_to_string("quotes/upload.json", data)
return HttpResponse(json_data,
content_type="application/json",
status=status_code)
  1. Similarly, there needs to be the delete_quote_picture view to handle the removal of the uploads:
# quotes/views.py
# ...

@csrf_protect
def delete_quote_picture(request, filename):
if request.method == "DELETE"
and request.is_ajax()
and filename:
try:
upload_to = _upload_to(request, filename)
upload_storage.delete(upload_to)
except FileNotFoundError:
pass
json = render_to_string("quotes/upload.json", {"files": []})
return HttpResponse(json,
content_type="application/json",
status=200)
  1. We set the URL rules for the new upload and deletion views, as follows:
# quotes/urls.py 
from django.urls import path

from .views import (add_quote,
QuotesList,
upload_quote_picture,
delete_quote_picture)


urlpatterns = [
# ...
path('upload/', upload_quote_picture,
name='quote-picture-upload'),
path('upload/<str:filename>', delete_quote_picture,
name='quote-picture-delete'),
]
  1. The new views render their JSON output via a new template, so we can define that file next:
{# templates/quotes/upload.json #}
{% load thumbnail %}
{
{% if error %}"error": "{{ error }}",{% endif %}
"files": [{% for file in files %}
{
"name": "{{ file.filename }}",
"size": {{ file.size }},
"deleteType": "DELETE",
"deleteUrl": "{% url 'quote-picture-delete' filename=file.filename %}",
"thumbnailUrl": "{% thumbnail file '200x200' %}",
"type": "{{ file.content_type }}",
"path": "{{ file.name }}"
}{% if not forloop.last %},{% endif %}
{% endfor %}]
}
  1. Now we move on to create the JavaScript template that will be used to display a file we have selected for upload:
{# templates/quotes/includes/tmpl-upload.html #}
{% verbatim %}
<script type="text/x-tmpl" id="template-upload">
{% for (var i=0, file; file=o.files[i]; i++) { %}
<tr class="template-upload">
<td><span class="preview"></span></td>
<td>
<p class="name">{%=file.name%}</p>
<strong class="error text-danger"></strong>
</td>
<td>
<p class="size">{%=o.options.i18n('Processing...') %}</p>
</td>
<td width="20%">
<div role="progressbar" aria-valuenow="0"
aria-valuemin="0" aria-valuemax="100"
class="progress progress-striped active"><div
class="progress-bar progress-bar-success"
style="width:0%;"></div></div>
</td>
<td>
{% if (!i && !o.options.autoUpload) { %}
<button class="btn btn-primary start">
<i class="ion-upload"></i>
<span>{%=o.options.i18n('Start') %}</span>
</button>
{% } %}
{% if (!i) { %}
<button class="btn btn-warning cancel">
<i class="ion-close-circled"></i>
<span>{%=o.options.i18n('Cancel') %}</span>
</button>
{% } %}
</td>
</tr>
{% } %}
</script>
{% endverbatim %}
  1. There is also a corresponding JavaScript template for displaying the file once it has been uploaded successfully:
{# templates/quotes/includes/tmpl-download.html #}
{% verbatim %}
<script type="text/x-tmpl" id="template-download">
{% for (var i=0, file; file=o.files[i]; i++) { %}
<tr class="template-download">
<td>
<span class="preview">
{% if (file.thumbnailUrl) { %}
<a href="{%=file.url%}" data-gallery
title="{%=file.name%}"
download="{%=file.name%}">
<img src="{%=file.thumbnailUrl%}"></a>
{% } %}
</span>
</td>
<td>
<p class="name">
{% if (file.url) { %}
<a href="{%=file.url%}"
{%=file.thumbnailUrl?'data-gallery':''%}
title="{%=file.name%}"
download="{%=file.name%}">
<img src="{%=file.thumbnailUrl%}"></a>
{% } else { %}
<span>{%=file.name%}</span>
{% } %}
</p>
{% if (file.error) { %}
<div>
<span class="label label-danger">
{%=o.options.i18n('Error') %}</span>
{%=file.error%}
</div>
{% } %}
</td>
<td><span class="size">
{%=o.formatFileSize(file.size)%}
</span></td>
<td>
{% if (file.deleteUrl) { %}
<button data-type="{%=file.deleteType%}"
data-url="{%=file.deleteUrl%}"
{% if (file.deleteWithCredentials) { %}
data-xhr-fields='{"withCredentials":true}'
{% } %}
class="btn btn-danger delete">
<i class="ion-trash-a"></i>
<span>{%=o.options.i18n('Remove') %}</span>
</button>
{% } else { %}
<button class="btn btn-warning cancel">
<i class="ion-close-circled"></i>
<span>{%=o.options.i18n('Cancel') %}</span>
</button>
{% } %}
</td>
</tr>
{% } %}
</script>
{% endverbatim %}
  1. These new includes, and the supporting CSS and JS need to be added to the form markup. Let's update that template now, as follows:
{# templates/quotes/add_quote.html #}
{% extends "base.html" %}
{% load i18n static %}

{% block stylesheet %}
<link rel="stylesheet" type="text/css"
href="{% static 'site/css/lib/jquery.fileupload.css' %}">
{% endblock %}

{% block content %}
<form method="post" action="" enctype="multipart/form-data"
class="change-quote">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">{% trans "Save" %}</button>
</form>
{% endblock %}

{% block extrabody %}
{% include "quotes/includes/tmpl-upload.html" %}
{% include "quotes/includes/tmpl-download.html" %}
{% endblock %}

{% block js %}
<script src="{% static 'site/js/lib/tmpl.min.js' %}"></script>
<script src="{% static 'site/js/lib/jquery.ui.widget.js' %}"></script>
<script src="{% static 'site/js/lib/jquery.fileupload.js' %}"></script>
<script src="{% static 'site/js/lib/jquery.fileupload-ui.js' %}"></script>
<script src="{% static 'quotes/js/uploader.js' %}"></script>
{% endblock %}
  1. Then, let's set up the JavaScript file that integrates the Ajax upload functionality as a progressive enhancement:
// static/quotes/js/uploader.js
(function($) {
var SELECTORS = {
CSRF_TOKEN: "input[name='csrfmiddlewaretoken']",
PICTURE: "input[type='file'][name='picture']",
PATH: "input[name='picture_path']"
};

var DEFAULTS = {
labels: {
"Select": "Select File"
}
};

function Uploader(form, uploadUrl, options) {
if (form && uploadUrl) {
this.form = form;
this.url = uploadUrl;

this.processOptions(options);
this.gatherFormElements();
this.wrapFileField();
this.setupFileUpload();
}
}

Uploader.prototype.mergeObjects = function(source, target) {
var self = this;
Object.keys(source).forEach(function(key) {
var sourceVal = source[key];
var targetVal = target[key];
if (!target.hasOwnProperty(key)) {
target[key] = sourceVal;
} else if (typeof sourceVal === "object"
&& typeof targetVal === "object") {
self.mergeObjects(sourceVal, targetVal);
}
});
};

Uploader.prototype.processOptions = function(options) {
options = options || {};
this.mergeObjects(DEFAULTS, options);
this.options = options;
};

Uploader.prototype.gatherFormElements = function() {
this.csrf = this.form.querySelector(SELECTORS.CSRF_TOKEN);
this.picture = this.form.querySelector(SELECTORS.PICTURE);
this.path = this.form.querySelector(SELECTORS.PATH);

this.createButton();
this.createContainer();
};

Uploader.prototype.createButton = function() {
var label = this.options.labels["Select Picture"];
this.button = document.createElement("button");
this.button.appendChild(document.createTextNode(label));
this.button.setAttribute("type", "button");
this.button.classList.add(
"btn", "btn-primary", "fileinput-button");
};

Uploader.prototype.createContainer = function() {
this.container = document.createElement("table");
this.container.setAttribute("role", "presentation");
this.container.classList.add("table", "table-striped");

this.list = document.createElement("tbody");
this.list.classList.add("files");
this.container.appendChild(this.list);
};

Uploader.prototype.wrapFileField = function() {
this.picture.parentNode.insertBefore(
this.button, this.picture);
this.button.appendChild(this.picture);
this.button.parentNode.insertBefore(
this.container, this.button);
};

Uploader.prototype.setupFileUpload = function() {
var self = this;
var safeMethodsRE = /^(GET|HEAD|OPTIONS|TRACE)$/;
$.ajaxSettings.beforeSend = (function(existing) {
var csrftoken = document.cookie.replace(
/^(?:.*;)?csrftoken=(.*?)(?:;.*)?$/, "$1");
return function(xhr, settings) {
if (!safeMethodsRE.test(settings.type)
&& !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}

$(this.form).fileupload({
url: this.url,
dataType: 'json',
acceptFileTypes: /^image/(gif|p?jpeg|jpg|png)$/,
autoUpload: false,
replaceFileInput: true,
messages: self.options.labels,
maxNumberOfFiles: 1
}).on("fileuploaddone", function(e, data) {
self.path.value = data.result.files[0].path;
}).on("fileuploaddestroy", function(e, data) {
self.path.value = "";
});
};

window.Uploader = Uploader;
}(jQuery));
  1. Finally, we add one last integration piece to the change form template:
{# templates/quotes/add_quote.html #}
{% block js %}
{# ... #}
<script>
jQuery(function($){
if (typeof Uploader !== "undefined") {
var form = document.querySelector("form.change-quote");
var uploadUrl = "{% url 'quote-picture-upload' %}";
new Uploader(form, uploadUrl, {
"labels": {
"Select Picture": "{% trans 'Select Picture' %}",
"Cancel": "{% trans 'Cancel' %}",
"Remove": "{% trans 'Remove' %}",
"Error": "{% trans 'Error' %}",
"Processing...": "{% trans 'Processing...' %}",
"Start": "{% trans 'Start' %}"
}
});
}
});
</script>
{% endblock %}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.213.128