Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6da81f2115 | ||
|
|
d12b1e80ac | ||
|
|
880033d3a5 | ||
|
|
c15074873b | ||
|
|
8e912f9738 | ||
|
|
70b4f2e9b6 | ||
|
|
124fe21d06 | ||
|
|
738eb869fc | ||
|
|
32566873a4 | ||
|
|
bbde0d7005 | ||
|
|
11ae827643 | ||
|
|
337a8f9a08 | ||
|
|
51005db0ca | ||
|
|
02e78b1a6c | ||
|
|
b29c5964af | ||
|
|
5c7f1d7037 | ||
|
|
7666e781e5 | ||
|
|
27eeca4f8f | ||
|
|
a8bf966933 | ||
|
|
202df097b0 | ||
|
|
0de152f292 | ||
|
|
1fd41a622a | ||
|
|
b87ba745e7 | ||
|
|
69e81bd808 | ||
|
|
91b0e4d400 | ||
|
|
a86cc67520 | ||
|
|
271b4239a2 | ||
|
|
ca5ba2d0f8 | ||
|
|
95860405f3 | ||
|
|
ff2494a33e | ||
|
|
9c8ad2087e | ||
|
|
a8ef50d8d0 | ||
|
|
5c17f753e7 | ||
|
|
ed42468767 | ||
|
|
a19d0f9545 | ||
|
|
c4bb0b8fd1 | ||
|
|
86791e12f3 |
2
.coveragerc
Normal file
2
.coveragerc
Normal file
@@ -0,0 +1,2 @@
|
||||
[run]
|
||||
source = jsignature
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.pyc
|
||||
.coverage
|
||||
htmlcov
|
||||
42
.travis.yml
Normal file
42
.travis.yml
Normal 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
24
CHANGES
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
django-jsignature
|
||||
=================
|
||||
|
||||
Use jSignature jQuery plugin in your django projects
|
||||
143
README.rst
143
README.rst
@@ -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
|
||||
|
||||
|
||||
@@ -3,17 +3,21 @@
|
||||
with jSignature jQuery plugin
|
||||
"""
|
||||
import json
|
||||
from django.db import models
|
||||
from django.core import validators
|
||||
from django.core.exceptions import ValidationError
|
||||
from .forms import JSignatureField as JSignatureFormField
|
||||
import six
|
||||
|
||||
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
|
||||
"""
|
||||
description = "A signature captured with jSignature"
|
||||
__metaclass__ = models.SubfieldBase
|
||||
|
||||
def get_internal_type(self):
|
||||
return 'TextField'
|
||||
@@ -23,7 +27,7 @@ class JSignatureField(models.Field):
|
||||
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
|
||||
@@ -33,12 +37,9 @@ class JSignatureField(models.Field):
|
||||
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:
|
||||
if value in JSIGNATURE_EMPTY_VALUES:
|
||||
return None
|
||||
elif isinstance(value, basestring):
|
||||
elif isinstance(value, six.string_types):
|
||||
return value
|
||||
elif isinstance(value, list):
|
||||
return json.dumps(value)
|
||||
@@ -49,6 +50,8 @@ class JSignatureField(models.Field):
|
||||
defaults.update(kwargs)
|
||||
return super(JSignatureField, self).formfield(**defaults)
|
||||
|
||||
|
||||
try:
|
||||
from south.modelsinspector import add_introspection_rules
|
||||
add_introspection_rules([], ["jsignature.fields.JSignatureField"])
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -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)
|
||||
|
||||
BIN
jsignature/locale/fr/LC_MESSAGES/django.mo
Normal file
BIN
jsignature/locale/fr/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
30
jsignature/locale/fr/LC_MESSAGES/django.po
Normal file
30
jsignature/locale/fr/LC_MESSAGES/django.po
Normal 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"
|
||||
@@ -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()
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
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,
|
||||
@@ -16,4 +25,5 @@ JSIGNATURE_DEFAULT_CONFIG = {
|
||||
'decor-color': JSIGNATURE_DECOR_COLOR,
|
||||
'lineWidth': JSIGNATURE_LINE_WIDTH,
|
||||
'UndoButton': JSIGNATURE_UNDO_BUTTON,
|
||||
'ResetButton': JSIGNATURE_RESET_BUTTON,
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
10
jsignature/templates/jsignature/widget.html
Normal file
10
jsignature/templates/jsignature/widget.html
Normal 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>
|
||||
5
jsignature/tests/__init__.py
Normal file
5
jsignature/tests/__init__.py
Normal 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
|
||||
62
jsignature/tests/fields.py
Normal file
62
jsignature/tests/fields.py
Normal 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
27
jsignature/tests/forms.py
Normal 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)
|
||||
61
jsignature/tests/mixins.py
Normal file
61
jsignature/tests/mixins.py
Normal 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)
|
||||
6
jsignature/tests/models.py
Normal file
6
jsignature/tests/models.py
Normal 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
36
jsignature/tests/utils.py
Normal 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()))
|
||||
83
jsignature/tests/widgets.py
Normal file
83
jsignature/tests/widgets.py
Normal 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)))
|
||||
@@ -3,13 +3,12 @@
|
||||
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
|
||||
|
||||
|
||||
def draw_signature(data, as_file=False):
|
||||
""" Draw signature based on lines stored in 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
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
@@ -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
82
quicktest.py
Normal 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)
|
||||
@@ -1,2 +1,3 @@
|
||||
Django >= 1.4
|
||||
pillow
|
||||
pyquery
|
||||
six
|
||||
|
||||
BIN
screen.png
Normal file
BIN
screen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
26
setup.py
26
setup.py
@@ -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',
|
||||
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'],
|
||||
'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'
|
||||
],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user