Skip to content

Commit

Permalink
Added restricted form field, cleaned example application
Browse files Browse the repository at this point in the history
  • Loading branch information
matllubos committed Mar 13, 2019
1 parent d59b699 commit 690864b
Show file tree
Hide file tree
Showing 14 changed files with 145 additions and 80 deletions.
3 changes: 1 addition & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ env:
# command to install dependencies
install:
- cd ./example;
- pip install -r requirements/base.txt
- pip install -r requirements.txt
- pip uninstall -y -q django
- pip install -q Django==$DJANGO_VERSION
- ln -sf $(pwd)/../chamber/ $(python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())");

# command to run tests
script:
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
include README.md
include LICENSE
include MANIFEST.in
recursive-include chamber *.py *.txt *.html *.po *.mo *.xml *.xslt *.xsl *.js *.sass *.css *.png *.jpg *.jpeg *.gif
22 changes: 22 additions & 0 deletions chamber/forms/fields.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from django import forms
from django.utils.translation import ugettext

from chamber import config

from .validators import (
RestrictedFileValidator, AllowedContentTypesByFilenameFileValidator, AllowedContentTypesByContentFileValidator
)

class DecimalField(forms.DecimalField):

Expand Down Expand Up @@ -38,3 +43,20 @@ def __init__(self, *args, **kwargs):
if 'widget' not in kwargs:
kwargs['widget'] = PriceNumberInput(currency)
super().__init__(*args, **kwargs)


class RestrictedFileField(forms.FileField):

def __init__(self, *args, **kwargs):
max_upload_size = kwargs.pop('max_upload_size', config.CHAMBER_MAX_FILE_UPLOAD_SIZE) * 1024 * 1024
allowed_content_types = kwargs.pop('allowed_content_types', None)

validators = tuple(kwargs.pop('validators', [])) + (
RestrictedFileValidator(max_upload_size),
)
if allowed_content_types:
validators += (
AllowedContentTypesByFilenameFileValidator(allowed_content_types),
AllowedContentTypesByContentFileValidator(allowed_content_types),
)
super().__init__(validators=validators, *args, **kwargs)
53 changes: 53 additions & 0 deletions chamber/forms/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import mimetypes

import magic # pylint: disable=E0401

from django.core.exceptions import ValidationError
from django.utils.translation import ugettext
from django.template.defaultfilters import filesizeformat


class RestrictedFileValidator:

def __init__(self, max_upload_size):
self.max_upload_size = max_upload_size

def __call__(self, data):
if data.size > self.max_upload_size:
raise ValidationError(
ugettext('Please keep filesize under {max}. Current filesize {current}').format(
max=filesizeformat(self.max_upload_size),
current=filesizeformat(data.size)
)
)
else:
return data


class AllowedContentTypesByFilenameFileValidator:

def __init__(self, content_types):
self.content_types = content_types

def __call__(self, data):
extension_mime_type = mimetypes.guess_type(data.name)[0]

if extension_mime_type not in self.content_types:
raise ValidationError(ugettext('Extension of file name is not allowed'))

return data


class AllowedContentTypesByContentFileValidator:

def __init__(self, content_types):
self.content_types = content_types

def __call__(self, data):
with magic.Magic(flags=magic.MAGIC_MIME_TYPE) as m:
mime_type = m.id_buffer(data.file.read(1024))
data.file.seek(0)
if mime_type not in self.content_types:
raise ValidationError(ugettext('File content was evaluated as not supported file type'))

return data
53 changes: 3 additions & 50 deletions chamber/models/fields.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import mimetypes
import os
from decimal import Decimal
from uuid import uuid4 as uuid

import magic # pylint: disable=E0401

from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import FileField as OriginFileField
from django.db.models.fields import DecimalField as OriginDecimalField
from django.forms import forms
from django.template.defaultfilters import filesizeformat
from django.utils.encoding import force_text
from django.utils.translation import ugettext

