From 9d6cb94bd83dd9c513fea796dbded30fdf0b37d5 Mon Sep 17 00:00:00 2001
From: Pouria Hadjibagheri
Date: Mon, 16 Jan 2017 20:18:07 +0000
Subject: [PATCH] Rectified the error causing image size alteration for non-svg
images being ignored. More clean ups, and breakdown of functions inline with
functional/OOP best practices, to make them easier to debug.
---
markdownx/forms.py | 134 +++++++++++++++++++++++++++++----------------
1 file changed, 87 insertions(+), 47 deletions(-)
diff --git a/markdownx/forms.py b/markdownx/forms.py
index 8a03cac..3014c00 100755
--- a/markdownx/forms.py
+++ b/markdownx/forms.py
@@ -1,14 +1,17 @@
-from os import path
-from os import SEEK_END
+# Python internal library.
+from os import path, SEEK_END
from uuid import uuid4
+from collections import namedtuple
+# Django library.
from django import forms
from django.utils.six import BytesIO
from django.core.files.storage import default_storage
from django.utils.translation import ugettext_lazy as _
from django.core.files.uploadedfile import InMemoryUploadedFile
-from django.template import defaultfilters as filters
+from django.template.defaultfilters import filesizeformat
+# Internal.
from .utils import scale_and_crop
from .settings import (
MARKDOWNX_IMAGE_MAX_SIZE,
@@ -17,20 +20,33 @@ from .settings import (
MARKDOWNX_UPLOAD_MAX_SIZE,
)
-# -------------------------------------------------------
-# Constants
-# -------------------------------------------------------
-
-SVG_TYPE = 'image/svg+xml'
-SVG_EXTENSION = 'svg'
-
-# -------------------------------------------------------
-
class ImageForm(forms.Form):
-
image = forms.FileField()
+ # Separately defined as it needs to be processed a text file rather than image.
+ _SVG_TYPE = 'image/svg+xml'
+
+ _error_templates = {
+ # No file is available to upload.
+ 'not_uploaded':
+ forms.ValidationError(_('No files have been uploaded.')),
+
+ # The file is of a format not defined in "settings.py"
+ # or if default, in "markdownx/settings.py".
+ 'unsupported_format':
+ forms.ValidationError(_('File type is not supported.')),
+
+ # The file is larger in size that the maximum allow in "settings.py" (or the default).
+ 'invalid_size':
+ lambda current: forms.ValidationError(
+ _('Please keep file size under {max}. Current file size {current}').format(
+ max=filesizeformat(MARKDOWNX_UPLOAD_MAX_SIZE),
+ current=filesizeformat(current)
+ )
+ )
+ }
+
def save(self, commit=True):
"""
Saves the uploaded image in the designated location.
@@ -39,80 +55,104 @@ class ImageForm(forms.Form):
subsequently save; otherwise, the SVG is saved in its existing `charset`
as an `image/svg+xml`.
- The dimension of image files (excluding SVG) are set using `Pillow`.
+ The dimension of image files (excluding SVG) are set using `PIL`.
:param commit: If `True`, the file is saved to the disk; otherwise, it is held in the memory.
:type commit: bool
- :return: An instance of saved image if `commit` is `True`, else `full_path, uploaded_image`.
+ :return: An instance of saved image if `commit is True`, else `namedtuple(path, image)`.
+ :rtype: bool, namedtuple
"""
image = self.files.get('image')
content_type = image.content_type
- thumb_io = None
file_name = image.name
+ image_extension = content_type.split('/')[-1].upper()
+ image_size = getattr(image, '_size')
- if image.content_type is SVG_TYPE:
- thumb_io = BytesIO()
- preped_image = scale_and_crop(image, **MARKDOWNX_IMAGE_MAX_SIZE)
- image_extension = image.content_type.split('/')[-1].upper()
- preped_image.save(thumb_io, image_extension)
- file_name = str(image)
- thumb_io.seek(0, SEEK_END)
+ if content_type.lower() != self._SVG_TYPE:
+ # Processing the raster graphic image:
+ image = self._process_raster(image, image_extension)
+ image_size = image.tell()
+ # Processed file (or the actual file in the case of SVG) is now
+ # saved in the memory as a Django object.
uploaded_image = InMemoryUploadedFile(
- file=thumb_io if thumb_io else image,
+ file=image,
field_name=None,
name=file_name,
content_type=content_type,
- size=thumb_io.tell() if thumb_io else getattr(image, '_size'),
+ size=image_size,
charset=None
)
+ return self._save(uploaded_image, file_name, commit)
+
+ def _save(self, image, file_name, commit):
+ """
+ Final saving process, called internally after the image had processed.
+ """
+ # Defining a universally unique name for the file
+ # to be saved on the disk.
unique_file_name = self.get_unique_file_name(file_name)
full_path = path.join(MARKDOWNX_MEDIA_PATH, unique_file_name)
if commit:
- default_storage.save(full_path, uploaded_image)
+ default_storage.save(full_path, image)
return default_storage.url(full_path)
- # If `commit=False`, return the path and in-memory image.
- return full_path, uploaded_image
+ # If `commit is False`, return the path and in-memory image.
+ image_data = namedtuple('image_data', ['path', 'image'])
+ return image_data(path=full_path, image=image)
@staticmethod
- def get_unique_file_name(filename):
+ def _process_raster(image, extension):
"""
- Generates a universally unique ID using Python `UUID` and
- attaches the extension of file name to it.
- :param filename: Name of the uploaded file, including the extension.
- :type filename: str
- :return: Universally unique ID, ending with the extension extracted from `filename`.
+ Processing of raster graphic image.
+ """
+ # File needs to be uploaded and saved temporarily in
+ # the memory for additional processing using PIL.
+ thumb_io = BytesIO()
+ preped_image = scale_and_crop(image, **MARKDOWNX_IMAGE_MAX_SIZE)
+ preped_image.save(thumb_io, extension)
+ thumb_io.seek(0, SEEK_END)
+ return thumb_io
+
+ @staticmethod
+ def get_unique_file_name(file_name):
+ """
+ Generates a universally unique ID using Python `UUID` and attaches the extension of file name to it.
+
+ :param file_name: Name of the uploaded file, including the extension.
+ :type file_name: str
+ :return: Universally unique ID, ending with the extension extracted from `file_name`.
:rtype: str
"""
- ext = filename.split('.')[-1]
- filename = "%s.%s" % (uuid4(), ext)
- return filename
+ file_name = "{unique_name}.{extension}".format(
+ unique_name=uuid4(),
+ extension=path.splitext(file_name)[1][1:] # [1] is the extension, [1:] discards the dot.
+ )
+ return file_name
def clean(self):
"""
Checks the upload against allowed extensions and maximum size.
+
:return: Upload
"""
upload = self.cleaned_data.get('image')
+
+ # -----------------------------------------------
+ # See comments in `self._error_templates` for
+ # additional information on each error.
+ # -----------------------------------------------
if not upload:
- raise forms.ValidationError(_('No files have been uploaded.'))
+ raise self._error_templates['not_uploaded']
content_type = upload.content_type
file_size = getattr(upload, '_size')
if content_type not in MARKDOWNX_UPLOAD_CONTENT_TYPES:
- raise forms.ValidationError(_('File type is not supported.'))
-
+ raise self._error_templates['unsupported_format']
elif file_size > MARKDOWNX_UPLOAD_MAX_SIZE:
- raise forms.ValidationError(
- _('Please keep file size under %(max)s. Current file size %(current)s') %
- {
- 'max': filters.filesizeformat(MARKDOWNX_UPLOAD_MAX_SIZE),
- 'current': filters.filesizeformat(upload._size)
- }
- )
+ raise self._error_templates['invalid_size'](file_size)
return upload