├── .coveragerc ├── .github ├── CODEOWNERS └── workflows │ └── main.yml ├── .gitignore ├── AUTHORS ├── CHANGELOG ├── CONTRIBUTING.rst ├── INSTALL ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── VERSION ├── demo ├── README.rst ├── django_anysign_demo │ ├── __init__.py │ ├── fixtures │ │ ├── demo.json │ │ └── hello-world.txt │ ├── manage.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20160705_0819.py │ │ └── __init__.py │ ├── models.py │ ├── settings.py │ ├── signature_urls.py │ ├── templates │ │ ├── callback.html │ │ ├── home.html │ │ ├── send.html │ │ ├── signer.html │ │ └── signer_return.html │ ├── tests.py │ ├── urls.py │ ├── views.py │ └── wsgi.py ├── requirements.pip └── setup.py ├── django_anysign ├── __init__.py ├── api.py ├── backend.py ├── loading.py ├── models.py ├── settings.py └── utils │ ├── __init__.py │ └── importlib.py ├── django_dummysign ├── __init__.py └── backend.py ├── docs ├── Makefile ├── about │ ├── alternatives.txt │ ├── authors.txt │ ├── changelog.txt │ ├── index.txt │ ├── license.txt │ └── vision.txt ├── backends.txt ├── conf.py ├── contributing.txt ├── demo.txt ├── index.txt ├── install.txt ├── loading.txt ├── models.txt ├── settings.txt └── views.txt ├── setup.cfg ├── setup.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | 2 | [run] 3 | branch = True 4 | 5 | [report] 6 | show_missing = True 7 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @peopledoc/esignature 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | include: 11 | - python-version: 3.8 12 | tox-options: "-e flake8 -e py38-django22 -e py38-django32 -e readme -e sphinx" 13 | - python-version: 3.7 14 | tox-options: "-e py37-django22 -e py37-django32" 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Setup Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install tox and any other packages 22 | run: pip install tox 23 | - name: Run tox 24 | run: tox ${{ matrix.tox-options }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python files. 2 | *.pyc 3 | *.pyo 4 | *.egg-info 5 | 6 | # Tox files. 7 | /.tox/ 8 | 9 | # Virtualenv files (created by tox). 10 | /build/ 11 | /dist/ 12 | 13 | # Data files. 14 | /var/ -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | ###################### 2 | Authors & contributors 3 | ###################### 4 | 5 | Author: Benoît Bryon , as a former member of the 6 | `PeopleDoc`_ team: https://github.com/peopledoc/ 7 | 8 | Maintainer: the PeopleDoc team 9 | 10 | Developers: https://github.com/peopledoc/django-anysign/graphs/contributors 11 | 12 | 13 | .. rubric:: Notes & references 14 | 15 | .. target-notes:: 16 | 17 | .. _`PeopleDoc`: https://www.people-doc.com 18 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | This document describes changes between each past release. For information 5 | about future releases, check `milestones`_ and :doc:`/about/vision`. 6 | 7 | 8 | 1.6 (unreleased) 9 | ---------------- 10 | 11 | - Nothing changed yet. 12 | 13 | 14 | 1.5 (2022-02-04) 15 | ---------------- 16 | 17 | - Drop compat for Django <2.2.27 and Python 3.5 and 3.6 18 | 19 | - Add compat for Django 3.2 and Python 3.8 20 | 21 | - Run rests for Django 2.2.27, 3.2 and Python 3.7, 3.8 22 | 23 | - Run flake8, sphinx and readme on Python 3.8 24 | 25 | - Move from travis to circleci 26 | 27 | 1.4 (2019-04-17) 28 | ---------------- 29 | 30 | - Drop compat for django <1.11 and Python 3.4 31 | 32 | - Run tests for Django 2.2 and Python 3.5, 3.6 and 3.7 33 | 34 | - Run flake8, sphinx and readme on Python 3.6 35 | 36 | 37 | 1.3 (2018-02-15) 38 | ---------------- 39 | 40 | - Bug #40 - Remove some RemovedInDjango20Warning warnings. 41 | 42 | - Added support of Django 2.0. 43 | 44 | - Added support of Python 3.6. 45 | 46 | - Project repository moved to github.com/peopledoc (was github.com/novafloss). 47 | 48 | 49 | 1.2 (2017-09-04) 50 | ---------------- 51 | 52 | - Add explicit on_delete on ForeignKeys. 53 | 54 | - Confirm compatibility with Django 1.11. 55 | 56 | 57 | 1.1 (2017-03-24) 58 | ---------------- 59 | 60 | - Feature #35 - Add support of Django 1.10. 61 | 62 | 63 | 1.0 (2016-07-18) 64 | ---------------- 65 | 66 | - Feature #21 - Drop support of Django 1.5, 1.6 and 1.7 67 | Explicitly mark Django 1.5, 1.6 and 1.7 as not supported 68 | (tests fail with those versions) in packaging. 69 | 70 | - Add support of Django 1.9. 71 | 72 | - Breaking: `django_anysign.api` package is no longer imported in 73 | `django_anysign` root package, as it is not compatible with Django 1.9. 74 | Instead of doing ``import django_anysign``, just do: 75 | ``from django_anysign import api as django_anysign``. 76 | 77 | - Feature #24 - Use django UUIDField instead of uuidfield external app. 78 | 79 | 80 | 0.4 (2015-06-25) 81 | ---------------- 82 | 83 | Workaround identifiers and Django version. 84 | 85 | - Features #7 and #8 - ``Signature`` and ``Signer`` models have 86 | ``anysign_internal_id`` attribute. It is an unique identifier for signature 87 | or signer on Django side. For use as a "foreign key" in backend's database, 88 | whenever possible. It defaults to an UUID. You may override it with a custom 89 | property if your models already have some UUID. 90 | 91 | - Feature #5 - ``Signer`` model has ``signature_backend_id`` attribute. Use it 92 | to store the backend's signer identifier, i.e. signer's identifier in 93 | external database. 94 | 95 | - Refactoring #15 - Project repository moved to github.com/novafloss (was 96 | github.com/novapost). 97 | 98 | - Refactoring #18 - Tox runs tests for multiple Django version. 99 | 100 | - Features #16 and #17 - Tests include Django version 1.7 and 1.8. 101 | 102 | 103 | 0.3 (2014-10-08) 104 | ---------------- 105 | 106 | Signers' ordering. 107 | 108 | - Feature #4 - Added ``signing_order`` attribute to ``Signer`` model. 109 | 110 | 111 | 0.2 (2014-09-12) 112 | ---------------- 113 | 114 | Minor fixes. 115 | 116 | - Feature #2 - Explicitely mark Django 1.7 as not supported (tests fail with 117 | Django 1.7) in packaging. 118 | 119 | - Bug #3 - Fixed wrong usage of `django-anysign` API in `django-dummysign` 120 | backend. 121 | 122 | 123 | 0.1 (2014-08-11) 124 | ---------------- 125 | 126 | Initial release. 127 | 128 | - Introduced base model ``SignatureType`` and base model factories 129 | ``SignatureFactory`` and ``SignerFactory``. 130 | 131 | - Introduced base backend class ``SignatureBackend``. 132 | 133 | - Introduced loaders for custom models and backend: 134 | ``get_signature_backend_instance``, ``get_signature_type_model``, 135 | ``get_signature_model`` and ``get_signer_model``. 136 | 137 | 138 | .. rubric:: Notes & references 139 | 140 | .. target-notes:: 141 | 142 | .. _`milestones`: https://github.com/peopledoc/django-anysign/milestones 143 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ############ 2 | Contributing 3 | ############ 4 | 5 | This document provides guidelines for people who want to contribute to the 6 | `django-anysign` project. 7 | 8 | 9 | ************** 10 | Create tickets 11 | ************** 12 | 13 | Please use `django-anysign bugtracker`_ **before** starting some work: 14 | 15 | * check if the bug or feature request has already been filed. It may have been 16 | answered too! 17 | 18 | * else create a new ticket. 19 | 20 | * if you plan to contribute, tell us, so that we are given an opportunity to 21 | give feedback as soon as possible. 22 | 23 | * Then, in your commit messages, reference the ticket with some 24 | ``refs #TICKET-ID`` syntax. 25 | 26 | 27 | ****************** 28 | Use topic branches 29 | ****************** 30 | 31 | * Work in branches. 32 | 33 | * Prefix your branch with the ticket ID corresponding to the issue. As an 34 | example, if you are working on ticket #23 which is about contribute 35 | documentation, name your branch like ``23-contribute-doc``. 36 | 37 | 38 | *********** 39 | Fork, clone 40 | *********** 41 | 42 | Clone `django-anysign` repository (adapt to use your own fork): 43 | 44 | .. code:: sh 45 | 46 | git clone git@github.com:peopledoc/django-anysign.git 47 | cd django-anysign/ 48 | 49 | 50 | ************* 51 | Usual actions 52 | ************* 53 | 54 | The `Makefile` is the reference card for usual actions in development 55 | environment: 56 | 57 | * Install development toolkit with `pip`_: ``make develop``. 58 | 59 | * Run tests with `tox`_: ``make test``. 60 | 61 | * Build documentation: ``make documentation``. It builds `Sphinx`_ 62 | documentation in `var/docs/html/index.html`. 63 | 64 | * Release `django-anysign` project with `zest.releaser`_: ``make release``. 65 | 66 | * Cleanup local repository: ``make clean``, ``make distclean`` and 67 | ``make maintainer-clean``. 68 | 69 | See also ``make help``. 70 | 71 | 72 | .. rubric:: Notes & references 73 | 74 | .. target-notes:: 75 | 76 | .. _`django-anysign bugtracker`: https://github.com/peopledoc/django-anysign/issues 77 | .. _`rebase`: https://git-scm.com/book/en/v2/Git-Branching-Rebasing 78 | .. _`merge-based rebase`: https://tech.people-doc.com/psycho-rebasing.html 79 | .. _`pip`: https://pypi.org/project/pip/ 80 | .. _`tox`: https://pypi.org/project/tox/ 81 | .. _`Sphinx`: https://pypi.org/project/Sphinx/ 82 | .. _`zest.releaser`: https://pypi.org/project/zest.releaser/ 83 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | ####### 2 | Install 3 | ####### 4 | 5 | `django-anysign` is open-source software, published under BSD license. 6 | See :doc:`/about/license` for details. 7 | 8 | .. note:: 9 | 10 | If you want to install a development environment, you should go to 11 | :doc:`/contributing` documentation. 12 | 13 | 14 | ************ 15 | Requirements 16 | ************ 17 | 18 | `django-anysign` has been tested against `Python`_ 2.7, 3.5, 3.6 and 3.7. Other 19 | versions may work, but they are not part of the test suite at the moment. 20 | 21 | Installing `django-anysign` will automatically trigger the installation of the 22 | following requirements: 23 | 24 | .. literalinclude:: /../setup.py 25 | :language: python 26 | :start-after: BEGIN requirements 27 | :end-before: END requirements 28 | 29 | 30 | ************ 31 | As a library 32 | ************ 33 | 34 | In most cases, you will use `django-anysign` as a dependency of another 35 | project. In such a case, you should add ``django-anysign`` in your main 36 | project's requirements. Typically in :file:`setup.py`: 37 | 38 | .. code:: python 39 | 40 | from setuptools import setup 41 | 42 | setup( 43 | install_requires=[ 44 | 'django-anysign', 45 | #... 46 | ] 47 | # ... 48 | ) 49 | 50 | Then when you install your main project with your favorite package manager 51 | (like `pip`_), `django-anysign` and its recursive dependencies will 52 | automatically be installed. 53 | 54 | 55 | ********** 56 | Standalone 57 | ********** 58 | 59 | You can install `django-anysign` with your favorite Python package manager. 60 | As an example with `pip`_: 61 | 62 | .. code:: sh 63 | 64 | pip install django-anysign 65 | 66 | 67 | ***** 68 | Check 69 | ***** 70 | 71 | Check `django-anysign` has been installed: 72 | 73 | .. code:: sh 74 | 75 | python -c "import django_anysign;print(django_anysign.__version__)" 76 | 77 | You should get installed `django_anysign`'s version. 78 | 79 | 80 | .. rubric:: References 81 | 82 | .. target-notes:: 83 | 84 | .. _`Python`: https://www.python.org/ 85 | .. _`pip`: https://pypi.org/project/pip/ 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ####### 2 | License 3 | ####### 4 | 5 | Copyright (c) 2014-2016, PeopleDoc. 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of django-anysign nor the names of its contributors 19 | may be used to endorse or promote products derived from this software without 20 | specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include django_anysign * 2 | recursive-include django_dummysign * 3 | global-exclude *.pyc 4 | include AUTHORS 5 | include CHANGELOG 6 | include CONTRIBUTING.rst 7 | include INSTALL 8 | include LICENSE 9 | include README.rst 10 | include VERSION 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Reference card for usual actions in development environment. 2 | # 3 | # For standard installation of django-anysign as a library, see INSTALL. 4 | # 5 | # For details about django-anysign's development environment, see 6 | # CONTRIBUTING.rst. 7 | # 8 | PIP = pip 9 | TOX = tox 10 | PROJECT = $(shell python -c "import setup; print setup.NAME") 11 | DEMO = $(PROJECT)-demo 12 | 13 | 14 | #: help - Display callable targets. 15 | .PHONY: help 16 | help: 17 | @echo "Reference card for usual actions in development environment." 18 | @echo "Here are available targets:" 19 | @egrep -o "^#: (.+)" [Mm]akefile | sed 's/#: /* /' 20 | 21 | 22 | #: develop - Install minimal development utilities. 23 | .PHONY: develop 24 | develop: 25 | mkdir -p var 26 | $(PIP) install -e .[test] 27 | 28 | 29 | #: clean - Basic cleanup, mostly temporary files. 30 | .PHONY: clean 31 | clean: 32 | find . -name "*.pyc" -delete 33 | find . -name '*.pyo' -delete 34 | find . -name "__pycache__" -delete 35 | 36 | 37 | #: distclean - Remove local builds, such as *.egg-info. 38 | .PHONY: distclean 39 | distclean: clean 40 | rm -rf *.egg 41 | rm -rf *.egg-info 42 | rm -rf demo/*.egg-info 43 | 44 | 45 | #: maintainer-clean - Remove almost everything that can be re-generated. 46 | .PHONY: maintainer-clean 47 | maintainer-clean: distclean 48 | rm -rf build/ 49 | rm -rf dist/ 50 | rm -rf .tox/ 51 | 52 | 53 | #: test - Run test suites. 54 | .PHONY: test 55 | test: 56 | $(TOX) 57 | 58 | 59 | #: documentation - Build documentation (Sphinx, README, ...) 60 | .PHONY: documentation 61 | documentation: sphinx readme 62 | 63 | 64 | #: sphinx - Build Sphinx documentation (docs). 65 | .PHONY: sphinx 66 | sphinx: 67 | $(TOX) -e sphinx 68 | 69 | 70 | #: readme - Build standalone documentation files (README, CONTRIBUTING...). 71 | .PHONY: readme 72 | readme: 73 | $(TOX) -e readme 74 | 75 | 76 | #: demo - Install and setup demo project. 77 | .PHONY: demo 78 | demo: develop 79 | pip install -e demo 80 | $(DEMO) migrate --noinput 81 | 82 | 83 | #: serve - Run development server for demo project. 84 | .PHONY: serve 85 | serve: demo 86 | $(DEMO) runserver 87 | 88 | #: release - Tag and push to PyPI. 89 | .PHONY: release 90 | release: 91 | $(TOX) -e release 92 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |Build Status| |Documentation Status| |Pypi Status| 2 | 3 | ############## 4 | django-anysign 5 | ############## 6 | 7 | `django-anysign` is a Django application to manage online signature in a 8 | generic way. 9 | 10 | Its goal is to provide a consistent API whatever the signature implementation, 11 | so that you can either: 12 | 13 | * switch from one signature backend to another; 14 | * use several backends at once. 15 | 16 | See `Alternatives & related projects`_ for details about supported online 17 | signature services. 18 | 19 | 20 | ************** 21 | Project status 22 | ************** 23 | 24 | `django-anysign` is under active development. The project is not mature yet, 25 | but authors already use it! It means that, while API and implementation may 26 | change (improve!), authors do care of the changes. 27 | 28 | Also, help is welcome! Feel free to report issues, request features or 29 | refactoring! 30 | 31 | 32 | ********* 33 | Resources 34 | ********* 35 | 36 | * Documentation: https://django-anysign.readthedocs.io 37 | * PyPI page: https://pypi.org/project/django-anysign 38 | * Bugtracker: https://github.com/peopledoc/django-anysign/issues 39 | * Changelog: https://django-anysign.readthedocs.io/en/latest/about/changelog.html 40 | * Code repository: https://github.com/peopledoc/django-anysign 41 | * Continuous integration: https://travis-ci.org/peopledoc/django-anysign 42 | 43 | .. _`Alternatives & related projects`: 44 | https://django-anysign.readthedocs.io/en/latest/about/alternatives.html 45 | 46 | .. |Build Status| image:: https://github.com/peopledoc/django-anysign/actions/workflows/main.yml/badge.svg?branch=master 47 | :target: https://github.com/peopledoc/django-anysign/actions 48 | 49 | .. |Documentation Status| image:: https://readthedocs.org/projects/django-anysign/badge/ 50 | :target: http://django-anysign.readthedocs.io/en/latest/ 51 | 52 | .. |Pypi Status| image:: https://img.shields.io/pypi/v/django-anysign.svg 53 | :target: https://pypi.org/project/django-anysign 54 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.6.dev0 2 | -------------------------------------------------------------------------------- /demo/README.rst: -------------------------------------------------------------------------------- 1 | ############ 2 | Demo project 3 | ############ 4 | 5 | `Demo folder in project's repository`_ contains a Django project to illustrate 6 | `django-anysign` usage. It basically integrates :ref:`django-dummysign-section` 7 | in a project. 8 | 9 | Examples in the documentation are imported from the demo project. 10 | 11 | Feel free to use the demo project as a sandbox. See :doc:`/contributing` for 12 | details about development environment setup. 13 | 14 | 15 | .. rubric:: Notes & references 16 | 17 | .. target-notes:: 18 | 19 | .. _`demo folder in project's repository`: 20 | https://github.com/peopledoc/django-anysign/tree/master/demo/ 21 | -------------------------------------------------------------------------------- /demo/django_anysign_demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peopledoc/django-anysign/cba177dc95e8ea940682ca24fd62420a2d077dfd/demo/django_anysign_demo/__init__.py -------------------------------------------------------------------------------- /demo/django_anysign_demo/fixtures/demo.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "object.document", 5 | "fields": { 6 | "slug": "hello-world", 7 | "file": "document/hello-world.txt" 8 | } 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /demo/django_anysign_demo/fixtures/hello-world.txt: -------------------------------------------------------------------------------- 1 | Hello world! 2 | -------------------------------------------------------------------------------- /demo/django_anysign_demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | from django.core.management import execute_from_command_line 6 | 7 | 8 | def main(): 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", 10 | "{package}.settings".format(package=__package__)) 11 | execute_from_command_line(sys.argv) 12 | 13 | 14 | if __name__ == "__main__": 15 | main() 16 | -------------------------------------------------------------------------------- /demo/django_anysign_demo/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | import uuid 4 | 5 | import django.db.models.deletion 6 | from django.db import models, migrations 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Signature', 17 | fields=[ 18 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 19 | ('signature_backend_id', models.CharField(default='', max_length=100, verbose_name='ID for signature backend', db_index=True, blank=True)), 20 | ('anysign_internal_id', models.UUIDField(verbose_name='ID in internal database', default=uuid.uuid4)), 21 | ], 22 | options={ 23 | 'abstract': False, 24 | }, 25 | ), 26 | migrations.CreateModel( 27 | name='SignatureType', 28 | fields=[ 29 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 30 | ('signature_backend_code', models.CharField(max_length=50, verbose_name='signature backend', db_index=True)), 31 | ], 32 | options={ 33 | 'abstract': False, 34 | }, 35 | ), 36 | migrations.CreateModel( 37 | name='Signer', 38 | fields=[ 39 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 40 | ('signing_order', models.PositiveSmallIntegerField(default=0, help_text='Position in the list of signers. Starts at 1.', verbose_name='signing order')), 41 | ('signature_backend_id', models.CharField(default='', max_length=100, verbose_name='ID in signature backend', db_index=True, blank=True)), 42 | ('anysign_internal_id', models.UUIDField(verbose_name='ID in internal database', default=uuid.uuid4)), 43 | ('signature', models.ForeignKey(related_name='signers', on_delete=django.db.models.deletion.CASCADE, to='django_anysign_demo.Signature')), 44 | ], 45 | options={ 46 | 'abstract': False, 47 | }, 48 | ), 49 | migrations.AddField( 50 | model_name='signature', 51 | name='signature_type', 52 | field=models.ForeignKey(verbose_name='signature type', on_delete=django.db.models.deletion.CASCADE, to='django_anysign_demo.SignatureType'), 53 | ), 54 | ] 55 | -------------------------------------------------------------------------------- /demo/django_anysign_demo/migrations/0002_auto_20160705_0819.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_anysign_demo', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='signer', 16 | name='signing_order', 17 | field=models.PositiveSmallIntegerField(default=0, help_text='Position in the list of signers.', verbose_name='signing order'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /demo/django_anysign_demo/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peopledoc/django-anysign/cba177dc95e8ea940682ca24fd62420a2d077dfd/demo/django_anysign_demo/migrations/__init__.py -------------------------------------------------------------------------------- /demo/django_anysign_demo/models.py: -------------------------------------------------------------------------------- 1 | from django_anysign import api as django_anysign 2 | 3 | 4 | class SignatureType(django_anysign.SignatureType): 5 | pass 6 | 7 | 8 | class Signature(django_anysign.SignatureFactory(SignatureType)): 9 | pass 10 | 11 | 12 | class Signer(django_anysign.SignerFactory(Signature)): 13 | pass 14 | -------------------------------------------------------------------------------- /demo/django_anysign_demo/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Django settings for django-anysign demo project.""" 3 | import os 4 | 5 | 6 | # Configure some relative directories. 7 | demoproject_dir = os.path.dirname(os.path.abspath(__file__)) 8 | demo_dir = os.path.dirname(demoproject_dir) 9 | root_dir = os.path.dirname(demo_dir) 10 | data_dir = os.path.join(root_dir, 'var') 11 | cfg_dir = os.path.join(root_dir, 'etc') 12 | 13 | 14 | # Mandatory settings. 15 | ROOT_URLCONF = 'django_anysign_demo.urls' 16 | WSGI_APPLICATION = 'django_anysign_demo.wsgi.application' 17 | 18 | 19 | # Database. 20 | DATABASES = { 21 | 'default': { 22 | 'ENGINE': 'django.db.backends.sqlite3', 23 | 'NAME': os.path.join(data_dir, 'db.sqlite'), 24 | } 25 | } 26 | 27 | 28 | # Template. 29 | TEMPLATES = [ 30 | { 31 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 32 | 'APP_DIRS': True, 33 | }, 34 | ] 35 | 36 | 37 | # Required. 38 | SECRET_KEY = "This is a secret made public on project's repository." 39 | 40 | # Media and static files. 41 | MEDIA_ROOT = os.path.join(data_dir, 'media') 42 | MEDIA_URL = '/media/' 43 | STATIC_ROOT = os.path.join(data_dir, 'static') 44 | STATIC_URL = '/static/' 45 | 46 | 47 | # Applications. 48 | INSTALLED_APPS = [ 49 | # The actual django-anysign demo. 50 | 'django_anysign_demo', 51 | # Standard Django applications. 52 | 'django.contrib.auth', 53 | 'django.contrib.contenttypes', 54 | 'django.contrib.sessions', 55 | 'django.contrib.sites', 56 | 'django.contrib.messages', 57 | 'django.contrib.staticfiles', 58 | # Stuff that must be at the end. 59 | 'django_nose', 60 | ] 61 | 62 | 63 | # BEGIN middlewares 64 | MIDDLEWARE_CLASSES = [ 65 | 'django.middleware.common.CommonMiddleware', 66 | 'django.middleware.csrf.CsrfViewMiddleware', 67 | ] 68 | # END middlewares 69 | 70 | 71 | # BEGIN settings.ANYSIGN. 72 | ANYSIGN = { 73 | 'BACKENDS': { 74 | 'dummysign': 'django_dummysign.backend.DummySignBackend', 75 | }, 76 | 'SIGNATURE_TYPE_MODEL': 'django_anysign_demo.models.SignatureType', 77 | 'SIGNATURE_MODEL': 'django_anysign_demo.models.Signature', 78 | 'SIGNER_MODEL': 'django_anysign_demo.models.Signer', 79 | } 80 | # END settings.ANYSIGN. 81 | 82 | 83 | # Test/development settings. 84 | DEBUG = True 85 | TEMPLATE_DEBUG = DEBUG 86 | TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' 87 | NOSE_ARGS = [ 88 | '--all-modules', 89 | '--cover-package=django_anysign', 90 | '--cover-package=django_anysign_demo', 91 | '--cover-package=django_dummysign', 92 | '--exclude-dir=demo/django_anysign_demo/migrations', 93 | '--no-path-adjustment', 94 | '--nocapture', 95 | '--verbosity=2', 96 | '--with-coverage', 97 | '--with-doctest', 98 | ] 99 | SOUTH_TESTS_MIGRATE = False 100 | 101 | 102 | LOGGING = { 103 | 'version': 1, 104 | 'disable_existing_loggers': True, 105 | 'filters': { 106 | 'require_debug_false': { 107 | '()': 'django.utils.log.RequireDebugFalse', 108 | }, 109 | 'require_debug_true': { 110 | '()': 'django.utils.log.RequireDebugTrue', 111 | }, 112 | }, 113 | 'handlers': { 114 | 'console': { 115 | 'level': 'DEBUG', 116 | 'filters': ['require_debug_true'], 117 | 'class': 'logging.StreamHandler', 118 | }, 119 | 'null': { 120 | 'class': 'logging.NullHandler', 121 | }, 122 | 'mail_admins': { 123 | 'level': 'ERROR', 124 | 'filters': ['require_debug_false'], 125 | 'class': 'django.utils.log.AdminEmailHandler' 126 | } 127 | }, 128 | 'loggers': { 129 | 'django_anysign': { 130 | 'handlers': ['console'], 131 | 'level': 'DEBUG', 132 | }, 133 | 'django_anysign_demo': { 134 | 'handlers': ['console'], 135 | 'level': 'DEBUG', 136 | }, 137 | 'django_dummysign': { 138 | 'handlers': ['console'], 139 | 'level': 'DEBUG', 140 | }, 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /demo/django_anysign_demo/signature_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.views.generic import TemplateView 3 | 4 | from django_anysign_demo import views 5 | 6 | 7 | app_name = 'anysign' 8 | 9 | signer_view = views.SignerView.as_view() 10 | return_view = TemplateView.as_view(template_name='signer_return.html') 11 | callback_view = TemplateView.as_view(template_name='callback.html') 12 | 13 | urlpatterns = [ 14 | path('signer//', signer_view, name='signer'), 15 | path('signer//return/', return_view, name='signer_return'), 16 | path('callback//', callback_view, name='callback'), 17 | ] 18 | -------------------------------------------------------------------------------- /demo/django_anysign_demo/templates/callback.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | django-anysign demo 4 | 5 | 6 |

Callback success

7 |

8 | Your callback request was received. 9 |

10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/django_anysign_demo/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | django-anysign demo 4 | 5 | 6 |

Welcome to django-anysign demo!

7 |

Here are some demo links. Browse the code to see how they are implemented ;)

