37 Commits
0.7.5 ... 0.8

Author SHA1 Message Date
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 760 additions and 85 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

42
.travis.yml Normal file
View File

@@ -0,0 +1,42 @@
language: python
python:
- 2.6
- 2.7
- 3.2
- 3.3
- 3.4
env:
- DJANGO_VERSION=1.4 MODULE=jsignature
- DJANGO_VERSION=1.5 MODULE=jsignature
- DJANGO_VERSION=1.6 MODULE=jsignature.tests
- DJANGO_VERSION=1.7 MODULE=jsignature.tests
install:
- pip install -r requirements.txt --use-mirrors
- pip install -q Django==$DJANGO_VERSION --use-mirrors
- pip install coverage
script: coverage run quicktest.py $MODULE
after_success:
- pip install coveralls
- coveralls
# We need to exclude old versions of Django for tests with python 3.
# We need to exclude old versions of Python for tests with Django >= 1.7.
matrix:
exclude:
- python: 3.2
env: DJANGO_VERSION=1.4 MODULE=jsignature
- python: 3.3
env: DJANGO_VERSION=1.4 MODULE=jsignature
- python: 3.4
env: DJANGO_VERSION=1.4 MODULE=jsignature
- python: 3.4
env: DJANGO_VERSION=1.5 MODULE=jsignature
- python: 3.4
env: DJANGO_VERSION=1.6 MODULE=jsignature.tests
- python: 2.6
env: DJANGO_VERSION=1.7 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 include README.rst LICENSE CHANGES
recursive-include jsignature/templates *.html
recursive-include jsignature/static *.js 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. 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: It provides:
* A form field and a form widget to handle jquery plugin through a Django form; * A form field and a form widget to handle jquery plugin through a Django form;
* A model field to store a captured signature; * 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,17 +3,21 @@
with jSignature jQuery plugin with jSignature jQuery plugin
""" """
import json import json
from django.db import models import six
from django.core import validators
from django.core.exceptions import ValidationError
from .forms import JSignatureField as JSignatureFormField
class JSignatureField(models.Field): from django.db import models
from django.core.exceptions import ValidationError
from .forms import (
JSignatureField as JSignatureFormField,
JSIGNATURE_EMPTY_VALUES)
class JSignatureField(six.with_metaclass(models.SubfieldBase, models.Field)):
""" """
A model field handling a signature captured with jSignature A model field handling a signature captured with jSignature
""" """
description = "A signature captured with jSignature" description = "A signature captured with jSignature"
__metaclass__ = models.SubfieldBase
def get_internal_type(self): def get_internal_type(self):
return 'TextField' return 'TextField'
@@ -23,7 +27,7 @@ class JSignatureField(models.Field):
Validates that the input can be red as a JSON object. Returns a Python Validates that the input can be red as a JSON object. Returns a Python
datetime.date object. datetime.date object.
""" """
if value in validators.EMPTY_VALUES: if value in JSIGNATURE_EMPTY_VALUES:
return None return None
elif isinstance(value, list): elif isinstance(value, list):
return value return value
@@ -33,12 +37,9 @@ class JSignatureField(models.Field):
raise ValidationError('Invalid JSON format.') raise ValidationError('Invalid JSON format.')
def get_prep_value(self, value): def get_prep_value(self, value):
return self.to_python(value) if value in JSIGNATURE_EMPTY_VALUES:
def get_db_prep_value(self, value, connection, prepared=False):
if value in validators.EMPTY_VALUES:
return None return None
elif isinstance(value, basestring): elif isinstance(value, six.string_types):
return value return value
elif isinstance(value, list): elif isinstance(value, list):
return json.dumps(value) return json.dumps(value)
@@ -49,6 +50,8 @@ class JSignatureField(models.Field):
defaults.update(kwargs) defaults.update(kwargs)
return super(JSignatureField, self).formfield(**defaults) return super(JSignatureField, self).formfield(**defaults)
try:
from south.modelsinspector import add_introspection_rules from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ["jsignature.fields.JSignatureField"]) add_introspection_rules([], ["jsignature.fields.JSignatureField"])
except ImportError:
pass

View File