from chamber import config
from chamber.forms import fields as chamber_fields
from chamber.forms.validators import (
RestrictedFileValidator, AllowedContentTypesByFilenameFileValidator, AllowedContentTypesByContentFileValidator
)
from chamber.models.humanized_helpers import price_humanized
from chamber.utils.datastructures import SequenceChoicesEnumMixin, SubstatesChoicesNumEnum

Expand Down Expand Up @@ -50,52 +49,6 @@ def formfield(self, **kwargs):
return super().formfield(**defaults)


class RestrictedFileValidator:

def __init__(self, max_upload_size):
self.max_upload_size = max_upload_size

def __call__(self, data):
if data.size > self.max_upload_size:
raise forms.ValidationError(
ugettext('Please keep filesize under {max}. Current filesize {current}').format(
max=filesizeformat(self.max_upload_size),
current=filesizeformat(data.size)
)
)
else:
return data


class AllowedContentTypesByFilenameFileValidator:

def __init__(self, content_types):
self.content_types = content_types

def __call__(self, data):
extension_mime_type = mimetypes.guess_type(data.name)[0]

if extension_mime_type not in self.content_types:
raise ValidationError(ugettext('Extension of file name is not allowed'))

return data


class AllowedContentTypesByContentFileValidator:

def __init__(self, content_types):
self.content_types = content_types

def __call__(self, data):
with magic.Magic(flags=magic.MAGIC_MIME_TYPE) as m:
mime_type = m.id_buffer(data.file.read(1024))
data.file.seek(0)
if mime_type not in self.content_types:
raise ValidationError(ugettext('File content was evaluated as not supported file type'))

return data