8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/django_anysign_demo/templates/send.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | django-anysign demo 4 | 5 | 6 |

Create and send a signature

7 |

8 | By clicking the button below, you request a Signature instance 9 | for a sample PDF file. 10 |

11 |

12 | Once the signature object is saved in database, you will be redirected 13 | to the "signer" URL. 14 |

15 |
16 | {% csrf_token %} 17 | {{ form }} 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /demo/django_anysign_demo/templates/signer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | django-anysign demo 4 | 5 | 6 |

Sign a document

7 |

8 | By clicking the button below, you will sign a sample PDF file. 9 |

10 |

11 | The signature backend will call "callback" URL. 12 |

13 |

14 | As a signer, you will be redirected to "signer_return" URL. 15 |

16 |
17 | {% csrf_token %} 18 | {{ form }} 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/django_anysign_demo/templates/signer_return.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | django-anysign demo 4 | 5 | 6 |

Signature complete

7 |

8 | Thank you! 9 |

10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/django_anysign_demo/tests.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | """Test suite for demoproject.download.""" 3 | from django.test import TestCase 4 | try: 5 | from django.urls import reverse 6 | except ImportError: 7 | from django.core.urlresolvers import reverse 8 | 9 | from django_anysign import api as django_anysign 10 | 11 | 12 | class HomeURLTestCase(TestCase): 13 | """Test homepage.""" 14 | def test_get(self): 15 | """Homepage returns HTTP 200.""" 16 | url = reverse('home') 17 | response = self.client.get(url) 18 | self.assertEqual(response.status_code, 200) 19 | 20 | 21 | class SendURLTestCase(TestCase): 22 | """Test "create and send signature" view.""" 23 | def test_get(self): 24 | """GET "send" URL returns HTTP 200.""" 25 | url = reverse('send') 26 | response = self.client.get(url) 27 | self.assertEqual(response.status_code, 200) 28 | 29 | def test_post(self): 30 | """POST "send" URL creates a signature and redirects to signer view.""" 31 | Signature = django_anysign.get_signature_model() 32 | self.assertEqual(Signature.objects.all().count(), 0) 33 | url = reverse('send') 34 | response = self.client.post(url) 35 | self.assertEqual(Signature.objects.all().count(), 1) 36 | signature = Signature.objects.get() 37 | signer = signature.signers.all()[0] 38 | signer_url = signature.signature_backend.get_signer_url(signer) 39 | self.assertRedirects(response, signer_url) 40 | 41 | 42 | class SignerURLTestCase(TestCase): 43 | """Test "create and send signature" view.""" 44 | def test_get(self): 45 | """GET "anysign:signer" URL returns HTTP 200.""" 46 | # Create a signature. 47 | SignatureType = django_anysign.get_signature_type_model() 48 | Signature = django_anysign.get_signature_model() 49 | Signer = django_anysign.get_signer_model() 50 | signature_type, created = SignatureType.objects.get_or_create( 51 | signature_backend_code='dummysign') 52 | signature = Signature.objects.create(signature_type=signature_type) 53 | signer = Signer.objects.create(signature=signature) 54 | signature.signers.add(signer) 55 | 56 | url = reverse('anysign:signer', args=[signer.pk]) 57 | response = self.client.get(url) 58 | self.assertEqual(response.status_code, 200) 59 | 60 | def test_post(self): 61 | """POST "anysign:signer" URL redirects to "signer return".""" 62 | # Create a signature. 63 | SignatureType = django_anysign.get_signature_type_model() 64 | Signature = django_anysign.get_signature_model() 65 | Signer = django_anysign.get_signer_model() 66 | signature_type, created = SignatureType.objects.get_or_create( 67 | signature_backend_code='dummysign') 68 | signature = Signature.objects.create(signature_type=signature_type) 69 | signer = Signer.objects.create(signature=signature) 70 | 71 | url = reverse('anysign:signer', args=[signer.pk]) 72 | response = self.client.post(url, follow=True) 73 | signer_return_url = signature.signature_backend.get_signer_return_url( 74 | signer) 75 | self.assertEqual( 76 | signer_return_url, 77 | reverse('anysign:signer_return', args=[signer.pk]) 78 | ) 79 | self.assertRedirects(response, signer_return_url) 80 | self.assertEqual(response.status_code, 200) 81 | -------------------------------------------------------------------------------- /demo/django_anysign_demo/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from django.views.generic import TemplateView 3 | 4 | from django_anysign_demo import views 5 | 6 | 7 | home_view = TemplateView.as_view(template_name='home.html') 8 | send_view = views.SendView.as_view() 9 | 10 | 11 | urlpatterns = [ 12 | path( 13 | 'signature/', 14 | include('django_anysign_demo.signature_urls', namespace='anysign') 15 | ), 16 | path('send/', send_view, name='send'), 17 | path('', home_view, name='home') 18 | ] 19 | -------------------------------------------------------------------------------- /demo/django_anysign_demo/views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django import forms 4 | from django.views.generic import FormView, UpdateView 5 | 6 | from django_anysign import api as django_anysign 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class SendView(FormView): 13 | form_class = forms.Form 14 | template_name = 'send.html' 15 | 16 | def form_valid(self, form): 17 | # Create signature instance in local DB. 18 | SignatureType = django_anysign.get_signature_type_model() 19 | Signature = django_anysign.get_signature_model() 20 | Signer = django_anysign.get_signer_model() 21 | signature_type, created = SignatureType.objects.get_or_create( 22 | signature_backend_code='dummysign') 23 | signature = Signature.objects.create(signature_type=signature_type) 24 | Signer.objects.create(signature=signature) 25 | logger.debug( 26 | '[django_anysign_demo] Signature ID={id} created in local DB' 27 | .format(id=signature.id)) 28 | # Register signature in backend's DB. 29 | signature.signature_backend.create_signature(signature) 30 | # Remember the signature object for later use (:meth:`get_success_url`) 31 | self.signature = signature 32 | # Handle the form as always. 33 | return FormView.form_valid(self, form) 34 | 35 | def get_success_url(self): 36 | backend = self.signature.signature_backend 37 | signer = self.signature.signers.all()[0] 38 | return backend.get_signer_url(signer) 39 | 40 | 41 | class SignerForm(forms.ModelForm): 42 | """A noop (but pass) model form.""" 43 | class Meta: 44 | model = django_anysign.get_signer_model() 45 | fields = [] 46 | 47 | def is_valid(self): 48 | return True 49 | 50 | def save(self, commit=True): 51 | return self.instance 52 | 53 | 54 | class SignerView(UpdateView): 55 | form_class = SignerForm 56 | template_name = 'signer.html' 57 | 58 | def get_queryset(self): 59 | Signer = django_anysign.get_signer_model() 60 | return Signer.objects.all() 61 | 62 | def get_success_url(self): 63 | backend = self.object.signature_backend 64 | return backend.get_signer_return_url(self.object) 65 | -------------------------------------------------------------------------------- /demo/django_anysign_demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """WSGI config for django-anysign demo project. 2 | 3 | This module contains the WSGI application used by Django's development server 4 | and any production WSGI deployments. It should expose a module-level variable 5 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 6 | this application via the ``WSGI_APPLICATION`` setting. 7 | 8 | Usually you will have the standard Django WSGI application here, but it also 9 | might make sense to replace the whole Django WSGI application with a custom one 10 | that later delegates to the Django one. For example, you could introduce WSGI 11 | middleware here, or combine a Django application with an application of another 12 | framework. 13 | 14 | """ 15 | from django.core.wsgi import get_wsgi_application 16 | import os 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "%s.settings" % __package__) 19 | 20 | # This application object is used by any WSGI server configured to use this 21 | # file. This includes Django's development server, if the WSGI_APPLICATION 22 | # setting points here. 23 | application = get_wsgi_application() 24 | 25 | # Apply WSGI middleware here. 26 | # from helloworld.wsgi import HelloWorldApplication 27 | # application = HelloWorldApplication(application) 28 | -------------------------------------------------------------------------------- /demo/requirements.pip: -------------------------------------------------------------------------------- 1 | django-nose 2 | -------------------------------------------------------------------------------- /demo/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Python packaging.""" 3 | import os 4 | 5 | from setuptools import setup 6 | 7 | 8 | here = os.path.abspath(os.path.dirname(__file__)) 9 | project_root = os.path.dirname(here) 10 | 11 | 12 | NAME = 'django-anysign-demo' 13 | DESCRIPTION = 'Demo for django-anysign.' 14 | README = open(os.path.join(here, 'README.rst')).read() 15 | VERSION = open(os.path.join(project_root, 'VERSION')).read().strip() 16 | AUTHOR = u'Benoît Bryon' 17 | EMAIL = u'novafloss@people-doc.com' 18 | URL = 'https://django-anysign.readthedocs.io/' 19 | CLASSIFIERS = ['Development Status :: 5 - Production/Stable', 20 | 'License :: OSI Approved :: BSD License', 21 | 'Programming Language :: Python :: 3', 22 | 'Programming Language :: Python :: 3.7', 23 | 'Programming Language :: Python :: 3.8', 24 | 'Framework :: Django'] 25 | KEYWORDS = [] 26 | PACKAGES = ['django_anysign_demo'] 27 | REQUIREMENTS = [ 28 | 'django-anysign', 29 | 'django-nose', 30 | 'setuptools', 31 | ] 32 | ENTRY_POINTS = { 33 | 'console_scripts': [ 34 | 'django-anysign-demo = django_anysign_demo.manage:main', 35 | ] 36 | } 37 | 38 | 39 | if __name__ == '__main__': # Don't run setup() when we import this module. 40 | setup(name=NAME, 41 | version=VERSION, 42 | description=DESCRIPTION, 43 | long_description=README, 44 | classifiers=CLASSIFIERS, 45 | keywords=' '.join(KEYWORDS), 46 | author=AUTHOR, 47 | author_email=EMAIL, 48 | url=URL, 49 | license='BSD', 50 | packages=PACKAGES, 51 | include_package_data=True, 52 | zip_safe=False, 53 | install_requires=REQUIREMENTS, 54 | entry_points=ENTRY_POINTS) 55 | -------------------------------------------------------------------------------- /django_anysign/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pkg_resources 3 | 4 | 5 | #: Module version, as defined in PEP-0396. 6 | __version__ = pkg_resources.get_distribution(__package__).version 7 | -------------------------------------------------------------------------------- /django_anysign/api.py: -------------------------------------------------------------------------------- 1 | """Declaration of API shortcuts. 2 | 3 | Everything declared (or imported) in this module is exposed in 4 | :mod:`django_anysign.api` package, i.e. available when one does 5 | ``from django_anysign import api as django_anysign``. 6 | 7 | Here are the motivations of such an "api" module: 8 | 9 | * as a `django-anysign` library user, in order to use `django-anysign`, I just 10 | do ``from django_anysign import api as django_anysign``. 11 | It is enough for most use cases. I do not need to bother with more 12 | `django_anysign` internals. I know this API will be maintained, documented, 13 | and not deprecated/refactored without notice. 14 | 15 | * as a `django-anysign` library developer, in order to maintain 16 | `django-anysign` API, I focus on things declared in 17 | :mod:`django_anysign.api`. It is enough. It is required. I take care of this 18 | API. If there is a change in this API between consecutive releases, then I 19 | use :class:`DeprecationWarning` and I mention it in release notes. 20 | 21 | It also means that things not exposed in :mod:`django_anysign.api` are not part 22 | of the deprecation policy. They can be moved, changed, removed without notice. 23 | 24 | """ 25 | from django_anysign.backend import SignatureBackend # NoQA 26 | from django_anysign.loading import get_signature_backend # NoQA 27 | from django_anysign.loading import get_signature_type_model # NoQA 28 | from django_anysign.loading import get_signature_model # NoQA 29 | from django_anysign.loading import get_signer_model # NoQA 30 | from django_anysign.models import SignatureType # NoQA 31 | from django_anysign.models import SignatureFactory # NoQA 32 | from django_anysign.models import SignerFactory # NoQA 33 | -------------------------------------------------------------------------------- /django_anysign/backend.py: -------------------------------------------------------------------------------- 1 | """Base material for signature backends.""" 2 | from django.urls import reverse 3 | 4 | 5 | class SignatureBackend(object): 6 | """Encapsulate signature workflow and integration with vendor backend. 7 | 8 | Here is a typical workflow: 9 | 10 | * :class:`~django_anysign.models.SignatureType` instance is created. It 11 | encapsulates the backend type and its configuration. 12 | 13 | * A :class:`~django_anysign.models.Signature` instance is created. 14 | The signature instance has a signature type attribute, hence a backend. 15 | 16 | * Signers are notified, by email, text or whatever. They get an hyperlink 17 | to the "signer view". The URL may vary depending on the signature 18 | backend. 19 | 20 | * A signer goes to the backend's "signer view" entry point: typically a 21 | view that integrates backend specific form to sign a document. 22 | 23 | * Most backends have a "notification view", for the third-party service to 24 | signal updates. 25 | 26 | * Most backends have a "signer return view", where the signer is redirected 27 | when he ends the signature process (whatever signature status). 28 | 29 | * The backend's specific workflow can be made of several views. At the 30 | beginning, there is a Signature instance which carries data (typically a 31 | document). At the end, Signature is done. 32 | 33 | """ 34 | def __init__(self, name, code, url_namespace='anysign', **kwargs): 35 | """Configure backend.""" 36 | #: Human-readable name. 37 | self.name = name 38 | 39 | #: Machine-readable name. Should be lowercase alphanumeric only, i.e. 40 | #: PEP-8 compliant. 41 | self.code = code 42 | 43 | #: Namespace for URL resolution. 44 | self.url_namespace = url_namespace 45 | 46 | def send_signature(self, signature): 47 | """Initiate the signature process. 48 | 49 | At this state, the signature object has been configured. 50 | 51 | Typical implementation consists in sending signer URL to first signer. 52 | 53 | Raise ``NotImplementedError`` if the backend does not support such a 54 | feature. 55 | 56 | """ 57 | raise NotImplementedError() 58 | 59 | def get_signer_url(self, signer): 60 | """Return URL where signer signs document. 61 | 62 | Raise ``NotImplementedError`` in case the backend does not support 63 | "signer view" feature. 64 | 65 | Default implementation reverses :meth:`get_signer_url_name` with 66 | ``signer.pk`` as argument. 67 | 68 | """ 69 | return reverse(self.get_signer_url_name(), args=[signer.pk]) 70 | 71 | def get_signer_url_name(self): 72 | """Return URL name where signer signs document. 73 | 74 | Raise ``NotImplementedError`` in case the backend does not support 75 | "signer view" feature. 76 | 77 | Default implementation returns ``anysign:signer``. 78 | 79 | """ 80 | return '{ns}:signer'.format(ns=self.url_namespace) 81 | 82 | def get_signer_return_url(self, signer): 83 | """Return absolute URL where signer is redirected after signing. 84 | 85 | The URL must be **absolute** because it is typically used by external 86 | signature service: the signer uses external web UI to sign the 87 | document(s) and then the signature service redirects the signer to 88 | (this) `Django` website. 89 | 90 | Raise ``NotImplementedError`` in case the backend does not support 91 | "signer return view" feature. 92 | 93 | Default implementation reverses :meth:`get_signer_return_url_name` 94 | with ``signer.pk`` as argument. 95 | 96 | """ 97 | return reverse( 98 | self.get_signer_return_url_name(), 99 | args=[signer.pk]) 100 | 101 | def get_signer_return_url_name(self): 102 | """Return URL name where signer is redirected once document has been 103 | signed. 104 | 105 | Raise ``NotImplementedError`` in case the backend does not support 106 | "signer return view" feature. 107 | 108 | Default implementation returns ``anysign:signer_return``. 109 | 110 | """ 111 | return '{ns}:signer_return'.format(ns=self.url_namespace) 112 | 113 | def get_signature_callback_url(self, signature): 114 | """Return URL where backend can post signature notifications. 115 | 116 | Raise ``NotImplementedError`` in case the backend does not support 117 | "signature callback url" feature. 118 | 119 | Default implementation reverses :meth:`get_signature_callback_url_name` 120 | with ``signature.pk`` as argument. 121 | 122 | """ 123 | return reverse( 124 | self.get_signature_callback_url_name(), 125 | args=[signature.pk]) 126 | 127 | def get_signature_callback_url_name(self): 128 | """Return URL name where backend can post signature notifications. 129 | 130 | Raise ``NotImplementedError`` in case the backend does not support 131 | "signer return view" feature. 132 | 133 | Default implementation returns ``anysign:signature_callback``. 134 | 135 | """ 136 | return '{ns}:signature_callback'.format(ns=self.url_namespace) 137 | 138 | def create_signature(self, signature): 139 | """Register ``signature`` in backend, return updated object. 140 | 141 | This method is typically called by views which create 142 | :class:`~django_anysign.models.Signature` instances. 143 | 144 | If backend stores a signature object, then implementation should update 145 | :attr:`~django_anysign.models.Signature.signature_backend_id`. 146 | 147 | Base implementation does nothing: override this method in backends. 148 | 149 | """ 150 | return signature 151 | -------------------------------------------------------------------------------- /django_anysign/loading.py: -------------------------------------------------------------------------------- 1 | """Utilities to load custom stuff for signatures.""" 2 | from django.conf import settings 3 | 4 | from django_anysign.utils.importlib import import_member 5 | 6 | 7 | def get_signature_backend(code, *args, **kwargs): 8 | """Instantiate instance for ``backend_code`` with ``args`` and ``kwargs``. 9 | 10 | Get the backend factory (class) using ``settings.ANYSIGN['BACKENDS']``. 11 | 12 | Positional and keyword arguments are proxied as is. 13 | 14 | """ 15 | factory_path = settings.ANYSIGN['BACKENDS'][code] 16 | factory = import_member(factory_path) 17 | return factory(*args, **kwargs) 18 | 19 | 20 | def get_model(setting): 21 | """Import and return the model class by ``settings.ANYSIGN[{setting}]``.""" 22 | model_path = settings.ANYSIGN[setting] 23 | model = import_member(model_path) 24 | return model 25 | 26 | 27 | def get_signature_type_model(): 28 | """Return model defined as ``settings.ANYSIGN['SIGNATURE_TYPE_MODEL']``.""" 29 | return get_model('SIGNATURE_TYPE_MODEL') 30 | 31 | 32 | def get_signature_model(): 33 | """Return model defined as ``settings.ANYSIGN['SIGNATURE_MODEL']``.""" 34 | return get_model('SIGNATURE_MODEL') 35 | 36 | 37 | def get_signer_model(): 38 | """Return model defined as ``settings.ANYSIGN['SIGNER_MODEL']``.""" 39 | return get_model('SIGNER_MODEL') 40 | -------------------------------------------------------------------------------- /django_anysign/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.db import models 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from django_anysign import settings 7 | from django_anysign.loading import get_signature_backend 8 | 9 | 10 | def signature_backend_choices(): 11 | """Return choices for available backends.""" 12 | return [(code, code) for code in settings.ANYSIGN['BACKENDS'].keys()] 13 | 14 | 15 | class SignatureType(models.Model): 16 | """Abstract base model for signature type. 17 | 18 | A signature type encapsulates backend setup. Typically: 19 | 20 | * a "configured backend" is a backend class (such as 21 | :class:`~django-dummysign.backend.DummySignBackend`) and related 22 | configuration (URL, credentials...). 23 | 24 | * a ``Signature`` instance will be related to a configured backend, via a 25 | ``SignatureType``. 26 | 27 | """ 28 | #: Machine-readable code for the backend. 29 | #: Typically related to settings, by default keys in 30 | #: ``settings.ANYSIGN['BACKENDS']`` dictionary. 31 | signature_backend_code = models.CharField( 32 | _('signature backend'), 33 | max_length=50, 34 | choices=signature_backend_choices(), 35 | db_index=True, 36 | ) 37 | 38 | class Meta: 39 | abstract = True 40 | 41 | @property 42 | def signature_backend_options(self): 43 | """Dictionary for backend's specific configuration. 44 | 45 | Default implementation returns empty dictionary. 46 | 47 | There are 2 main ways for you to setup backends with the right 48 | arguments: 49 | 50 | * in the model subclassing this one, override this property. This is 51 | the good option if you can have several ``SignatureType`` instances 52 | for one backend, i.e. if :attr:`signature_backend_code` is not 53 | unique. 54 | 55 | * in the backend's subclass, make ``__init__()`` read the Django 56 | settings or environment. This can be a good option if you have an 57 | unique ``SignatureBackend`` instance matching a backend 58 | (:attr:`signature_backend_code` is unique). 59 | 60 | """ 61 | return {} 62 | 63 | def get_signature_backend(self): 64 | """Instanciate and return signature backend instance. 65 | 66 | Default implementation uses 67 | :func:`~django-anysign.loading.get_backend_instance` with 68 | :attr:`signature_backend_code` as positional arguement and with 69 | :meth:`signature_backend_options` as keyword arguments. 70 | 71 | """ 72 | return get_signature_backend( 73 | self.signature_backend_code, 74 | **self.signature_backend_options) 75 | 76 | @property 77 | def signature_backend(self): 78 | """Return backend from internal cache or new instance. 79 | 80 | If :attr:`signature_backend_code` changed since the last access, then 81 | the internal (instance level) cache is invalidated and a new instance 82 | is returned. 83 | 84 | """ 85 | try: 86 | if self._signature_backend.code != self.signature_backend_code: 87 | raise AttributeError 88 | return self._signature_backend 89 | except AttributeError: 90 | self._signature_backend = self.get_signature_backend() 91 | return self._signature_backend 92 | 93 | 94 | def SignatureFactory(SignatureType, on_stype_delete=models.CASCADE): 95 | 96 | """Return base class for signature model, using ``SignatureType`` model. 97 | 98 | This pattern is the best one we found at the moment to have an abstract 99 | base model ``SignatureBase`` with appropriate foreign key to 100 | ``SignatureType`` model. Feel free to propose a better option if you know 101 | one ;) 102 | 103 | :param SignatureType: concrete SignatureType model 104 | :param on_stype_delete: on_delete SignatureType ForeignKey behaviour 105 | """ 106 | class Signature(models.Model): 107 | """Base model for signature models.""" 108 | #: Type of the signature, i.e. a backend and its configuration. 109 | signature_type = models.ForeignKey( 110 | SignatureType, 111 | verbose_name=_('signature type'), 112 | on_delete=on_stype_delete) 113 | 114 | #: Identifier in backend's external database. 115 | signature_backend_id = models.CharField( 116 | _('ID for signature backend'), 117 | max_length=100, 118 | db_index=True, 119 | blank=True, 120 | default=u'') 121 | 122 | #: Identifier in Django's internal database. 123 | anysign_internal_id = models.UUIDField( 124 | verbose_name=_('ID in internal database'), 125 | default=uuid.uuid4) 126 | 127 | class Meta: 128 | abstract = True 129 | 130 | @property 131 | def signature_backend(self): 132 | """Signature backend instance. 133 | 134 | This is just an utility shortcut, an alias to signature type's 135 | backend property. 136 | 137 | """ 138 | return self.signature_type.signature_backend 139 | 140 | def signature_documents(self): 141 | """Return list of documents (file wrappers) to sign. 142 | 143 | The following properties are expected for returned items: 144 | 145 | * ``name`` 146 | * ``bytes``: binary bytes. 147 | Typically ``lambda x: x.open('rb').read()`` 148 | 149 | Default implementation raises :class:`NotImplementedError`, i.e. 150 | your custom signature class must override this method. 151 | 152 | """ 153 | raise NotImplementedError 154 | return Signature 155 | 156 | 157 | def SignerFactory(Signature, on_signature_delete=models.CASCADE): 158 | """Return base class for signer model, using ``Signature`` model. 159 | 160 | This pattern is the best one we found at the moment to have an abstract 161 | base model ``Signer`` with appropriate foreign key to ``Signature`` 162 | model. Feel free to propose a better option if you know one ;) 163 | 164 | :param Signature: concrete Signature model 165 | :param on_signature_delete: on_delete Signature ForeignKey behaviour 166 | """ 167 | class Signer(models.Model): 168 | """Base class for signer. 169 | 170 | A signer is typically related to an user... but could be anything you 171 | want! By default, it is just related to a signature. 172 | 173 | """ 174 | #: Signature. 175 | signature = models.ForeignKey( 176 | Signature, 177 | related_name='signers', 178 | on_delete=on_signature_delete) 179 | 180 | #: Position as a signer. 181 | signing_order = models.PositiveSmallIntegerField( 182 | _('signing order'), 183 | default=0, 184 | help_text=_('Position in the list of signers.')) 185 | 186 | #: Identifier in backend's external database. 187 | signature_backend_id = models.CharField( 188 | _('ID in signature backend'), 189 | max_length=100, 190 | db_index=True, 191 | blank=True, 192 | default=u'') 193 | 194 | #: Identifier in Django's internal database. 195 | anysign_internal_id = models.UUIDField( 196 | verbose_name=_('ID in internal database'), 197 | default=uuid.uuid4) 198 | 199 | class Meta: 200 | abstract = True 201 | 202 | @property 203 | def signature_backend(self): 204 | """Signature backend instance. 205 | 206 | This is just an utility shortcut, an alias to signature type's 207 | backend property. 208 | 209 | """ 210 | return self.signature.signature_backend 211 | 212 | def get_absolute_url(self): 213 | return self.signature_backend.get_signer_url(self) 214 | return Signer 215 | -------------------------------------------------------------------------------- /django_anysign/settings.py: -------------------------------------------------------------------------------- 1 | """Specific settings for `django-anysign`.""" 2 | from django.conf import settings 3 | 4 | 5 | ANYSIGN = { 6 | 'BACKENDS': {}, 7 | 'SIGNATURE_TYPE_MODEL': None, 8 | 'SIGNATURE_MODEL': None, 9 | 'SIGNER_MODEL': None, 10 | } 11 | if not hasattr(settings, 'ANYSIGN'): 12 | setattr(settings, 'ANYSIGN', ANYSIGN) 13 | -------------------------------------------------------------------------------- /django_anysign/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Utilities that may be distributed as separate packages.""" 2 | -------------------------------------------------------------------------------- /django_anysign/utils/importlib.py: -------------------------------------------------------------------------------- 1 | """Import utilities.""" 2 | 3 | 4 | def import_member(import_string): 5 | """Import one member of Python module by path. 6 | 7 | >>> import os.path 8 | >>> imported = import_member('os.path.supports_unicode_filenames') 9 | >>> os.path.supports_unicode_filenames is imported 10 | True 11 | 12 | """ 13 | module_name, factory_name = str(import_string).rsplit('.', 1) 14 | module = __import__(module_name, globals(), locals(), [factory_name], 0) 15 | return getattr(module, factory_name) 16 | -------------------------------------------------------------------------------- /django_dummysign/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pkg_resources 3 | 4 | 5 | #: Module version, as defined in PEP-0396. 6 | __version__ = pkg_resources.get_distribution('django-anysign').version 7 | -------------------------------------------------------------------------------- /django_dummysign/backend.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django_anysign import api as django_anysign 4 | 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class DummySignBackend(django_anysign.SignatureBackend): 10 | def __init__(self): 11 | super(DummySignBackend, self).__init__( 12 | name='DummySign', 13 | code='dummysign', 14 | ) 15 | 16 | def create_signature(self, signature): 17 | """Register ``signature`` in backend, return updated object. 18 | 19 | As a dummy backend: just emit a log. 20 | 21 | """ 22 | signature = super(DummySignBackend, self).create_signature(signature) 23 | logger.debug('[django_dummysign] Signature created in backend') 24 | return signature 25 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = ../var/docs 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-anysign.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-anysign.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-anysign" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-anysign" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/about/alternatives.txt: -------------------------------------------------------------------------------- 1 | ################################# 2 | Alternatives and related projects 3 | ################################# 4 | 5 | This document presents other projects that provide similar or complementary 6 | functionalities. It focuses on differences or relationships with 7 | `django-anysign`. 8 | 9 | 10 | .. _django-dummysign-section: 11 | 12 | **************** 13 | django-dummysign 14 | **************** 15 | 16 | `django-dummysign`_ provides a dummy backend that implements `django-anysign` 17 | API. It is made for tests, prototypes or developments. 18 | 19 | .. note:: 20 | 21 | At the moment, `django-dummysign` is distributed as part of 22 | `django-anysign` itself. When you ``pip install django-anysign`` you get 23 | both ``django_anysign`` and ``django_dummysign`` packages. 24 | 25 | This happened because `django-anysign` and `django-dummysign` are developed 26 | together and tests from one require updates from the other, and vice-versa. 27 | They may be separated again later, as an example if `django-dummysign` gets 28 | additional requirements such as pyPdf you do not need in `django-anysign`. 29 | 30 | 31 | *************** 32 | django-docusign 33 | *************** 34 | 35 | `django-docusign`_ provides a backend for `DocuSign`_ signature service. It 36 | uses `django-anysign` to integrate `pydocusign`_ in `Django`. 37 | 38 | 39 | ***************** 40 | django-hello_sign 41 | ***************** 42 | 43 | `django-hello_sign`_ integrates `hellosign`_ in `Django`. 44 | It does not use `django-anysign` API. 45 | 46 | 47 | .. rubric:: References 48 | 49 | .. target-notes:: 50 | 51 | .. _`django-dummysign`: 52 | https://github.com/peopledoc/django-anysign/tree/master/django_dummysign/ 53 | .. _`django-docusign`: https://github.com/peopledoc/django-docusign/ 54 | .. _`DocuSign`: https://www.docusign.com/ 55 | .. _`pydocusign`: https://github.com/peopledoc/pydocusign/ 56 | .. _`django-hello_sign`: https://pypi.org/project/django-hello_sign/ 57 | .. _`hellosign`: https://www.hellosign.com/ 58 | -------------------------------------------------------------------------------- /docs/about/authors.txt: -------------------------------------------------------------------------------- 1 | .. include:: ../../AUTHORS -------------------------------------------------------------------------------- /docs/about/changelog.txt: -------------------------------------------------------------------------------- 1 | .. include:: ../../CHANGELOG -------------------------------------------------------------------------------- /docs/about/index.txt: -------------------------------------------------------------------------------- 1 | #################### 2 | About django-anysign 3 | #################### 4 | 5 | This section is about the `django-anysign` project itself. 6 | 7 | .. toctree:: 8 | 9 | vision 10 | alternatives 11 | license 12 | authors 13 | changelog -------------------------------------------------------------------------------- /docs/about/license.txt: -------------------------------------------------------------------------------- 1 | .. include:: ../../LICENSE -------------------------------------------------------------------------------- /docs/about/vision.txt: -------------------------------------------------------------------------------- 1 | ###### 2 | Vision 3 | ###### 4 | 5 | `django-anysign` provides conventions and base resources to implement digital 6 | signature features within a Django project. 7 | 8 | `django-anysign`'s goal is to provide a consistent API whatever the signature 9 | implementation. This concept basically covers the following use cases: 10 | 11 | * plug several signature backends and their specific workflows into a single 12 | website. 13 | 14 | * in a website using a single signature backend, migrate from one backend to 15 | another with minimum efforts. 16 | 17 | * as a developer, implement bindings for a new signature service vendor. 18 | 19 | `django-anysign` presumes the following items are generally involved in digital 20 | signature features: 21 | 22 | * models. Such as signature, signer and signature type (backend options). 23 | 24 | * workflows. They usually start with the creation of a document to sign (setup 25 | a signature, assign signers, choose a backend). They usually end when the 26 | document has been signed by all signers. Steps between "start" and "end" 27 | typically vary depending on the vendor signature service. 28 | 29 | * views. Most signature workflows use similar views, such as 30 | "create signature", "sign document", "signer processed document" or 31 | "API callback". Of course, the implementation and order vary depending on the 32 | vendor signature service. But some bits are generic. 33 | 34 | `django-anysign` does not include vendor-specific implementation. Third-party 35 | projects do. And they can be based on `django-anysign`. So as a developer, you 36 | are likely to discover `django-anysign` via these vendor-specific projects. See 37 | :doc:`/about/alternatives` for details about third-party projects. 38 | 39 | `django-anysign` is a framework. It does not provide all-in-one solutions. You 40 | may have to implement some things in your Django project. `django-anysign` 41 | tries to make this custom code easier to imagine and write, using conventions, 42 | utilities and base classes. 43 | -------------------------------------------------------------------------------- /docs/backends.txt: -------------------------------------------------------------------------------- 1 | ######## 2 | Backends 3 | ######## 4 | 5 | `django-anysign`'s signature backend encapsulates signature workflow and 6 | integration with vendor specific implementation. 7 | 8 | .. note:: 9 | 10 | The backend API is quite experimental. This document deals with both vision 11 | (concepts) and current implementation (which may improve). 12 | 13 | 14 | ****************** 15 | Scope of a backend 16 | ****************** 17 | 18 | A signature backend is typically known by models and views. They use the 19 | backend to perform vendor-specific operations. The backend contains 20 | vendor-specific implementation that has to be shared with several consumers 21 | such as models and views. 22 | 23 | A signature backend also typically knowns the workflows. So it should be 24 | helpful for URL resolution. 25 | 26 | 27 | ********************************* 28 | django-anysign's SignatureBackend 29 | ********************************* 30 | 31 | Here is the current implementation of base backend. 32 | 33 | .. autoclass:: django_anysign.backend.SignatureBackend 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | :member-order: bysource 38 | 39 | *********************************** 40 | django-dummysign's SignatureBackend 41 | *********************************** 42 | 43 | Here is the demo signature backend implementation provided by 44 | :ref:`django-dummysign-section`. 45 | 46 | .. literalinclude:: /../django_dummysign/backend.py 47 | :language: python 48 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """django-anysign documentation build configuration file.""" 3 | import os 4 | import re 5 | import sys 6 | 7 | 8 | # Minimal Django settings. Required to use sphinx.ext.autodoc, because 9 | # django-anysign depends on Django... 10 | sys.path.append(os.path.join(os.path.abspath('.'), '..', 'demo')) 11 | os.environ['DJANGO_SETTINGS_MODULE'] = 'django_anysign_demo.settings' 12 | import django 13 | django.setup() 14 | 15 | 16 | # -- General configuration ---------------------------------------------------- 17 | 18 | # Extensions. 19 | extensions = [ 20 | 'sphinx.ext.autodoc', 21 | 'sphinx.ext.autosummary', 22 | 'sphinx.ext.doctest', 23 | 'sphinx.ext.coverage', 24 | 'sphinx.ext.intersphinx', 25 | ] 26 | 27 | # Add any paths that contain templates here, relative to this directory. 28 | templates_path = ['_templates'] 29 | 30 | # The suffix of source filenames. 31 | source_suffix = '.txt' 32 | 33 | # The encoding of source files. 34 | source_encoding = 'utf-8' 35 | 36 | # The master toctree document. 37 | master_doc = 'index' 38 | 39 | # General information about the project. 40 | project = u'django-anysign' 41 | project_slug = re.sub(r'([\w_.-]+)', u'-', project) 42 | copyright = u'2014-2016, PeopleDoc' 43 | author = u'Benoît Bryon' 44 | author_slug = re.sub(r'([\w_.-]+)', u'-', author) 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | configuration_dir = os.path.dirname(__file__) 50 | documentation_dir = configuration_dir 51 | version_file = os.path.normpath(os.path.join( 52 | documentation_dir, 53 | '../VERSION')) 54 | 55 | # The full version, including alpha/beta/rc tags. 56 | release = open(version_file).read().strip() 57 | # The short X.Y version. 58 | version = '.'.join(release.split('.')[0:1]) 59 | 60 | # The language for content autogenerated by Sphinx. Refer to documentation 61 | # for a list of supported languages. 62 | language = 'en' 63 | 64 | # List of patterns, relative to source directory, that match files and 65 | # directories to ignore when looking for source files. 66 | exclude_patterns = ['_build'] 67 | 68 | # The name of the Pygments (syntax highlighting) style to use. 69 | pygments_style = 'sphinx' 70 | 71 | # Ignore some warnings 72 | suppress_warnings = ['image.nonlocal_uri'] 73 | 74 | 75 | # -- Options for HTML output -------------------------------------------------- 76 | 77 | # The theme to use for HTML and HTML Help pages. See the documentation for 78 | # a list of builtin themes. 79 | html_theme = 'alabaster' 80 | 81 | # Add any paths that contain custom static files (such as style sheets) here, 82 | # relative to this directory. They are copied after the builtin static files, 83 | # so a file named "default.css" will overwrite the builtin "default.css". 84 | html_static_path = [] 85 | 86 | # Custom sidebar templates, maps document names to template names. 87 | html_sidebars = { 88 | '**': ['globaltoc.html', 89 | 'relations.html', 90 | 'sourcelink.html', 91 | 'searchbox.html'], 92 | } 93 | 94 | # Output file base name for HTML help builder. 95 | htmlhelp_basename = u'{project}doc'.format(project=project_slug) 96 | 97 | 98 | # -- Options for sphinx.ext.intersphinx --------------------------------------- 99 | 100 | intersphinx_mapping = { 101 | 'python': ('https://docs.python.org/2.7', None), 102 | } 103 | 104 | 105 | # -- Options for LaTeX output ------------------------------------------------- 106 | 107 | latex_elements = {} 108 | 109 | # Grouping the document tree into LaTeX files. List of tuples 110 | # (source start file, target name, title, author, documentclass 111 | # [howto/manual]). 112 | latex_documents = [ 113 | ('index', 114 | u'{project}.tex'.format(project=project_slug), 115 | u'{project} Documentation'.format(project=project), 116 | author, 117 | 'manual'), 118 | ] 119 | 120 | 121 | # -- Options for manual page output ------------------------------------------- 122 | 123 | # One entry per manual page. List of tuples 124 | # (source start file, name, description, authors, manual section). 125 | man_pages = [ 126 | ('index', 127 | project, 128 | u'{project} Documentation'.format(project=project), 129 | [author], 130 | 1) 131 | ] 132 | 133 | 134 | # -- Options for Texinfo output ----------------------------------------------- 135 | 136 | # Grouping the document tree into Texinfo files. List of tuples 137 | # (source start file, target name, title, author, 138 | # dir menu entry, description, category) 139 | texinfo_documents = [ 140 | ('index', 141 | project_slug, 142 | u'{project} Documentation'.format(project=project), 143 | author, 144 | project, 145 | 'One line description of project.', 146 | 'Miscellaneous'), 147 | ] 148 | -------------------------------------------------------------------------------- /docs/contributing.txt: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/demo.txt: -------------------------------------------------------------------------------- 1 | .. include:: ../demo/README.rst 2 | -------------------------------------------------------------------------------- /docs/index.txt: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | 4 | ******** 5 | Contents 6 | ******** 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | :titlesonly: 11 | 12 | install 13 | settings 14 | models 15 | backends 16 | views 17 | loading 18 | demo 19 | about/index 20 | contributing.txt 21 | 22 | 23 | ****************** 24 | Indices and tables 25 | ****************** 26 | 27 | * :ref:`genindex` 28 | * :ref:`modindex` 29 | * :ref:`search` 30 | -------------------------------------------------------------------------------- /docs/install.txt: -------------------------------------------------------------------------------- 1 | .. include:: ../INSTALL 2 | -------------------------------------------------------------------------------- /docs/loading.txt: -------------------------------------------------------------------------------- 1 | ####### 2 | Loading 3 | ####### 4 | 5 | Since `django-anysign` does not provide concrete :doc:`/models`, and models 6 | are configured in :doc:`settings `, here are tools to load models 7 | and backends. 8 | 9 | 10 | ********************* 11 | get_signature_backend 12 | ********************* 13 | 14 | .. autofunction:: django_anysign.loading.get_signature_backend 15 | 16 | 17 | ************************ 18 | get_signature_type_model 19 | ************************ 20 | 21 | .. autofunction:: django_anysign.loading.get_signature_type_model 22 | 23 | 24 | ******************* 25 | get_signature_model 26 | ******************* 27 | 28 | .. autofunction:: django_anysign.loading.get_signature_model 29 | 30 | 31 | **************** 32 | get_signer_model 33 | **************** 34 | 35 | .. autofunction:: django_anysign.loading.get_signer_model 36 | -------------------------------------------------------------------------------- /docs/models.txt: -------------------------------------------------------------------------------- 1 | ###### 2 | Models 3 | ###### 4 | 5 | `django-anysign` presumes digital signature involves models in the Django 6 | project: one to store the signatures, another to store signers, and one to 7 | store backend specific options. 8 | 9 | That said, `django-anysign` does not embed concrete models: it provides base 10 | models you have to extend in your applications. This design allows you to 11 | customize models the way you like, i.e. depending on your use case. 12 | 13 | 14 | ******************* 15 | Minimal integration 16 | ******************* 17 | 18 | Here is the minimal integration you need in some :file:`models.py`: 19 | 20 | .. literalinclude:: /../demo/django_anysign_demo/models.py 21 | :language: python 22 | 23 | The example above is taken from `django-anysign`'s :doc:`/demo`. 24 | 25 | 26 | .. _SignatureType-section: 27 | 28 | ************* 29 | SignatureType 30 | ************* 31 | 32 | .. autoclass:: django_anysign.models.SignatureType 33 | :members: 34 | :undoc-members: 35 | :show-inheritance: 36 | :member-order: bysource 37 | 38 | 39 | .. _Signature-section: 40 | 41 | ********* 42 | Signature 43 | ********* 44 | 45 | .. autofunction:: django_anysign.models.SignatureFactory 46 | 47 | Here is what you get in the :doc:`/demo`: 48 | 49 | .. autoclass:: django_anysign_demo.models.Signature 50 | :members: 51 | :undoc-members: 52 | :show-inheritance: 53 | :member-order: bysource 54 | 55 | 56 | .. _Signer-section: 57 | 58 | ****** 59 | Signer 60 | ****** 61 | 62 | .. autofunction:: django_anysign.models.SignerFactory 63 | 64 | Here is what you get in the :doc:`/demo`: 65 | 66 | .. autoclass:: django_anysign_demo.models.Signer 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | :member-order: bysource 71 | -------------------------------------------------------------------------------- /docs/settings.txt: -------------------------------------------------------------------------------- 1 | ######### 2 | Configure 3 | ######### 4 | 5 | Here is the list of settings used by `django-anysign`. 6 | 7 | 8 | ************** 9 | INSTALLED_APPS 10 | ************** 11 | 12 | There is no need to register `django-anysign` application in your Django's 13 | ``INSTALLED_APPS`` setting. 14 | 15 | 16 | ******* 17 | ANYSIGN 18 | ******* 19 | 20 | The ``settings.ANYSIGN`` is a dictionary that contains all specific 21 | configuration for `django-anysign`. 22 | 23 | Example from the :doc:`/demo`: 24 | 25 | .. literalinclude:: /../demo/django_anysign_demo/settings.py 26 | :language: python 27 | :start-after: BEGIN settings.ANYSIGN 28 | :end-before: END settings.ANYSIGN 29 | 30 | BACKENDS 31 | ======== 32 | 33 | A dictionary where: 34 | 35 | * keys are backend codes, i.e. machine-readable names for backends. These keys 36 | are typically stored in the database as 37 | :attr:`django_anysign.models.SignatureType.signature_backend_code`. 38 | 39 | * values are Python path to import backend's implementation, typically a class. 40 | 41 | See also :func:`~django_anysign.loading.get_signature_backend`. 42 | 43 | 44 | SIGNATURE_TYPE_MODEL 45 | ==================== 46 | 47 | The Python path to import the :ref:`SignatureType-section` model. 48 | 49 | 50 | SIGNATURE_MODEL 51 | =============== 52 | 53 | The Python path to import the :ref:`Signature-section` model. 54 | 55 | 56 | SIGNER_MODEL 57 | ============ 58 | 59 | The Python path to import the :ref:`Signer-section` model. 60 | -------------------------------------------------------------------------------- /docs/views.txt: -------------------------------------------------------------------------------- 1 | ##### 2 | Views 3 | ##### 4 | 5 | At the moment, `django-anysign` does not provide views or generic views. But 6 | this feature is part of the :doc:`/about/vision`... 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Python packaging.""" 4 | import os 5 | import sys 6 | 7 | from setuptools import setup 8 | from setuptools.command.test import test as TestCommand 9 | 10 | 11 | class Tox(TestCommand): 12 | """Test command that runs tox.""" 13 | def finalize_options(self): 14 | TestCommand.finalize_options(self) 15 | self.test_args = [] 16 | self.test_suite = True 17 | 18 | def run_tests(self): 19 | import tox # import here, cause outside the eggs aren't loaded. 20 | errno = tox.cmdline(self.test_args) 21 | sys.exit(errno) 22 | 23 | 24 | #: Absolute path to directory containing setup.py file. 25 | here = os.path.abspath(os.path.dirname(__file__)) 26 | #: Boolean, ``True`` if environment is running Python version 2. 27 | IS_PYTHON2 = sys.version_info[0] == 2 28 | 29 | 30 | NAME = 'django-anysign' 31 | DESCRIPTION = 'Online signature for Django: multiple services, generic API.' 32 | README = open(os.path.join(here, 'README.rst')).read() 33 | VERSION = open(os.path.join(here, 'VERSION')).read().strip() 34 | AUTHOR = u'Benoît Bryon' 35 | EMAIL = u'novafloss@people-doc.com' 36 | LICENSE = 'BSD' 37 | URL = 'https://{name}.readthedocs.io/'.format(name=NAME) 38 | CLASSIFIERS = [ 39 | 'Development Status :: 5 - Production/Stable', 40 | 'License :: OSI Approved :: BSD License', 41 | 'Programming Language :: Python :: 3', 42 | 'Programming Language :: Python :: 3.7', 43 | 'Programming Language :: Python :: 3.8', 44 | 'Framework :: Django', 45 | 'Framework :: Django :: 2.2', 46 | 'Framework :: Django :: 3.2', 47 | ] 48 | KEYWORDS = [ 49 | 'signature', 50 | 'sign', 51 | 'signer', 52 | 'generic', 53 | ] 54 | PACKAGES = [NAME.replace('-', '_'), 'django_dummysign'] 55 | REQUIREMENTS = [ 56 | # BEGIN requirements 57 | 'Django>=2.2.27,<3.3', 58 | 'setuptools', 59 | # END requirements 60 | ] 61 | ENTRY_POINTS = {} 62 | SETUP_REQUIREMENTS = ['setuptools'] 63 | TEST_REQUIREMENTS = ['tox'] 64 | CMDCLASS = {'test': Tox} 65 | EXTRA_REQUIREMENTS = { 66 | 'test': TEST_REQUIREMENTS, 67 | } 68 | 69 | 70 | if __name__ == '__main__': # Don't run setup() when we import this module. 71 | setup( 72 | name=NAME, 73 | version=VERSION, 74 | description=DESCRIPTION, 75 | long_description=README, 76 | classifiers=CLASSIFIERS, 77 | keywords=' '.join(KEYWORDS), 78 | author=AUTHOR, 79 | author_email=EMAIL, 80 | url=URL, 81 | license=LICENSE, 82 | packages=PACKAGES, 83 | include_package_data=True, 84 | zip_safe=False, 85 | install_requires=REQUIREMENTS, 86 | entry_points=ENTRY_POINTS, 87 | tests_require=TEST_REQUIREMENTS, 88 | cmdclass=CMDCLASS, 89 | setup_requires=SETUP_REQUIREMENTS, 90 | extras_require=EXTRA_REQUIREMENTS, 91 | ) 92 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{37,38}-django{22,32} 4 | flake8 5 | sphinx 6 | readme 7 | 8 | [testenv] 9 | basepython = 10 | py37: python3.7 11 | py38: python3.8 12 | deps = 13 | coverage 14 | django22: Django~=2.2.27 15 | django32: Django~=3.2 16 | nose 17 | nose-exclude 18 | py27: mock 19 | commands = 20 | pip install -e . 21 | pip install -e demo 22 | python -Wd {envbindir}/django-anysign-demo test {posargs: django_anysign django_dummysign django_anysign_demo} 23 | coverage erase 24 | pip freeze 25 | 26 | [testenv:flake8] 27 | basepython = python3.8 28 | deps = 29 | flake8 30 | commands = 31 | flake8 django_anysign 32 | flake8 django_dummysign 33 | flake8 --exclude=*migrations demo 34 | 35 | [testenv:sphinx] 36 | basepython = python3.8 37 | deps = 38 | Sphinx 39 | commands = 40 | pip install -e . 41 | pip install -r demo/requirements.pip 42 | make --directory=docs SPHINXOPTS='-W' clean {posargs:html doctest linkcheck} 43 | whitelist_externals = 44 | make 45 | 46 | [testenv:readme] 47 | basepython = python3.8 48 | deps = 49 | docutils 50 | pygments 51 | commands = 52 | mkdir -p var/docs 53 | rst2html.py --exit-status=2 README.rst var/docs/README.html 54 | rst2html.py --exit-status=2 CONTRIBUTING.rst var/docs/CONTRIBUTING.html 55 | whitelist_externals = 56 | mkdir 57 | 58 | [testenv:release] 59 | deps = 60 | wheel 61 | zest.releaser 62 | commands = 63 | fullrelease 64 | --------------------------------------------------------------------------------