@@ -8,6 +8,9 @@ from django.core import validators
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from .widgets import JSignatureWidget from .widgets import JSignatureWidget
JSIGNATURE_EMPTY_VALUES = validators.EMPTY_VALUES + ('[]', )
class JSignatureField(Field): class JSignatureField(Field):
""" """
A field handling a signature capture field with with jSignature A field handling a signature capture field with with jSignature
@@ -16,10 +19,10 @@ class JSignatureField(Field):
def to_python(self, value): def to_python(self, value):
""" """
Validates that the input can be red as a JSON object. Returns a Python Validates that the input can be red as a JSON object.
datetime.date object. Returns a Python list (JSON object unserialized).
""" """
if value in validators.EMPTY_VALUES: if value in JSIGNATURE_EMPTY_VALUES:
return None return None
try: try:
return json.loads(value) 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 django.utils.translation import ugettext_lazy as _
from .fields import JSignatureField from .fields import JSignatureField
class JSignatureFieldsMixin(models.Model): class JSignatureFieldsMixin(models.Model):
""" Mixin class providing fields to store a signature with jSignature """ """ Mixin class providing fields to store a signature with jSignature """
signature = JSignatureField( signature = JSignatureField(
@@ -24,9 +25,8 @@ class JSignatureFieldsMixin(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
is_new = self.pk is None 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 self.signature:
if is_new or self.signature != original.signature: if is_new or self.signature != original.signature:
self.signature_date = datetime.now() self.signature_date = datetime.now()

View File

@@ -1,12 +1,21 @@
from django.conf import settings from django.conf import settings
JSIGNATURE_WIDTH = getattr(settings, 'JSIGNATURE_WIDTH', 'ratio') JSIGNATURE_WIDTH = getattr(
JSIGNATURE_HEIGHT = getattr(settings, 'JSIGNATURE_HEIGHT', 'ratio') settings, 'JSIGNATURE_WIDTH', 'ratio')
JSIGNATURE_COLOR = getattr(settings, 'JSIGNATURE_COLOR', '#000') JSIGNATURE_HEIGHT = getattr(
JSIGNATURE_BACKGROUND_COLOR = getattr(settings, 'JSIGNATURE_BACKGROUND_COLOR', '#FFF') settings, 'JSIGNATURE_HEIGHT', 'ratio')
JSIGNATURE_DECOR_COLOR = getattr(settings, 'JSIGNATURE_DECOR_COLOR', '#DDD') JSIGNATURE_COLOR = getattr(
JSIGNATURE_LINE_WIDTH = getattr(settings, 'JSIGNATURE_LINE_WIDTH', 0) settings, 'JSIGNATURE_COLOR', '#000')
JSIGNATURE_UNDO_BUTTON = getattr(settings, 'JSIGNATURE_UNDO_BUTTON', False) 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 = { JSIGNATURE_DEFAULT_CONFIG = {
'width': JSIGNATURE_WIDTH, 'width': JSIGNATURE_WIDTH,
@@ -16,4 +25,5 @@ JSIGNATURE_DEFAULT_CONFIG = {
'decor-color': JSIGNATURE_DECOR_COLOR, 'decor-color': JSIGNATURE_DECOR_COLOR,
'lineWidth': JSIGNATURE_LINE_WIDTH, 'lineWidth': JSIGNATURE_LINE_WIDTH,
'UndoButton': JSIGNATURE_UNDO_BUTTON, 'UndoButton': JSIGNATURE_UNDO_BUTTON,
'ResetButton': JSIGNATURE_RESET_BUTTON,
} }

View File

@@ -1,15 +1,21 @@
$(document).ready(function() { $(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 */ /* 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 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)); $('#id_' + django_field_name).val(JSON.stringify(jSignature_data));
}); });
/* Bind clear button */ /* Bind clear button */
$(document).delegate(".jsign-wrapper input", "click", function(e) { $(".jsign-wrapper input").on("click", function(e) {
$(this).siblings('.jsign-container').jSignature('reset'); $(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,62 @@
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_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,13 +3,12 @@
https://github.com/zivezab/django-autograph/blob/master/autograph/utils.py https://github.com/zivezab/django-autograph/blob/master/autograph/utils.py
""" """
import json import json
import cStringIO
from itertools import chain from itertools import chain
from tempfile import NamedTemporaryFile
from PIL import Image, ImageDraw, ImageOps 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): def draw_signature(data, as_file=False):
""" Draw signature based on lines stored in json_string. """ Draw signature based on lines stored in json_string.
`data` can be a json object (list in fact) or a json string `data` can be a json object (list in fact) or a json string
@@ -24,15 +23,16 @@ def draw_signature(data, as_file=False):
raise ValueError raise ValueError
# Compute box # Compute box
width = max(chain(*[d['x'] for d in data])) + 10 width = max(chain(*[d['x'] for d in drawing])) + 10
height = max(chain(*[d['y'] for d in data])) + 10 height = max(chain(*[d['y'] for d in drawing])) + 10
# Draw image # Draw image
im = Image.new("RGBA", (width*AA, height*AA)) im = Image.new("RGBA", (width*AA, height*AA))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
for line in drawing: for line in drawing:
len_line = len(line['x']) 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) draw.line(points, fill="#000", width=2*AA)
im = ImageOps.expand(im) im = ImageOps.expand(im)
# Smart crop # Smart crop
@@ -45,7 +45,6 @@ def draw_signature(data, as_file=False):
if as_file: if as_file:
ret = im._dump(format='PNG') ret = im._dump(format='PNG')
else: else:
ret = img ret = im
return ret return ret

View File

@@ -3,16 +3,29 @@
with jSignature jQuery plugin with jSignature jQuery plugin
""" """
import json import json
import six
from django.template.loader import render_to_string
from django.forms.widgets import HiddenInput 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.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from jsignature.settings import JSIGNATURE_DEFAULT_CONFIG from jsignature.settings import JSIGNATURE_DEFAULT_CONFIG
JSIGNATURE_EMPTY_VALUES = validators.EMPTY_VALUES + ('[]', )
class JSignatureWidget(HiddenInput): class JSignatureWidget(HiddenInput):
""" """
A widget handling a signature capture field with with jSignature 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: class Media:
js = ('js/jSignature.min.js', js = ('js/jSignature.min.js',
'js/django_jsignature.js') 'js/django_jsignature.js')
@@ -22,20 +35,48 @@ class JSignatureWidget(HiddenInput):
# Store jSignature js config # Store jSignature js config
self.jsignature_attrs = jsignature_attrs or {} self.jsignature_attrs = jsignature_attrs or {}
def render(self, name, value, attrs=None): def build_jsignature_config(self):
""" Build javascript config for jSignature initialization.
# Build config It's a dict with for which default values come from settings
jsign_id = 'jsign_%s' % name and can be overriden by jsignature_attrs, given at widget
instanciation time """
jsignature_config = JSIGNATURE_DEFAULT_CONFIG.copy() jsignature_config = JSIGNATURE_DEFAULT_CONFIG.copy()
jsignature_config.update(self.jsignature_attrs) 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 # Build output
hidden_input = super(JSignatureWidget, self).render(name, value, attrs) context = {
div = u'<div id="%s" class="jsign-container"></div>' % jsign_id 'hidden': super(JSignatureWidget, self).render(name, value, attrs),
clr = u'<input type="button" value="%s" class="btn">' % _('Reset') 'jsign_id': jsign_id,
js = u'$("#%s").jSignature(%s);' % (jsign_id, json.dumps(jsignature_config)) 'reset_btn_text': _('Reset'),
js += u'$("#%s").jSignature("setData", %s,"native");' % (jsign_id, json.dumps(value)) 'config': jsignature_config,
js = u'<script type="text/javascript">%s</script>' % js 'js_config': mark_safe(json.dumps(jsignature_config)),
out = u'<div class="jsign-wrapper">%s%s%s%s</div>' % (hidden_input, div, clr, js) 'value': mark_safe(value),
}
out = render_to_string('jsignature/widget.html', context)
return mark_safe(out) 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 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( setup(
name='django-jsignature', name='django-jsignature',
version='0.7.5.dev0', version='0.8',
author='Florent Lebreton', author='Florent Lebreton',
author_email='florent.lebreton@makina-corpus.com', author_email='florent.lebreton@makina-corpus.com',
url='https://github.com/makinacorpus/django-jsignature', url='https://github.com/fle/django-jsignature',
download_url = "http://pypi.python.org/pypi/django-jsignature/", download_url='https://github.com/fle/django-jsignature/tarball/0.8',
description="Use jSignature jQuery plugin in your django projects", description='Use jSignature jQuery plugin in your django projects',
long_description=open(os.path.join(here, 'README.rst')).read(), long_description=open(os.path.join(here, 'README.rst')).read() + '\n\n' +
open(os.path.join(here, 'CHANGES')).read(),
license='LPGL, see LICENSE file.', license='LPGL, see LICENSE file.',
install_requires=['Django'], install_requires=['Django'],
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
classifiers = ['Topic :: Utilities', classifiers=[
'Topic :: Utilities',
'Natural Language :: English', 'Natural Language :: English',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Environment :: Web Environment', 'Environment :: Web Environment',
'Framework :: Django', 'Framework :: Django',
'Development Status :: 4 - Beta', 'Development Status :: 5 - Production/Stable',
'Programming Language :: Python :: 2.7'], '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'
],
) )