From b76d0a1d90004b0699dc011095c910ef3c975f56 Mon Sep 17 00:00:00 2001 From: Pouria Hadjibagheri Date: Mon, 16 Jan 2017 20:15:46 +0000 Subject: [PATCH 1/2] Small clean up of the code. --- markdownx/settings.py | 74 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/markdownx/settings.py b/markdownx/settings.py index 8a9ad15..c160b63 100755 --- a/markdownx/settings.py +++ b/markdownx/settings.py @@ -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')), + ) ) From 9d6cb94bd83dd9c513fea796dbded30fdf0b37d5 Mon Sep 17 00:00:00 2001 From: Pouria Hadjibagheri Date: Mon, 16 Jan 2017 20:18:07 +0000 Subject: [PATCH 2/2] 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