39 Commits
0.7.5 ... 1.10

Author SHA1 Message Date
Florent Lebreton
667a5fdb46 Fix travis.yml 2016-10-28 10:44:13 +02:00
Florent Lebreton
86e3e185f7 Add Django 1.10 compatibility 2016-10-28 10:41:03 +02:00
Florent Lebreton
6da81f2115 Release 0.8 2014-12-04 13:58:40 +01:00
Florent Lebreton
d12b1e80ac Merge pull request #5 from Gagaro/master
Python3 / Django1.7 support & better testing
2014-12-04 13:50:38 +01:00
Gagaro
880033d3a5 fix: django.utils.six not available before django 1.6 2014-12-04 12:34:49 +01:00
Gagaro
c15074873b fix: python 3 compatibility 2014-12-04 12:27:25 +01:00
Gagaro
8e912f9738 tests: improve testing by splitting tests + use six for basestring python3 compatibility 2014-12-04 12:18:53 +01:00
Gagaro
70b4f2e9b6 fix: .travis.yml matrix exclude 2014-12-04 10:31:30 +01:00
Gagaro
124fe21d06 tests: upgrade quicktest.py + improve travis configuration 2014-12-04 10:22:31 +01:00
Florent Lebreton
738eb869fc Merge pull request #4 from Gagaro/master
splitted widgets tests
2014-12-03 12:45:50 +01:00
Gagaro
32566873a4 splitted widgets tests 2014-12-03 11:18:28 +01:00
Florent Lebreton
bbde0d7005 Package for pypi 2014-11-27 10:58:52 +01:00
Florent Lebreton
11ae827643 Add a template example in README (@jsayles) 2014-11-26 19:21:12 +01:00
Florent Lebreton
337a8f9a08 Add a setting for reset button (@jsayles) 2014-11-26 19:19:03 +01:00
Florent Lebreton
51005db0ca South is not required (@jsayles) 2014-11-26 18:44:38 +01:00
Florent Lebreton
02e78b1a6c Better rendering and javascript initialization (@andybak / #2) 2014-11-26 18:44:26 +01:00
Robert Rouhani
b29c5964af Fix bug where form value is never set if the name contains an underscore. 2014-04-22 00:44:06 -04:00
Florent Lebreton
5c7f1d7037 PEP8 clean-up 2013-11-20 15:59:14 +01:00
Florent Lebreton
7666e781e5 Widget should not behave as a hidden field 2013-11-20 15:46:40 +01:00
Florent Lebreton
27eeca4f8f Update README 2013-11-20 15:30:20 +01:00
Florent Lebreton
a8bf966933 Update README 2013-09-29 15:30:51 +02:00
Florent Lebreton
202df097b0 Update .travis.yml to use coveralls 2013-09-29 14:52:51 +02:00
Florent Lebreton
0de152f292 Use coverage and add missing test 2013-09-29 14:46:05 +02:00
Florent Lebreton
1fd41a622a Update README 2013-09-29 12:19:02 +02:00
Florent Lebreton
b87ba745e7 Update README 2013-09-29 12:13:57 +02:00
Florent Lebreton
69e81bd808 Change README 2013-09-29 12:11:35 +02:00
Florent Lebreton
91b0e4d400 Move quicktest script 2013-09-29 11:37:32 +02:00
Florent Lebreton
a86cc67520 Fix travis.yml 2013-09-29 11:37:32 +02:00
Florent Lebreton
271b4239a2 Remove django from requirements to fix travis build 2013-09-29 11:26:01 +02:00
Florent Lebreton
ca5ba2d0f8 Fix bugs in draw_signature and add tests 2013-09-29 11:18:00 +02:00
Florent Lebreton
95860405f3 Use travis-ci 2013-09-29 10:24:18 +02:00
Florent Lebreton
ff2494a33e Add gitignore 2013-09-29 10:23:48 +02:00
Florent Lebreton
9c8ad2087e Add south to requirements 2013-09-29 10:22:02 +02:00
Florent Lebreton
a8ef50d8d0 Add tests 2013-09-19 16:23:57 +02:00
Florent Lebreton
5c17f753e7 Clean-up 2013-09-19 16:04:46 +02:00
Florent Lebreton
ed42468767 Fix MANIFEST.in 2013-09-18 11:30:31 +02:00
Florent Lebreton
a19d0f9545 Add french translation 2013-09-18 11:14:52 +02:00
Florent Lebreton
c4bb0b8fd1 Improve handling of different values types 2013-09-18 10:50:19 +02:00
Florent Lebreton
86791e12f3 Fix MANIFEST.in 2013-09-13 12:31:55 +02:00
28 changed files with 766 additions and 87 deletions

2
.coveragerc Normal file
View File

@@ -0,0 +1,2 @@
[run]
source = jsignature

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*.pyc
.coverage
htmlcov

32
.travis.yml Normal file
View File

@@ -0,0 +1,32 @@
language: python
python:
- 2.7
- 3.3
- 3.4
- 3.5
env:
- DJANGO_VERSION=1.7 MODULE=jsignature.tests
- DJANGO_VERSION=1.8 MODULE=jsignature.tests
- DJANGO_VERSION=1.9 MODULE=jsignature.tests
- DJANGO_VERSION=1.10 MODULE=jsignature.tests
install:
- pip install -r requirements.txt
- pip install -q Django==$DJANGO_VERSION
- pip install coverage
script: coverage run quicktest.py $MODULE
after_success:
- pip install coveralls
- coveralls
# We need to exclude old versions of Python for tests with Django >= 1.9.
matrix:
exclude:
- python: 3.3
env: DJANGO_VERSION=1.9 MODULE=jsignature.tests
- python: 3.3
env: DJANGO_VERSION=1.10 MODULE=jsignature.tests

24
CHANGES Normal file
View File

@@ -0,0 +1,24 @@
=========
CHANGELOG
=========
0.8 (2014-12-04)
==================
** New **
- Add support for Python 3 (@Gagaro)
- Add support for Django 1.7 (@Gagaro)
0.7.6 (2014-11-26)
==================
** New features **
- A setting to display (or not) the reset button has been added (@jsayles)
** Internal changes **
- Rendering is now based on a template (@andybak)
- Javascript is properly initialized (@andybak)

View File

@@ -1,4 +1,4 @@
include README.rst LICENSE
recursive-include jsignature/templates *.html
include README.rst LICENSE CHANGES
recursive-include jsignature/static *.js
recursive-include jsignature/static *.css
recursive-include jsignature/locale *.mo
recursive-include jsignature/templates *.html

View File

@@ -1,4 +0,0 @@
django-jsignature
=================
Use jSignature jQuery plugin in your django projects

View File

@@ -1,12 +1,145 @@
==================
django-jsignature
==================
A simple way to use `jSignature jQuery plugin <https://github.com/brinley/jSignature/blob/master/README.md>`_ in your `Django <https://www.djangoproject.com>`_ projects.
It provides:
* A form field and a form widget to handle jquery plugin through a Django form;
* A model field to store a captured signature;
* A mixin adding two fields (signature / signature_date) in any of your Django models;
* A mixin adding two fields (signature / signature_date) in any of your Django models.
.. image:: https://travis-ci.org/fle/django-jsignature.png?branch=master
:target: https://travis-ci.org/fle/django-jsignature
.. image:: https://coveralls.io/repos/fle/django-jsignature/badge.png
:target: https://coveralls.io/r/fle/django-jsignature
.. image:: https://github.com/fle/django-jsignature/blob/master/screen.png
==================
INSTALL
==================
For now:
::
pip install django-jsignature
==================
USAGE
==================
* Add ``jsignature`` to your ``INSTALLED_APPS``:
::
# settings.py
INSTALLED_APPS = (
...
'jsignature',
)
* Use provided form field and widget:
::
# forms.py
from django import forms
from jsignature.forms import JSignatureField
class SignatureForm(forms.Form):
signature = JSignatureField()
* In your template
::
{{ form.media }}
<form action="." method="POST">
{% for field in form %}
{{ field.label_tag }}
{{ field }}
{% endfor %}
<input type="submit" value="Save"/>
{% csrf_token %}
</form>
* Render image after form validation:
::
# views.py
from jsignature.utils import draw_signature
from myapp.forms import SignatureForm
def my_view(request):
form = SignatureForm(request.POST or None)
if form.is_valid():
signature = form.cleaned_data.get('signature')
if signature:
# as an image
signature_picture = draw_signature(signature)
# or as a file
signature_file_path = draw_signature(signature, as_file=True)
==================
CUSTOMIZATION
==================
JSignature plugin options are available in python:
* Globally, in your settings:
::
# settings.py
JSIGNATURE_WIDTH = 500
JSIGNATURE_HEIGHT = 200
* Specifically, in your form:
::
# forms.py
from jsignature.forms import JSignatureField
from jsignature.widgets import JSignatureWidget
JSignatureField(widget=JSignatureWidget(jsignature_attrs={'color': '#CCC'}))
Available settings are:
* ``JSIGNATURE_WIDTH`` (width)
* ``JSIGNATURE_HEIGHT`` (height)
* ``JSIGNATURE_COLOR`` (color)
* ``JSIGNATURE_BACKGROUND_COLOR`` (background-color)
* ``JSIGNATURE_DECOR_COLOR`` (decor-color)
* ``JSIGNATURE_LINE_WIDTH`` (lineWidth)
* ``JSIGNATURE_UNDO_BUTTON`` (UndoButton)
* ``JSIGNATURE_RESET_BUTTON`` (ResetButton)
==================
IN YOUR MODELS
==================
If you wan to store signatures, provided mixin gives a ``signature`` and a ``signature_date`` that update themselves:
::
from django.db import models
from jsignature.mixins import JSignatureFieldsMixin
class JSignatureModel(JSignatureFieldsMixin):
name = models.CharField()
==================
AUTHORS
==================
* Florent Lebreton <florent.lebreton@makina-corpus.com>
|makinacom|_
.. |makinacom| image:: http://depot.makina-corpus.org/public/logo.gif
.. _makinacom: http://www.makina-corpus.com

View File

@@ -3,27 +3,27 @@
with jSignature jQuery plugin
"""
import json
import six
from django.db import models
from django.core import validators
from django.core.exceptions import ValidationError
from .forms import JSignatureField as JSignatureFormField
from .forms import (
JSignatureField as JSignatureFormField,
JSIGNATURE_EMPTY_VALUES)
class JSignatureField(models.Field):
"""
A model field handling a signature captured with jSignature
"""
description = "A signature captured with jSignature"
__metaclass__ = models.SubfieldBase
def get_internal_type(self):
return 'TextField'
def to_python(self, value):
"""
Validates that the input can be red as a JSON object. Returns a Python
datetime.date object.
"""
if value in validators.EMPTY_VALUES:
if value in JSIGNATURE_EMPTY_VALUES:
return None
elif isinstance(value, list):
return value
@@ -32,13 +32,18 @@ class JSignatureField(models.Field):
except ValueError:
raise ValidationError('Invalid JSON format.')
def get_prep_value(self, value):
return self.to_python(value)
def get_db_prep_value(self, value, connection, prepared=False):
if value in validators.EMPTY_VALUES:
def from_db_value(self, value, expression, connection, context):
if value in JSIGNATURE_EMPTY_VALUES:
return None
elif isinstance(value, basestring):
try:
return json.loads(value)
except ValueError:
raise ValidationError('Invalid JSON format.')
def get_prep_value(self, value):
if value in JSIGNATURE_EMPTY_VALUES:
return None
elif isinstance(value, six.string_types):
return value
elif isinstance(value, list):
return json.dumps(value)
@@ -49,6 +54,3 @@ class JSignatureField(models.Field):
defaults.update(kwargs)
return super(JSignatureField, self).formfield(**defaults)
from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ["jsignature.fields.JSignatureField"])

View File

@@ -8,6 +8,9 @@ from django.core import validators
from django.core.exceptions import ValidationError
from .widgets import JSignatureWidget
JSIGNATURE_EMPTY_VALUES = validators.EMPTY_VALUES + ('[]', )
class JSignatureField(Field):
"""
A field handling a signature capture field with with jSignature
@@ -16,10 +19,10 @@ class JSignatureField(Field):
def to_python(self, value):
"""
Validates that the input can be red as a JSON object. Returns a Python
datetime.date object.
Validates that the input can be red as a JSON object.
Returns a Python list (JSON object unserialized).
"""
if value in validators.EMPTY_VALUES:
if value in JSIGNATURE_EMPTY_VALUES:
return None
try:
return json.loads(value)

Binary file not shown.

View File

@@ -0,0 +1,30 @@
# French translation for django-jsignature package.
# Copyright (C) 2013
# This file is distributed under the same license as the django-jsignature package.
# Florent Lebreton <florent.lebreton@makina-corpus.com>
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-09-18 11:10+0200\n"
"PO-Revision-Date: 2013-09-18 11:13+0100\n"
"Last-Translator: Florent Lebreton <lebreton.florent@wanadoo.fr>\n"
"Language-Team: contact@makina-corpus.com\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Poedit 1.5.4\n"
#: mixins.py:13
msgid "Signature"
msgstr "Signature"
#: mixins.py:17
msgid "Signature date"
msgstr "Date de signature"
#: widgets.py:65
msgid "Reset"
msgstr "Réinitialiser"

View File

@@ -7,6 +7,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from .fields import JSignatureField
class JSignatureFieldsMixin(models.Model):
""" Mixin class providing fields to store a signature with jSignature """
signature = JSignatureField(
@@ -24,9 +25,8 @@ class JSignatureFieldsMixin(models.Model):
def save(self, *args, **kwargs):
is_new = self.pk is None
original = not is_new and self.__class__.objects.get(pk=self.pk) or None
original = not is_new and self.__class__.objects.get(pk=self.pk)
print self.signature
if self.signature:
if is_new or self.signature != original.signature:
self.signature_date = datetime.now()

View File

@@ -1,19 +1,29 @@
from django.conf import settings
JSIGNATURE_WIDTH = getattr(settings, 'JSIGNATURE_WIDTH', 'ratio')
JSIGNATURE_HEIGHT = getattr(settings, 'JSIGNATURE_HEIGHT', 'ratio')
JSIGNATURE_COLOR = getattr(settings, 'JSIGNATURE_COLOR', '#000')
JSIGNATURE_BACKGROUND_COLOR = getattr(settings, 'JSIGNATURE_BACKGROUND_COLOR', '#FFF')
JSIGNATURE_DECOR_COLOR = getattr(settings, 'JSIGNATURE_DECOR_COLOR', '#DDD')
JSIGNATURE_LINE_WIDTH = getattr(settings, 'JSIGNATURE_LINE_WIDTH', 0)
JSIGNATURE_UNDO_BUTTON = getattr(settings, 'JSIGNATURE_UNDO_BUTTON', False)
JSIGNATURE_WIDTH = getattr(
settings, 'JSIGNATURE_WIDTH', 'ratio')
JSIGNATURE_HEIGHT = getattr(
settings, 'JSIGNATURE_HEIGHT', 'ratio')
JSIGNATURE_COLOR = getattr(
settings, 'JSIGNATURE_COLOR', '#000')
JSIGNATURE_BACKGROUND_COLOR = getattr(
settings, 'JSIGNATURE_BACKGROUND_COLOR', '#FFF')
JSIGNATURE_DECOR_COLOR = getattr(
settings, 'JSIGNATURE_DECOR_COLOR', '#DDD')
JSIGNATURE_LINE_WIDTH = getattr(
settings, 'JSIGNATURE_LINE_WIDTH', 0)
JSIGNATURE_UNDO_BUTTON = getattr(
settings, 'JSIGNATURE_UNDO_BUTTON', False)
JSIGNATURE_RESET_BUTTON = getattr(
settings, 'JSIGNATURE_RESET_BUTTON', True)
JSIGNATURE_DEFAULT_CONFIG = {
'width': JSIGNATURE_WIDTH,
'height': JSIGNATURE_HEIGHT,
'color': JSIGNATURE_COLOR,
'background-color': JSIGNATURE_BACKGROUND_COLOR,
'decor-color': JSIGNATURE_DECOR_COLOR,
'lineWidth': JSIGNATURE_LINE_WIDTH,
'UndoButton': JSIGNATURE_UNDO_BUTTON,
'width': JSIGNATURE_WIDTH,
'height': JSIGNATURE_HEIGHT,
'color': JSIGNATURE_COLOR,
'background-color': JSIGNATURE_BACKGROUND_COLOR,
'decor-color': JSIGNATURE_DECOR_COLOR,
'lineWidth': JSIGNATURE_LINE_WIDTH,
'UndoButton': JSIGNATURE_UNDO_BUTTON,
'ResetButton': JSIGNATURE_RESET_BUTTON,
}

View File

@@ -1,15 +1,21 @@
$(document).ready(function() {
$(".jsign-container").each(function(){
var config = $(this).data('config');
var value = $(this).data('initial-value');
$(this).jSignature(config);
$(this).jSignature("setData", value, "native");
});
/* Each time user is done drawing a stroke, update value of hidden input */
$(document).delegate(".jsign-container", "change", function(e) {
$(".jsign-container").on("change", function(e) {
var jSignature_data = $(this).jSignature('getData', 'native');
var django_field_name = $(this).attr('id').split('_')[1];
var django_field_name = $(this).attr('id').split(/_(.+)/)[1];
$('#id_' + django_field_name).val(JSON.stringify(jSignature_data));
});
/* Bind clear button */
$(document).delegate(".jsign-wrapper input", "click", function(e) {
$(".jsign-wrapper input").on("click", function(e) {
$(this).siblings('.jsign-container').jSignature('reset');
});
})
});

View File

@@ -0,0 +1,10 @@
<div class='jsign-wrapper'>
{{ hidden }}
<div id='{{ jsign_id }}'
data-config='{{ js_config }}'
data-initial-value='{{ value }}'
class='jsign-container'></div>
{% if config.ResetButton %}
<input type='button' value='{{ reset_btn_text }}' class="btn">
{% endif %}
</div>

View File

@@ -0,0 +1,5 @@
from .widgets import JSignatureWidgetTest
from .forms import JSignatureFormFieldTest
from .fields import JSignatureFieldTest
from .mixins import JSignatureFieldsMixinTest
from .utils import UtilsTest

View File

@@ -0,0 +1,77 @@
import json
import six
from django.test import SimpleTestCase
from django.core.exceptions import ValidationError
from ..fields import JSignatureField
from ..forms import JSignatureField as JSignatureFormField
class JSignatureFieldTest(SimpleTestCase):
def test_to_python_empty(self):
f = JSignatureField()
for val in ['', [], '[]']:
self.assertIsNone(f.to_python(val))
def test_to_python_correct_value_python(self):
f = JSignatureField()
val = [{"x": [1, 2], "y": [3, 4]}]
self.assertEquals(val, f.to_python(val))
def test_to_python_correct_value_json(self):
f = JSignatureField()
val = [{"x": [1, 2], "y": [3, 4]}]
val_str = '[{"x":[1,2], "y":[3,4]}]'
self.assertEquals(val, f.to_python(val_str))
def test_to_python_incorrect_value(self):
f = JSignatureField()
val = 'foo'
self.assertRaises(ValidationError, f.to_python, val)
def test_from_db_value_empty(self):
f = JSignatureField()
self.assertIsNone(f.from_db_value(''))
def test_from_db_value_correct_value_json(self):
f = JSignatureField()
val = [{"x": [1, 2], "y": [3, 4]}]
val_str = '[{"x":[1,2], "y":[3,4]}]'
self.assertEquals(val, f.from_db_value(val_str))
def test_from_db_value_incorrect_value(self):
f = JSignatureField()
val = 'foo'
self.assertRaises(ValidationError, f.to_python, val)
def test_get_prep_value_empty(self):
f = JSignatureField()
for val in ['', [], '[]']:
self.assertIsNone(f.get_prep_value(val))
def test_get_prep_value_correct_values_python(self):
f = JSignatureField()
val = [{"x": [1, 2], "y": [3, 4]}]
val_prep = f.get_prep_value(val)
self.assertIsInstance(val_prep, six.string_types)
self.assertEquals(val, json.loads(val_prep))
def test_get_prep_value_correct_values_json(self):
f = JSignatureField()
val = [{"x": [1, 2], "y": [3, 4]}]
val_str = '[{"x":[1,2], "y":[3,4]}]'
val_prep = f.get_prep_value(val_str)
self.assertIsInstance(val_prep, six.string_types)
self.assertEquals(val, json.loads(val_prep))
def test_get_prep_value_incorrect_values(self):
f = JSignatureField()
val = type('Foo')
self.assertRaises(ValidationError, f.get_prep_value, val)
def test_formfield(self):
f = JSignatureField()
cls = f.formfield().__class__
self.assertTrue(issubclass(cls, JSignatureFormField))

27
jsignature/tests/forms.py Normal file
View File

@@ -0,0 +1,27 @@
from django.test import SimpleTestCase
from django.core.exceptions import ValidationError
from ..widgets import JSignatureWidget
from ..forms import JSignatureField
class JSignatureFormFieldTest(SimpleTestCase):
def test_widget(self):
f = JSignatureField()
self.assertIsInstance(f.widget, JSignatureWidget)
def test_to_python_empty_values(self):
f = JSignatureField()
for val in ['', [], '[]']:
self.assertIsNone(f.to_python(val))
def test_to_python_correct_values(self):
f = JSignatureField()
val = '[{"x":[1,2], "y":[3,4]}]'
self.assertEquals([{'x': [1, 2], 'y': [3, 4]}], f.to_python(val))
def test_to_python_incorrect_values(self):
f = JSignatureField()
val = 'foo'
self.assertRaises(ValidationError, f.to_python, val)

View File

@@ -0,0 +1,61 @@
from datetime import date
from django.conf import settings
from django.db.models import loading
from django.test import SimpleTestCase
from django.core.management import call_command
from .models import JSignatureTestModel
class JSignatureFieldsMixinTest(SimpleTestCase):
def setUp(self):
self.old_installed_apps = settings.INSTALLED_APPS
settings.INSTALLED_APPS = list(settings.INSTALLED_APPS)
settings.INSTALLED_APPS.append('jsignature.tests')
loading.cache.loaded = False
call_command('syncdb', verbosity=0)
def tearDown(self):
settings.INSTALLED_APPS = self.old_installed_apps
def test_save_create(self):
# If an object is created signed, signature date must be set
signature_value = [{"x": [1, 2], "y": [3, 4]}]
i = JSignatureTestModel(signature=signature_value)
i.save()
i = JSignatureTestModel.objects.get(pk=i.pk)
self.assertEqual(date.today(), i.signature_date.date())
def test_save_no_change(self):
# If signature doesn't change, signature date must not be updated
signature_value = [{"x": [1, 2], "y": [3, 4]}]
i = JSignatureTestModel(signature=signature_value)
i.save()
i.signature_date = date(2013, 1, 1)
i.save()
i = JSignatureTestModel.objects.get(pk=i.pk)
self.assertEqual(date(2013, 1, 1), i.signature_date.date())
def test_save_change(self):
# If signature changes, signature date must be updated too
signature_value = [{"x": [1, 2], "y": [3, 4]}]
new_signature_value = [{"x": [5, 6], "y": [7, 8]}]
i = JSignatureTestModel(signature=signature_value,
signature_date=date(2013, 1, 1))
i.save()
i.signature_date = date(2013, 1, 1)
i.signature = new_signature_value
i.save()
i = JSignatureTestModel.objects.get(pk=i.pk)
self.assertEqual(date.today(), i.signature_date.date())
def test_save_none(self):
# If sinature is set to None, it must be the same for signature_date
signature_value = [{"x": [1, 2], "y": [3, 4]}]
i = JSignatureTestModel(signature=signature_value)
i.save()
i.signature = None
i.save()
i = JSignatureTestModel.objects.get(pk=i.pk)
self.assertIsNone(i.signature_date)

View File

@@ -0,0 +1,6 @@
""" Provides a dummy model implementing JSignatureFieldsMixin """
from ..mixins import JSignatureFieldsMixin
class JSignatureTestModel(JSignatureFieldsMixin):
pass

36
jsignature/tests/utils.py Normal file
View File

@@ -0,0 +1,36 @@
import json
import os
import imghdr
from PIL import Image
from django.test import SimpleTestCase
from ..utils import draw_signature
DUMMY_VALUE = [{"x": [205, 210], "y": [59, 63]},
{"x": [205, 207], "y": [67, 64]}]
DUMMY_STR_VALUE = json.dumps(DUMMY_VALUE)
class UtilsTest(SimpleTestCase):
def test_inputs_bad_str_value(self):
self.assertRaises(ValueError, draw_signature, 'foo_bar')
def test_inputs_bad_type_value(self):
self.assertRaises(ValueError, draw_signature, object())
def test_inputs_good_list_value(self):
draw_signature(DUMMY_VALUE)
def test_inputs_good_str_value(self):
draw_signature(DUMMY_STR_VALUE)
def test_outputs_as_file(self):
output = draw_signature(DUMMY_VALUE, as_file=True)
self.assertTrue(os.path.isfile(output))
self.assertIsNotNone(imghdr.what(output))
def test_outputs_as_image(self):
output = draw_signature(DUMMY_VALUE)
self.assertTrue(issubclass(output.__class__, Image.Image))
self.assertTrue(all(output.getbbox()))

View File

@@ -0,0 +1,83 @@
import json
from pyquery import PyQuery as pq
import six
from django.test import SimpleTestCase
from django.core.exceptions import ValidationError
from ..widgets import JSignatureWidget
from ..settings import JSIGNATURE_HEIGHT
class JSignatureWidgetTest(SimpleTestCase):
def test_default_media(self):
widget = JSignatureWidget()
media = widget.media
media_js = list(media.render_js())
self.assertEqual(2, len(media_js))
media_js_str = "".join(media_js)
self.assertIn('jSignature.min.js', media_js_str)
self.assertIn('django_jsignature.js', media_js_str)
media_css = list(media.render_css())
self.assertEquals([], media_css)
def test_init(self):
w = JSignatureWidget()
self.assertEquals({}, w.jsignature_attrs)
given_attrs = {'width': 300, 'height': 100}
w = JSignatureWidget(jsignature_attrs=given_attrs)
self.assertEquals(given_attrs, w.jsignature_attrs)
def test_build_jsignature_id(self):
w = JSignatureWidget()
id = w.build_jsignature_id('foo')
self.assertEqual('jsign_foo', id)
def test_build_jsignature_config(self):
w = JSignatureWidget(jsignature_attrs={'width': 400})
config = w.build_jsignature_config()
self.assertEqual(400, config.get('width'))
self.assertEqual(JSIGNATURE_HEIGHT, config.get('height'))
def test_prep_value_empty_values(self):
w = JSignatureWidget()
for val in ['', [], '[]']:
self.assertEqual('[]', w.prep_value(val))
def test_prep_value_correct_values_python(self):
w = JSignatureWidget()
val = [{"x": [1, 2], "y": [3, 4]}]
val_prep = w.prep_value(val)
self.assertIsInstance(val_prep, six.string_types)
self.assertEquals(val, json.loads(val_prep))
def test_prep_value_correct_values_json(self):
w = JSignatureWidget()
val = [{"x": [1, 2], "y": [3, 4]}]
val_str = '[{"x":[1,2], "y":[3,4]}]'
val_prep = w.prep_value(val_str)
self.assertIsInstance(val_prep, six.string_types)
self.assertEquals(val, json.loads(val_prep))
def test_prep_value_incorrect_values(self):
w = JSignatureWidget()
val = type('Foo')
self.assertRaises(ValidationError, w.prep_value, val)
def test_render(self):
w = JSignatureWidget()
output = w.render(name='foo', value=None)
# Almost useless :/
self.assertEqual(1, len(pq('.jsign-wrapper', output)))
self.assertEqual(1, len(pq('[type=hidden]', output)))
def test_render_reset_button_true(self):
w = JSignatureWidget(jsignature_attrs={'ResetButton': True})
output = w.render(name='foo', value=None)
self.assertEqual(1, len(pq('[type=button]', output)))
def test_render_reset_button_false(self):
w = JSignatureWidget(jsignature_attrs={'ResetButton': False})
output = w.render(name='foo', value=None)
self.assertEqual(0, len(pq('[type=button]', output)))

View File

@@ -3,12 +3,11 @@
https://github.com/zivezab/django-autograph/blob/master/autograph/utils.py
"""
import json
import cStringIO
from itertools import chain
from tempfile import NamedTemporaryFile
from PIL import Image, ImageDraw, ImageOps
AA = 5 # super sampling gor antialiasing
AA = 5 # super sampling gor antialiasing
def draw_signature(data, as_file=False):
""" Draw signature based on lines stored in json_string.
@@ -24,15 +23,16 @@ def draw_signature(data, as_file=False):
raise ValueError
# Compute box
width = max(chain(*[d['x'] for d in data])) + 10
height = max(chain(*[d['y'] for d in data])) + 10
width = max(chain(*[d['x'] for d in drawing])) + 10
height = max(chain(*[d['y'] for d in drawing])) + 10
# Draw image
im = Image.new("RGBA", (width*AA, height*AA))
draw = ImageDraw.Draw(im)
for line in drawing:
len_line = len(line['x'])
points = [(line['x'][i]*AA, line['y'][i]*AA) for i in range(0, len_line)]
points = [(line['x'][i]*AA, line['y'][i]*AA)
for i in range(0, len_line)]
draw.line(points, fill="#000", width=2*AA)
im = ImageOps.expand(im)
# Smart crop
@@ -45,7 +45,6 @@ def draw_signature(data, as_file=False):
if as_file:
ret = im._dump(format='PNG')
else:
ret = img
ret = im
return ret

View File

@@ -3,16 +3,29 @@
with jSignature jQuery plugin
"""
import json
import six
from django.template.loader import render_to_string
from django.forms.widgets import HiddenInput
from django.core import validators
from django.core.exceptions import ValidationError
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from jsignature.settings import JSIGNATURE_DEFAULT_CONFIG
JSIGNATURE_EMPTY_VALUES = validators.EMPTY_VALUES + ('[]', )
class JSignatureWidget(HiddenInput):
"""
A widget handling a signature capture field with with jSignature
"""
# Actually, this widget has a display so we want it to behave like a
# normal field, not a hidden one
is_hidden = False
class Media:
js = ('js/jSignature.min.js',
'js/django_jsignature.js')
@@ -22,20 +35,48 @@ class JSignatureWidget(HiddenInput):
# Store jSignature js config
self.jsignature_attrs = jsignature_attrs or {}
def render(self, name, value, attrs=None):
# Build config
jsign_id = 'jsign_%s' % name
def build_jsignature_config(self):
""" Build javascript config for jSignature initialization.
It's a dict with for which default values come from settings
and can be overriden by jsignature_attrs, given at widget
instanciation time """
jsignature_config = JSIGNATURE_DEFAULT_CONFIG.copy()
jsignature_config.update(self.jsignature_attrs)
return jsignature_config
def build_jsignature_id(self, name):
""" Build HTML id for jsignature container.
It's important because it's used in javascript code """
return 'jsign_%s' % name
def prep_value(self, value):
""" Prepare value before effectively render widget """
if value in JSIGNATURE_EMPTY_VALUES:
return "[]"
elif isinstance(value, six.string_types):
return value
elif isinstance(value, list):
return json.dumps(value)
raise ValidationError('Invalid format.')
def render(self, name, value, attrs=None):
""" Render widget """
# Build config
jsign_id = self.build_jsignature_id(name)
jsignature_config = self.build_jsignature_config()
# Prepare value
value = self.prep_value(value)
# Build output
hidden_input = super(JSignatureWidget, self).render(name, value, attrs)
div = u'<div id="%s" class="jsign-container"></div>' % jsign_id
clr = u'<input type="button" value="%s" class="btn">' % _('Reset')
js = u'$("#%s").jSignature(%s);' % (jsign_id, json.dumps(jsignature_config))
js += u'$("#%s").jSignature("setData", %s,"native");' % (jsign_id, json.dumps(value))
js = u'<script type="text/javascript">%s</script>' % js
out = u'<div class="jsign-wrapper">%s%s%s%s</div>' % (hidden_input, div, clr, js)
context = {
'hidden': super(JSignatureWidget, self).render(name, value, attrs),
'jsign_id': jsign_id,
'reset_btn_text': _('Reset'),
'config': jsignature_config,
'js_config': mark_safe(json.dumps(jsignature_config)),
'value': mark_safe(value),
}
out = render_to_string('jsignature/widget.html', context)
return mark_safe(out)

82
quicktest.py Normal file
View File

@@ -0,0 +1,82 @@
import os
import sys
import argparse
from django.conf import settings
class QuickDjangoTest(object):
"""
A quick way to run the Django test suite without a fully-configured project.
Example usage:
>>> QuickDjangoTest('app1', 'app2')
Based on a script published by Lukasz Dziedzia at:
http://stackoverflow.com/questions/3841725/how-to-launch-tests-for-django-reusable-app
"""
DIRNAME = os.path.dirname(__file__)
INSTALLED_APPS = (
)
def __init__(self, *args, **kwargs):
self.apps = args
self.run_tests()
def run_tests(self):
"""
Fire up the Django test suite developed for version 1.2
"""
settings.configure(
TEMPLATE_DIRS = ('jsignature/templates/',),
ROOT_URLCONF = 'jsignature.tests',
DEBUG = True,
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(self.DIRNAME, 'database.db'),
'USER': '',
'PASSWORD': '',
'HOST': '',
'PORT': '',
}
},
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
),
INSTALLED_APPS = self.INSTALLED_APPS + self.apps
)
# Setup is needed for Django >= 1.7
import django
if hasattr(django, 'setup'):
django.setup()
try:
from django.test.runner import DiscoverRunner
failures = DiscoverRunner().run_tests(self.apps, verbosity=1)
except ImportError:
# DjangoTestSuiteRunner has been deprecated in Django 1.7
from django.test.simple import DjangoTestSuiteRunner
failures = DjangoTestSuiteRunner().run_tests(self.apps, verbosity=1)
if failures: # pragma: no cover
sys.exit(failures)
if __name__ == '__main__':
"""
What do when the user hits this file from the shell.
Example usage:
$ python quicktest.py app1 app2
"""
parser = argparse.ArgumentParser(
usage="[args]",
description="Run Django tests on the provided applications."
)
parser.add_argument('apps', nargs='+', type=str)
args = parser.parse_args()
QuickDjangoTest(*args.apps)

View File

@@ -1,2 +1,3 @@
Django >= 1.4
pillow
pyquery
six

BIN
screen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -5,24 +5,34 @@ here = os.path.abspath(os.path.dirname(__file__))
setup(
name='django-jsignature',
version='0.7.5.dev0',
version='0.8',
author='Florent Lebreton',
author_email='florent.lebreton@makina-corpus.com',
url='https://github.com/makinacorpus/django-jsignature',
download_url = "http://pypi.python.org/pypi/django-jsignature/",
description="Use jSignature jQuery plugin in your django projects",
long_description=open(os.path.join(here, 'README.rst')).read(),
url='https://github.com/fle/django-jsignature',
download_url='https://github.com/fle/django-jsignature/tarball/0.8',
description='Use jSignature jQuery plugin in your django projects',
long_description=open(os.path.join(here, 'README.rst')).read() + '\n\n' +
open(os.path.join(here, 'CHANGES')).read(),
license='LPGL, see LICENSE file.',
install_requires=['Django'],
packages=find_packages(),
include_package_data = True,
zip_safe = False,
classifiers = ['Topic :: Utilities',
'Natural Language :: English',
'Operating System :: OS Independent',
'Intended Audience :: Developers',
'Environment :: Web Environment',
'Framework :: Django',
'Development Status :: 4 - Beta',
'Programming Language :: Python :: 2.7'],
include_package_data=True,
zip_safe=False,
classifiers=[
'Topic :: Utilities',
'Natural Language :: English',
'Operating System :: OS Independent',
'Intended Audience :: Developers',
'Environment :: Web Environment',
'Framework :: Django',
'Development Status :: 5 - Production/Stable',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4'
],
)