Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae58c7f725 | ||
|
|
cf9a37a009 | ||
|
|
382c824cca | ||
|
|
63c3d7d2dc | ||
|
|
ea3800375c | ||
|
|
a42a9ca543 | ||
|
|
9e81795f99 | ||
|
|
7fe2560910 | ||
|
|
e92132349c | ||
|
|
a740767a06 | ||
|
|
ea249473f1 | ||
|
|
bea517d746 | ||
|
|
4dd9e5f25f | ||
|
|
72e9f3d6da | ||
|
|
7601258ea8 |
66
.github/workflows/actions.yml
vendored
Normal file
66
.github/workflows/actions.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
name: Run Tests
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
django_version:
|
||||||
|
- '2.2'
|
||||||
|
- '3.2'
|
||||||
|
- '4.0'
|
||||||
|
python-version:
|
||||||
|
- 3.6
|
||||||
|
- 3.7
|
||||||
|
- 3.8
|
||||||
|
- 3.9
|
||||||
|
- "3.10"
|
||||||
|
exclude:
|
||||||
|
- django_version: '2.2'
|
||||||
|
python-version: '3.10'
|
||||||
|
|
||||||
|
- django_version: '4.0'
|
||||||
|
python-version: '3.6'
|
||||||
|
|
||||||
|
- django_version: '4.0'
|
||||||
|
python-version: '3.7'
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Cache pip
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
# This path is specific to Ubuntu
|
||||||
|
path: ~/.cache/pip
|
||||||
|
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ matrix.django_version }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -e .
|
||||||
|
pip install -U flake8 coveralls argparse
|
||||||
|
pip install -U Django~=${{ matrix.django_version }}
|
||||||
|
|
||||||
|
- name: Lint with flake8
|
||||||
|
run: flake8 --ignore=E501,W504 jsignature
|
||||||
|
|
||||||
|
- name: Test Django
|
||||||
|
run: |
|
||||||
|
python -W error::DeprecationWarning -W error::PendingDeprecationWarning \
|
||||||
|
-m coverage run ./runtests.py
|
||||||
|
|
||||||
|
- name: Coverage
|
||||||
|
if: ${{ success() }}
|
||||||
|
run: |
|
||||||
|
coveralls --service=github
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,3 +4,5 @@ htmlcov
|
|||||||
*.egg-info
|
*.egg-info
|
||||||
.tox
|
.tox
|
||||||
example.db
|
example.db
|
||||||
|
/dist
|
||||||
|
/build
|
||||||
|
|||||||
46
.travis.yml
46
.travis.yml
@@ -1,46 +0,0 @@
|
|||||||
language: python
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
fast_finish: true
|
|
||||||
allow_failures:
|
|
||||||
- env: DJANGO=master
|
|
||||||
include:
|
|
||||||
- python: 2.7
|
|
||||||
env: DJANGO=1.11
|
|
||||||
|
|
||||||
- python: 3.5
|
|
||||||
env: DJANGO=1.11
|
|
||||||
- python: 3.5
|
|
||||||
env: DJANGO=2.2
|
|
||||||
|
|
||||||
- python: 3.6
|
|
||||||
env: DJANGO=1.11
|
|
||||||
- python: 3.6
|
|
||||||
env: DJANGO=2.2
|
|
||||||
- python: 3.6
|
|
||||||
env: DJANGO=3.0
|
|
||||||
- python: 3.6
|
|
||||||
env: DJANGO=master
|
|
||||||
|
|
||||||
- python: 3.7
|
|
||||||
env: DJANGO=2.2
|
|
||||||
- python: 3.7
|
|
||||||
env: DJANGO=3.0
|
|
||||||
- python: 3.7
|
|
||||||
env: DJANGO=master
|
|
||||||
|
|
||||||
|
|
||||||
- python: 3.8
|
|
||||||
env: DJANGO=2.2
|
|
||||||
- python: 3.8
|
|
||||||
env: DJANGO=3.0
|
|
||||||
- python: 3.8
|
|
||||||
env: DJANGO=master
|
|
||||||
|
|
||||||
install:
|
|
||||||
- pip install tox tox-travis
|
|
||||||
script:
|
|
||||||
- tox
|
|
||||||
after_success:
|
|
||||||
- pip install coveralls
|
|
||||||
- coveralls
|
|
||||||
21
CHANGES
21
CHANGES
@@ -2,6 +2,27 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
0.11 (2022-01-17)
|
||||||
|
==================
|
||||||
|
|
||||||
|
** New **
|
||||||
|
|
||||||
|
- Django 4.0 compatibility.
|
||||||
|
- Moved to Github Actions for testing.
|
||||||
|
- Added flake8 in CI.
|
||||||
|
- Dropped support for python < 3.6
|
||||||
|
- Dropped support for Django 1.11, 2.0, 2.1, 3.0, 3.1.
|
||||||
|
|
||||||
|
|
||||||
|
0.10 (2020-07-26)
|
||||||
|
==================
|
||||||
|
|
||||||
|
** New **
|
||||||
|
|
||||||
|
- Add template filter to easily render a signature structure as base64 url.
|
||||||
|
- Add a full example of front-office usage.
|
||||||
|
|
||||||
|
|
||||||
0.9 (2020-04-18)
|
0.9 (2020-04-18)
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
|||||||
104
README.rst
104
README.rst
@@ -5,6 +5,7 @@ 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.
|
||||||
|
* A template filter to render signatures as base64 image urls.
|
||||||
|
|
||||||
.. image:: https://img.shields.io/pypi/v/django-jsignature.svg
|
.. image:: https://img.shields.io/pypi/v/django-jsignature.svg
|
||||||
:target: https://pypi.python.org/pypi/django-jsignature/
|
:target: https://pypi.python.org/pypi/django-jsignature/
|
||||||
@@ -21,17 +22,15 @@ It provides:
|
|||||||
.. image:: https://github.com/fle/django-jsignature/blob/master/screen.png
|
.. image:: https://github.com/fle/django-jsignature/blob/master/screen.png
|
||||||
|
|
||||||
==================
|
==================
|
||||||
INSTALL
|
Installation
|
||||||
==================
|
==================
|
||||||
|
|
||||||
For now:
|
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
pip install django-jsignature
|
pip install django-jsignature
|
||||||
|
|
||||||
==================
|
==================
|
||||||
USAGE
|
Usage
|
||||||
==================
|
==================
|
||||||
|
|
||||||
* Add ``jsignature`` to your ``INSTALLED_APPS``:
|
* Add ``jsignature`` to your ``INSTALLED_APPS``:
|
||||||
@@ -44,48 +43,37 @@ USAGE
|
|||||||
'jsignature',
|
'jsignature',
|
||||||
)
|
)
|
||||||
|
|
||||||
* Use provided form field and widget:
|
* Use provided model field (for easy storage):
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
# forms.py
|
# models.py
|
||||||
from django import forms
|
from django.db import models
|
||||||
from jsignature.forms import JSignatureField
|
from jsignature.fields import JSignatureField
|
||||||
|
|
||||||
class SignatureForm(forms.Form):
|
class SignatureModel(models.Model):
|
||||||
signature = JSignatureField()
|
signature = JSignatureField()
|
||||||
|
|
||||||
* In your template
|
* In your form template
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
{{ form.media }}
|
{{ form.media }}
|
||||||
<form action="." method="POST">
|
<form action="" method="post">
|
||||||
{% for field in form %}
|
{{ form }}
|
||||||
{{ field.label_tag }}
|
<input type="submit" value="Save" />
|
||||||
{{ field }}
|
|
||||||
{% endfor %}
|
|
||||||
<input type="submit" value="Save"/>
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
* Render image after form validation:
|
* Render image from db value in your display template:
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
# views.py
|
{# yourtemplate.html #}
|
||||||
from jsignature.utils import draw_signature
|
{% load jsignature_filters %}
|
||||||
from myapp.forms import SignatureForm
|
|
||||||
|
<img src="{{ obj.signature|signature_base64 }}" alt="{{ obj }}" />
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
* By default, jSignature is made to work outside of admin, requiring that
|
* By default, jSignature is made to work outside of admin, requiring that
|
||||||
you include the jQuery library in your ``<head>``.
|
you include the jQuery library in your ``<head>``.
|
||||||
@@ -94,8 +82,11 @@ USAGE
|
|||||||
``JSIGNATURE_JQUERY`` setting to ``admin``. Otherwise if set to any url
|
``JSIGNATURE_JQUERY`` setting to ``admin``. Otherwise if set to any url
|
||||||
pointing to jQuery, it will be automatically included.
|
pointing to jQuery, it will be automatically included.
|
||||||
|
|
||||||
|
It is strongly suggested to take example from ``example_project``, which is
|
||||||
|
`located in this repo <https://github.com/fle/django-jsignature/tree/master/example_project>`_
|
||||||
|
|
||||||
==================
|
==================
|
||||||
CUSTOMIZATION
|
Customization
|
||||||
==================
|
==================
|
||||||
|
|
||||||
JSignature plugin options are available in python:
|
JSignature plugin options are available in python:
|
||||||
@@ -130,10 +121,11 @@ Available settings are:
|
|||||||
* ``JSIGNATURE_RESET_BUTTON`` (ResetButton)
|
* ``JSIGNATURE_RESET_BUTTON`` (ResetButton)
|
||||||
|
|
||||||
==================
|
==================
|
||||||
IN YOUR MODELS
|
In your models
|
||||||
==================
|
==================
|
||||||
|
|
||||||
If you want to store signatures, provided mixin gives a ``signature`` and a ``signature_date`` that update themselves:
|
If you want to store signatures easily, a provided mixin gives a ``signature``
|
||||||
|
and a ``signature_date`` that update themselves:
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
@@ -144,6 +136,41 @@ If you want to store signatures, provided mixin gives a ``signature`` and a ``si
|
|||||||
name = models.CharField()
|
name = models.CharField()
|
||||||
|
|
||||||
|
|
||||||
|
==================
|
||||||
|
In your forms
|
||||||
|
==================
|
||||||
|
|
||||||
|
* If you need more precise handling of the form field, you can use it directly:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
# forms.py
|
||||||
|
from django import forms
|
||||||
|
from jsignature.forms import JSignatureField
|
||||||
|
|
||||||
|
class SignatureForm(forms.Form):
|
||||||
|
signature = JSignatureField()
|
||||||
|
|
||||||
|
|
||||||
|
* And upon saving, have direct access to the image with ``draw_signature()``
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
==================
|
==================
|
||||||
Example project
|
Example project
|
||||||
==================
|
==================
|
||||||
@@ -162,16 +189,17 @@ If you want to have a demo of this package, just use the example project:
|
|||||||
./manage.py migrate
|
./manage.py migrate
|
||||||
./manage.py createsuperuser
|
./manage.py createsuperuser
|
||||||
|
|
||||||
Fill the user info, launch django with ``./manage.py runserver`` and head over to
|
Fill the user info, launch django with ``./manage.py runserver`` and head over
|
||||||
`http://127.0.0.1:8000/ <http://127.0.0.1:8000/>`_ and login with the
|
to `http://127.0.0.1:8000/ <http://127.0.0.1:8000/>`_, you can also
|
||||||
credentials your provided.
|
`login to the admin <http://127.0.0.1:8000/admin>`_ with the credentials your
|
||||||
|
provided.
|
||||||
|
|
||||||
==================
|
==================
|
||||||
AUTHORS
|
Authors
|
||||||
==================
|
==================
|
||||||
|
|
||||||
* Florent Lebreton <florent.lebreton@makina-corpus.com>
|
* Florent Lebreton <florent.lebreton@makina-corpus.com> (original author)
|
||||||
* Sébastien Corbin <sebastien.corbin@makina-corpus.com>
|
* Sébastien Corbin <sebastien.corbin@makina-corpus.com> (maintainer)
|
||||||
|
|
||||||
|makinacom|_
|
|makinacom|_
|
||||||
|
|
||||||
|
|||||||
39
example_project/templates/base.html
Normal file
39
example_project/templates/base.html
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<!-- Required meta tags -->
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<title>Example project</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||||
|
<div class="container">
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'admin:index' %}">Admnistration</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'list' %}">List</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'create' %}">Create</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
|
||||||
|
{% block extra_media %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block extra_media %}
|
||||||
|
{{ form.media }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form action="" method="post">
|
||||||
|
{{ form }}
|
||||||
|
<input type="submit" value="Save" class="btn btn-success" />
|
||||||
|
<a href="{% url 'list' %}" class="btn btn-danger">Cancel</a>
|
||||||
|
{% csrf_token %}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{% extends "base.html" %}{% load jsignature_filters %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>List</h1>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{% for obj in object_list %}
|
||||||
|
<li>
|
||||||
|
<dl>
|
||||||
|
<dt>Raw data (from db)</dt>
|
||||||
|
<dd>{{ obj.signature }}</dd>
|
||||||
|
<dt>As image</dt>
|
||||||
|
<dd>
|
||||||
|
<img src="{{ obj.signature|signature_base64 }}" alt="{{ obj }}" />
|
||||||
|
<a href="{% url 'update' obj.pk %}" class="btn btn-primary">
|
||||||
|
Update
|
||||||
|
</a>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<a href="{% url 'create' %}" class="btn btn-primary">Create</a>
|
||||||
|
{% endblock %}
|
||||||
@@ -3,6 +3,11 @@ from __future__ import unicode_literals
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
|
from example_project import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', admin.site.urls),
|
path('', views.ExampleListView.as_view(), name='list'),
|
||||||
|
path('create', views.ExampleCreateView.as_view(), name='create'),
|
||||||
|
path('update/<int:pk>', views.ExampleUpdateView.as_view(), name='update'),
|
||||||
|
path('admin', admin.site.urls),
|
||||||
]
|
]
|
||||||
|
|||||||
20
example_project/views.py
Normal file
20
example_project/views.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.views import generic
|
||||||
|
|
||||||
|
from example_project.models import ExampleModel
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleCreateView(generic.CreateView):
|
||||||
|
model = ExampleModel
|
||||||
|
fields = '__all__'
|
||||||
|
success_url = reverse_lazy('list')
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleUpdateView(generic.UpdateView):
|
||||||
|
model = ExampleModel
|
||||||
|
fields = '__all__'
|
||||||
|
success_url = reverse_lazy('list')
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleListView(generic.ListView):
|
||||||
|
model = ExampleModel
|
||||||
@@ -18,13 +18,13 @@ class JSignatureField(Field):
|
|||||||
widget = JSignatureWidget()
|
widget = JSignatureWidget()
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
"""
|
"""
|
||||||
Validates that the input can be red as a JSON object.
|
Validates that the input can be red as a JSON object.
|
||||||
Returns a Python list (JSON object unserialized).
|
Returns a Python list (JSON object unserialized).
|
||||||
"""
|
"""
|
||||||
if value in JSIGNATURE_EMPTY_VALUES:
|
if value in JSIGNATURE_EMPTY_VALUES:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
return json.loads(value)
|
return json.loads(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValidationError('Invalid JSON format.')
|
raise ValidationError('Invalid JSON format.')
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from .fields import JSignatureField
|
from .fields import JSignatureField
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
0
jsignature/templatetags/__init__.py
Normal file
0
jsignature/templatetags/__init__.py
Normal file
21
jsignature/templatetags/jsignature_filters.py
Normal file
21
jsignature/templatetags/jsignature_filters.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import base64
|
||||||
|
import io
|
||||||
|
|
||||||
|
from django import template
|
||||||
|
from django.utils.encoding import iri_to_uri
|
||||||
|
|
||||||
|
from jsignature.utils import draw_signature
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def signature_base64(value):
|
||||||
|
if value is None or not isinstance(value, str):
|
||||||
|
return ""
|
||||||
|
in_mem_file = io.BytesIO()
|
||||||
|
draw_signature(value).save(in_mem_file, format="PNG")
|
||||||
|
in_mem_file.seek(0)
|
||||||
|
return "data:image/png;base64,{}".format(
|
||||||
|
iri_to_uri(base64.b64encode(in_mem_file.read()).decode('utf8'))
|
||||||
|
)
|
||||||
@@ -15,25 +15,31 @@ def draw_signature(data, as_file=False):
|
|||||||
if `as_file` is True, a temp file is returned instead of Image instance
|
if `as_file` is True, a temp file is returned instead of Image instance
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def _remove_empty_pts(pt):
|
||||||
|
return {
|
||||||
|
'x': list(filter(lambda n: n is not None, pt['x'])),
|
||||||
|
'y': list(filter(lambda n: n is not None, pt['y']))
|
||||||
|
}
|
||||||
|
|
||||||
if type(data) is str:
|
if type(data) is str:
|
||||||
drawing = json.loads(data)
|
drawing = json.loads(data, object_hook=_remove_empty_pts)
|
||||||
elif type(data) is list:
|
elif type(data) is list:
|
||||||
drawing = data
|
drawing = data
|
||||||
else:
|
else:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
|
||||||
# Compute box
|
# Compute box
|
||||||
width = max(chain(*[d['x'] for d in drawing])) + 10
|
width = int(round(max(chain(*[d['x'] for d in drawing])))) + 10
|
||||||
height = max(chain(*[d['y'] for d in drawing])) + 10
|
height = int(round(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)
|
points = [(line['x'][i] * AA, line['y'][i] * AA)
|
||||||
for i in range(0, len_line)]
|
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
|
||||||
bbox = im.getbbox()
|
bbox = im.getbbox()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from django.core import validators
|
|||||||
from django.core.exceptions import ValidationError
|
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 gettext_lazy as _
|
||||||
from jsignature.settings import JSIGNATURE_DEFAULT_CONFIG
|
from jsignature.settings import JSIGNATURE_DEFAULT_CONFIG
|
||||||
|
|
||||||
JSIGNATURE_EMPTY_VALUES = validators.EMPTY_VALUES + ('[]', )
|
JSIGNATURE_EMPTY_VALUES = validators.EMPTY_VALUES + ('[]', )
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pillow
|
pillow
|
||||||
pyquery
|
pyquery<1.4.2; python_version<="2.7"
|
||||||
|
pyquery>=1.4.2; python_version>"2.7"
|
||||||
|
|||||||
22
setup.py
22
setup.py
@@ -5,7 +5,7 @@ here = os.path.abspath(os.path.dirname(__file__))
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='django-jsignature',
|
name='django-jsignature',
|
||||||
version='0.9',
|
version='0.11',
|
||||||
author='Florent Lebreton',
|
author='Florent Lebreton',
|
||||||
author_email='florent.lebreton@makina-corpus.com',
|
author_email='florent.lebreton@makina-corpus.com',
|
||||||
url='https://github.com/fle/django-jsignature',
|
url='https://github.com/fle/django-jsignature',
|
||||||
@@ -14,8 +14,8 @@ setup(
|
|||||||
long_description=open(os.path.join(here, 'README.rst')).read() + '\n\n' +
|
long_description=open(os.path.join(here, 'README.rst')).read() + '\n\n' +
|
||||||
open(os.path.join(here, 'CHANGES')).read(),
|
open(os.path.join(here, 'CHANGES')).read(),
|
||||||
license='LPGL, see LICENSE file.',
|
license='LPGL, see LICENSE file.',
|
||||||
install_requires=['Django', 'pillow'],
|
install_requires=['Django>=1.11', 'pillow', 'pyquery>=1.4.2'],
|
||||||
packages=find_packages(),
|
packages=find_packages(exclude=['example_project*', 'tests']),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
classifiers=[
|
classifiers=[
|
||||||
@@ -29,13 +29,13 @@ setup(
|
|||||||
'Framework :: Django :: 2.2',
|
'Framework :: Django :: 2.2',
|
||||||
'Framework :: Django :: 3.0',
|
'Framework :: Django :: 3.0',
|
||||||
'Development Status :: 5 - Production/Stable',
|
'Development Status :: 5 - Production/Stable',
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
'Programming Language :: Python :: 2',
|
'Programming Language :: Python :: 2',
|
||||||
'Programming Language :: Python :: 2.7',
|
'Programming Language :: Python :: 2.7',
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.5'
|
'Programming Language :: Python :: 3.5',
|
||||||
'Programming Language :: Python :: 3.6'
|
'Programming Language :: Python :: 3.6',
|
||||||
'Programming Language :: Python :: 3.7'
|
'Programming Language :: Python :: 3.7',
|
||||||
'Programming Language :: Python :: 3.8'
|
'Programming Language :: Python :: 3.8',
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import os
|
|||||||
|
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
|
||||||
SECRET_KEY = 'thisisntactuallysecretatall'
|
SECRET_KEY = 'thisisntactuallysecretatall'
|
||||||
|
|||||||
12
tests/test_faulty_signature.py
Normal file
12
tests/test_faulty_signature.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
|
from jsignature.templatetags.jsignature_filters import signature_base64
|
||||||
|
|
||||||
|
FAULTY_SIGNATURE = [{"x": [120.00675156801722, 116.11464070635179, 114.16858527551909], "y": [83.0316983821957, 76.54484694608666, 224.44505968937275]}, {"x": [161.52260075911508, 165.4147116207805, 173.84761848772226, 184.2265807854967, 201.0923945193802], "y": [84.97775381302841, 79.78827266414118, 72.00405094081033, 62.922458930257676, 56.43560749414864]}]
|
||||||
|
DUMMY_STR_VALUE = json.dumps(FAULTY_SIGNATURE)
|
||||||
|
|
||||||
|
class TemplateFilterFailedTest(SimpleTestCase):
|
||||||
|
def test_throw_error(self):
|
||||||
|
output = signature_base64(DUMMY_STR_VALUE)
|
||||||
@@ -24,13 +24,13 @@ class JSignatureFieldTest(SimpleTestCase):
|
|||||||
def test_to_python_correct_value_python(self):
|
def test_to_python_correct_value_python(self):
|
||||||
f = JSignatureField()
|
f = JSignatureField()
|
||||||
val = [{"x": [1, 2], "y": [3, 4]}]
|
val = [{"x": [1, 2], "y": [3, 4]}]
|
||||||
self.assertEquals(val, f.to_python(val))
|
self.assertEqual(val, f.to_python(val))
|
||||||
|
|
||||||
def test_to_python_correct_value_json(self):
|
def test_to_python_correct_value_json(self):
|
||||||
f = JSignatureField()
|
f = JSignatureField()
|
||||||
val = [{"x": [1, 2], "y": [3, 4]}]
|
val = [{"x": [1, 2], "y": [3, 4]}]
|
||||||
val_str = '[{"x":[1,2], "y":[3,4]}]'
|
val_str = '[{"x":[1,2], "y":[3,4]}]'
|
||||||
self.assertEquals(val, f.to_python(val_str))
|
self.assertEqual(val, f.to_python(val_str))
|
||||||
|
|
||||||
def test_to_python_incorrect_value(self):
|
def test_to_python_incorrect_value(self):
|
||||||
f = JSignatureField()
|
f = JSignatureField()
|
||||||
@@ -47,7 +47,7 @@ class JSignatureFieldTest(SimpleTestCase):
|
|||||||
val = [{"x": [1, 2], "y": [3, 4]}]
|
val = [{"x": [1, 2], "y": [3, 4]}]
|
||||||
val_prep = f.get_prep_value(val)
|
val_prep = f.get_prep_value(val)
|
||||||
self.assertIsInstance(val_prep, string_types)
|
self.assertIsInstance(val_prep, string_types)
|
||||||
self.assertEquals(val, json.loads(val_prep))
|
self.assertEqual(val, json.loads(val_prep))
|
||||||
|
|
||||||
def test_get_prep_value_correct_values_json(self):
|
def test_get_prep_value_correct_values_json(self):
|
||||||
f = JSignatureField()
|
f = JSignatureField()
|
||||||
@@ -55,7 +55,7 @@ class JSignatureFieldTest(SimpleTestCase):
|
|||||||
val_str = '[{"x":[1,2], "y":[3,4]}]'
|
val_str = '[{"x":[1,2], "y":[3,4]}]'
|
||||||
val_prep = f.get_prep_value(val_str)
|
val_prep = f.get_prep_value(val_str)
|
||||||
self.assertIsInstance(val_prep, string_types)
|
self.assertIsInstance(val_prep, string_types)
|
||||||
self.assertEquals(val, json.loads(val_prep))
|
self.assertEqual(val, json.loads(val_prep))
|
||||||
|
|
||||||
def test_get_prep_value_incorrect_values(self):
|
def test_get_prep_value_incorrect_values(self):
|
||||||
f = JSignatureField()
|
f = JSignatureField()
|
||||||
|
|||||||
19
tests/test_filter.py
Normal file
19
tests/test_filter.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
|
from jsignature.templatetags.jsignature_filters import signature_base64
|
||||||
|
|
||||||
|
DUMMY_VALUE = [{"x": [205, 210], "y": [59, 63]},
|
||||||
|
{"x": [205, 207], "y": [67, 64]}]
|
||||||
|
DUMMY_STR_VALUE = json.dumps(DUMMY_VALUE)
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateFilterTest(SimpleTestCase):
|
||||||
|
def test_inputs_bad_type_value(self):
|
||||||
|
self.assertEqual(signature_base64(object()), '')
|
||||||
|
self.assertEqual(signature_base64(None), '')
|
||||||
|
|
||||||
|
def test_outputs_as_base64(self):
|
||||||
|
output = signature_base64(DUMMY_STR_VALUE)
|
||||||
|
self.assertEqual(output, "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANwAAABNCAYAAADNYApnAAABIklEQVR4nO3ZP0odURjG4d/ce5dgE3EHgXTp07iD6B7cga3YpUoh7iFrCti4BkGw0OLMoEVIEW4yxPs81ZxzGPial+/8KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPh3ttW0dhFwCN4GbbNaFXAAlrCdVh/m791KtcC7tmkE7mP1XP2sPs1rQgd7NjVCd1TdNkL3UJ2tWRQciovqqRG8q0aXc6aDPZsat5RVX6q7Rui+zXPbX/wD/KFla7mc306qH9XneazLwZ4s3eu8eqwuV6wF3rXllvK4um9sI7/Oc7s8hMNeLVf/3xthu5nHzmzwF0y9drjrxvPAMgcA/7/lWUBnAwAAAAAAfucFwt4TZmdXW+AAAAAASUVORK5CYII=")
|
||||||
@@ -19,7 +19,7 @@ class JSignatureFormFieldTest(SimpleTestCase):
|
|||||||
def test_to_python_correct_values(self):
|
def test_to_python_correct_values(self):
|
||||||
f = JSignatureField()
|
f = JSignatureField()
|
||||||
val = '[{"x":[1,2], "y":[3,4]}]'
|
val = '[{"x":[1,2], "y":[3,4]}]'
|
||||||
self.assertEquals([{'x': [1, 2], 'y': [3, 4]}], f.to_python(val))
|
self.assertEqual([{'x': [1, 2], 'y': [3, 4]}], f.to_python(val))
|
||||||
|
|
||||||
def test_to_python_incorrect_values(self):
|
def test_to_python_incorrect_values(self):
|
||||||
f = JSignatureField()
|
f = JSignatureField()
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class JSignatureWidgetTest(SimpleTestCase):
|
|||||||
self.assertIn('jSignature.min.js', media_js_str)
|
self.assertIn('jSignature.min.js', media_js_str)
|
||||||
self.assertIn('django_jsignature.js', media_js_str)
|
self.assertIn('django_jsignature.js', media_js_str)
|
||||||
media_css = list(media.render_css())
|
media_css = list(media.render_css())
|
||||||
self.assertEquals([], media_css)
|
self.assertEqual([], media_css)
|
||||||
|
|
||||||
@override_settings(JSIGNATURE_JQUERY='admin')
|
@override_settings(JSIGNATURE_JQUERY='admin')
|
||||||
def test_media_in_admin(self):
|
def test_media_in_admin(self):
|
||||||
@@ -34,12 +34,19 @@ class JSignatureWidgetTest(SimpleTestCase):
|
|||||||
media_js = list(media.render_js())
|
media_js = list(media.render_js())
|
||||||
self.assertEqual(4, len(media_js))
|
self.assertEqual(4, len(media_js))
|
||||||
|
|
||||||
|
@override_settings(JSIGNATURE_JQUERY='https://code.jquery.com/jquery-3.5.0.min.js')
|
||||||
|
def test_media_custom_jquery(self):
|
||||||
|
widget = JSignatureWidget()
|
||||||
|
media = widget.media
|
||||||
|
media_js = list(media.render_js())
|
||||||
|
self.assertEqual(3, len(media_js))
|
||||||
|
|
||||||
def test_init(self):
|
def test_init(self):
|
||||||
w = JSignatureWidget()
|
w = JSignatureWidget()
|
||||||
self.assertEquals({}, w.jsignature_attrs)
|
self.assertEqual({}, w.jsignature_attrs)
|
||||||
given_attrs = {'width': 300, 'height': 100}
|
given_attrs = {'width': 300, 'height': 100}
|
||||||
w = JSignatureWidget(jsignature_attrs=given_attrs)
|
w = JSignatureWidget(jsignature_attrs=given_attrs)
|
||||||
self.assertEquals(given_attrs, w.jsignature_attrs)
|
self.assertEqual(given_attrs, w.jsignature_attrs)
|
||||||
|
|
||||||
def test_build_jsignature_id(self):
|
def test_build_jsignature_id(self):
|
||||||
w = JSignatureWidget()
|
w = JSignatureWidget()
|
||||||
@@ -62,7 +69,7 @@ class JSignatureWidgetTest(SimpleTestCase):
|
|||||||
val = [{"x": [1, 2], "y": [3, 4]}]
|
val = [{"x": [1, 2], "y": [3, 4]}]
|
||||||
val_prep = w.prep_value(val)
|
val_prep = w.prep_value(val)
|
||||||
self.assertIsInstance(val_prep, string_types)
|
self.assertIsInstance(val_prep, string_types)
|
||||||
self.assertEquals(val, json.loads(val_prep))
|
self.assertEqual(val, json.loads(val_prep))
|
||||||
|
|
||||||
def test_prep_value_correct_values_json(self):
|
def test_prep_value_correct_values_json(self):
|
||||||
w = JSignatureWidget()
|
w = JSignatureWidget()
|
||||||
@@ -70,7 +77,7 @@ class JSignatureWidgetTest(SimpleTestCase):
|
|||||||
val_str = '[{"x":[1,2], "y":[3,4]}]'
|
val_str = '[{"x":[1,2], "y":[3,4]}]'
|
||||||
val_prep = w.prep_value(val_str)
|
val_prep = w.prep_value(val_str)
|
||||||
self.assertIsInstance(val_prep, string_types)
|
self.assertIsInstance(val_prep, string_types)
|
||||||
self.assertEquals(val, json.loads(val_prep))
|
self.assertEqual(val, json.loads(val_prep))
|
||||||
|
|
||||||
def test_prep_value_incorrect_values(self):
|
def test_prep_value_incorrect_values(self):
|
||||||
w = JSignatureWidget()
|
w = JSignatureWidget()
|
||||||
|
|||||||
26
tox.ini
26
tox.ini
@@ -1,23 +1,25 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
{py27,py35,py36}-django111,
|
{py36,py37,py38,py39,py310}-django22,
|
||||||
{py35,py36,py37,py38}-django22,
|
{py36,py37,py38,py39,py310}-django32,
|
||||||
{py36,py37,py38}-django{30,master}
|
{py38,py39,py310}-django{40,master},
|
||||||
|
py310-djangomaster
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
deps=
|
deps=
|
||||||
django111: Django>=1.11,<2.0
|
|
||||||
django22: Django>=2.2,<3.0
|
django22: Django>=2.2,<3.0
|
||||||
django30: Django>=3.0,<4.0
|
django32: Django>=3.2,<4.0
|
||||||
djangomaster: https://github.com/django/django/archive/master.tar.gz
|
django40: Django>=4.0,<4.1
|
||||||
|
djangomaster: https://github.com/django/django/archive/main.tar.gz
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
coverage
|
coverage
|
||||||
|
|
||||||
commands= coverage run ./runtests.py
|
commands= coverage run ./runtests.py
|
||||||
|
|
||||||
[travis:env]
|
[gh-actions]
|
||||||
DJANGO =
|
python =
|
||||||
1.11: django111
|
3.6: py36-django{22,32}
|
||||||
2.2: django22
|
3.7: py37-django{22,32}
|
||||||
3.0: django30
|
3.8: py38-django{22,32,40}
|
||||||
master: djangomaster
|
3.9: py39-django{22,32,40}
|
||||||
|
3.10: py310-django{22,32,40,master}
|
||||||
|
|||||||
Reference in New Issue
Block a user