Merge pull request #44 from adi-/v1.7.2

v1.7.2: Rectifies the bug causing non-svg images being ignored and adds "image/xml+svg" to the default settings.
This commit is contained in:
Pouria Hadjibagheri 2017-01-16 20:35:39 +00:00 committed by GitHub
commit cc6825f0d4
2 changed files with 147 additions and 61 deletions

View file

@ -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

View file

@ -1,32 +1,78 @@
# Django library.
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
# Constants
# ------------------------------------------------------------------
__FIFTY_MEGABYTES = 50 * 1024 * 1024
__VALID_CONTENT_TYPES = 'image/jpeg', 'image/png', 'image/svg+xml'
# ------------------------------------------------------------------
def _from_settings(var, default):
"""
Adds "MARXDOWX_" to the requested variable and retrieves its value
from settings or returns the default.
:param var: Variable to be retrieved.
:type var: str
:param default: Default value if the variable is not defined.
:return: Value corresponding to 'var'.
"""
return getattr(settings, 'MARKDOWNX_' + var, default)
# Markdownify
MARKDOWNX_MARKDOWNIFY_FUNCTION = getattr(settings, 'MARKDOWNX_MARKDOWNIFY_FUNCTION', 'markdownx.utils.markdownify')
# ------------
MARKDOWNX_MARKDOWNIFY_FUNCTION = _from_settings('MARKDOWNIFY_FUNCTION', 'markdownx.utils.markdownify')
# Markdown extensions
MARKDOWNX_MARKDOWN_EXTENSIONS = getattr(settings, 'MARKDOWNX_MARKDOWN_EXTENSIONS', [])
MARKDOWNX_MARKDOWN_EXTENSION_CONFIGS = getattr(settings, 'MARKDOWNX_MARKDOWN_EXTENSION_CONFIGS', {})
# --------------------
MARKDOWNX_MARKDOWN_EXTENSIONS = _from_settings('MARKDOWN_EXTENSIONS', list())
MARKDOWNX_MARKDOWN_EXTENSION_CONFIGS = _from_settings('MARKDOWN_EXTENSION_CONFIGS', dict())
# Markdown urls
MARKDOWNX_URLS_PATH = getattr(settings, 'MARKDOWNX_URLS_PATH', '/markdownx/markdownify/')
MARKDOWNX_UPLOAD_URLS_PATH = getattr(settings, 'MARKDOWNX_UPLOAD_URLS_PATH', '/markdownx/upload/')
# --------------
MARKDOWNX_URLS_PATH = _from_settings('URLS_PATH', '/markdownx/markdownify/')
MARKDOWNX_UPLOAD_URLS_PATH = _from_settings('UPLOAD_URLS_PATH', '/markdownx/upload/')
# Media path
MARKDOWNX_MEDIA_PATH = getattr(settings, 'MARKDOWNX_MEDIA_PATH', 'markdownx/')
# -----------
MARKDOWNX_MEDIA_PATH = _from_settings('MEDIA_PATH', 'markdownx/')
# Image
MARKDOWNX_UPLOAD_MAX_SIZE = getattr(settings, 'MARKDOWNX_UPLOAD_MAX_SIZE', 52428800) # 50MB
MARKDOWNX_UPLOAD_CONTENT_TYPES = getattr(settings, 'MARKDOWNX_UPLOAD_CONTENT_TYPES', ['image/jpeg', 'image/png'])
MARKDOWNX_IMAGE_MAX_SIZE = getattr(settings, 'MARKDOWNX_IMAGE_MAX_SIZE', {'size': (500, 500), 'quality': 90,})
# ------
MARKDOWNX_UPLOAD_MAX_SIZE = _from_settings('UPLOAD_MAX_SIZE', __FIFTY_MEGABYTES)
MARKDOWNX_UPLOAD_CONTENT_TYPES = _from_settings('UPLOAD_CONTENT_TYPES', __VALID_CONTENT_TYPES)
MARKDOWNX_IMAGE_MAX_SIZE = _from_settings('IMAGE_MAX_SIZE', dict(size=(500, 500), quality=90))
# Editor
MARKDOWNX_EDITOR_RESIZABLE = getattr(settings, 'MARKDOWNX_EDITOR_RESIZABLE', True)
# -------
MARKDOWNX_EDITOR_RESIZABLE = _from_settings('EDITOR_RESIZABLE', True)
# ------------------------------------------------
# Translations
LANGUAGES = getattr(settings, 'LANGUAGES', (
('en', _('English')),
('pl', _('Polish')),
)
# ------------------------------------------------
# This is not called using `_from_settings` as
# it does not need "_MARKDOWNX" prefix.
LANGUAGES = getattr(
settings,
'LANGUAGES',
(
('en', _('English')),
('pl', _('Polish')),
)
)