class RestrictedFileFieldMixin:
"""
Same as FileField, but you can specify:
Expand Down
7 changes: 7 additions & 0 deletions docs/forms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,10 @@ A widget for safe rendering of readonly form values.
.. class:: chamber.forms.fields.PriceField

``django.forms.NumberInput`` with ``currency`` as a placeholder.

.. class:: chamber.forms.fields.RestictedFileField

``django.forms.FileField`` where you can set ``allowed_content_types`` amd ``max_upload_size``. File type is validated by file extension and content::

class FileForm(forms.Form):
file = RestictedFileField(allowed_content_types=('image/jpeg', 'application/pdf'), max_upload_size=10) # allowed JPEG or PDF file with max size 10 MB
2 changes: 1 addition & 1 deletion example/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ cleanvar: clean cleanvirtualenv
cleanall: cleanvar

pip:
$(PYTHON_BIN)/pip install -r requirements/base.txt
$(PYTHON_BIN)/pip install -r requirements.txt

initvirtualenv:
virtualenv -p $(PYTHON) --no-site-packages $(VIRTUAL_ENV)
Expand Down
2 changes: 1 addition & 1 deletion example/dj/apps/test_chamber/tests/formatters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from chamber.formatters import natural_number_with_currency

from germanium.anotations import data_provider # pylint: disable=E0401
from germanium.decorators import data_provider # pylint: disable=E0401
from germanium.tools import assert_equal # pylint: disable=E0401


Expand Down
41 changes: 35 additions & 6 deletions example/dj/apps/test_chamber/tests/forms/fields.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
from django.forms import TextInput
from django.core.files.uploadedfile import SimpleUploadedFile
from django.forms import TextInput, ValidationError
from django.test import TransactionTestCase

from germanium.tools import assert_equal, assert_true # pylint: disable=E0401
from germanium.tools import assert_equal, assert_true, assert_raises, assert_not_raises # pylint: disable=E0401

from chamber.forms.fields import DecimalField
from chamber.forms.fields import DecimalField, RestrictedFileField


class DecimalFieldTestCase(TransactionTestCase):
__all__ = (
'FormFieldsTestCase',
)

def test_should_return_correct_widget_attrs(self):

class FormFieldsTestCase(TransactionTestCase):

def test_decimal_field_should_return_correct_widget_attrs(self):
kwargs = {
'step': 0.5,
'min': 1.0,
Expand All @@ -20,7 +26,30 @@ def test_should_return_correct_widget_attrs(self):
for attr, value in kwargs.items():
assert_equal(value, widget_attrs[attr])

def test_should_return_default_attrs(self):
def test_decimal_field_should_return_default_attrs(self):
field = DecimalField()
widget_attrs = field.widget_attrs(TextInput())
assert_equal({'step': 'any'}, widget_attrs)

def test_restricted_file_field_should_raise_validation_error_for_invalid_files(self):
field = RestrictedFileField(allowed_content_types=('image/jpeg', 'application/pdf'), max_upload_size=1)

# Invalid file type
with open('data/all_fields_filled.csv', 'rb') as f:
with assert_raises(ValidationError):
field.clean(SimpleUploadedFile('all_fields_filled.pdf', f.read()))

# Invalid size
with open('data/test.jpg', 'rb') as f:
with assert_raises(ValidationError):
field.clean(SimpleUploadedFile('test.jpg', f.read()))

# Invalid file suffix
with open('data/test.pdf', 'rb') as f:
with assert_raises(ValidationError):
field.clean(SimpleUploadedFile('test.csv', f.read()))

# Valid file
with open('data/test.pdf', 'rb') as f:
with assert_not_raises(ValidationError):
field.clean(SimpleUploadedFile('test.pdf', f.read()))
2 changes: 1 addition & 1 deletion example/dj/apps/test_chamber/tests/models/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from chamber.models.fields import generate_random_upload_path
from chamber.shortcuts import change_and_save

from germanium.anotations import data_provider
from germanium.decorators import data_provider
from germanium.tools import assert_equal, assert_is_none, assert_false, assert_is_not_none, assert_raises, assert_true

from test_chamber.models import CSVRecord, TestFieldsModel # pylint: disable=E0401
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from chamber.models.humanized_helpers import price_humanized

from germanium.anotations import data_provider # pylint: disable=E0401
from germanium.decorators import data_provider # pylint: disable=E0401
from germanium.tools import assert_equal # pylint: disable=E0401


Expand Down
2 changes: 1 addition & 1 deletion example/dj/apps/test_chamber/tests/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from chamber.utils import get_class_method, keep_spacing, remove_accent

from germanium.anotations import data_provider # pylint: disable=E0401
from germanium.decorators import data_provider # pylint: disable=E0401
from germanium.tools import assert_equal, assert_true # pylint: disable=E0401

from .datastructures import * # NOQA
Expand Down
5 changes: 3 additions & 2 deletions example/requirements/base.txt → example/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
Django==1.11
Django==2.0
diff-match-patch==20110725.1
django-germanium==2.0.5
django-germanium==2.1.0
six==1.10.0
coverage==4.0.2
pyprind==2.9.9
coveralls==1.1
filemagic==1.6
unidecode==0.4.20
pillow==4.1.1
-e ../
30 changes: 15 additions & 15 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,25 @@


setup(
name="django-chamber",
name='django-chamber',
version=get_version(),
description="Utilities library meant as a complement to django-is-core.",
author="Lubos Matl, Oskar Hollmann",
author_email="matllubos@gmail.com, oskar@hollmann.me",
url="http://github.com/druids/django-chamber",
description='Utilities library meant as a complement to django-is-core.',
author='Lubos Matl, Oskar Hollmann',
author_email='matllubos@gmail.com, oskar@hollmann.me',
url='http://github.com/druids/django-chamber',
packages=find_packages(),
package_dir={"chamber": "chamber"},
package_dir={'chamber': 'chamber'},
include_package_data=True,
classifiers=[
"Development Status :: 4 - Beta",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.5",
"Framework :: Django",
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.5',
'Framework :: Django',
],
install_requires=[
'Django>=1.8',
Expand Down

0 comments on commit 690864b

Please sign in to comment.