├── transaction_hooks ├── backends │ ├── __init__.py │ ├── mysql │ │ ├── __init__.py │ │ └── base.py │ ├── postgis │ │ ├── __init__.py │ │ └── base.py │ ├── sqlite3 │ │ ├── __init__.py │ │ └── base.py │ └── postgresql_psycopg2 │ │ ├── __init__.py │ │ └── base.py ├── test │ ├── __init__.py │ ├── settings.py │ ├── models.py │ ├── settings_sqlite3.py │ ├── settings_mysql.py │ ├── settings_postgis.py │ ├── settings_pg.py │ └── test_basic.py ├── __init__.py └── mixin.py ├── Makefile ├── .coveragerc ├── .gitignore ├── MANIFEST.in ├── README.rst ├── runtests.py ├── CONTRIBUTING.rst ├── requirements.txt ├── AUTHORS.rst ├── tox.ini ├── CHANGES.rst ├── setup.py ├── LICENSE.txt ├── .travis.yml └── doc ├── make.bat ├── Makefile ├── conf.py └── index.rst /transaction_hooks/backends/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /transaction_hooks/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /transaction_hooks/backends/mysql/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /transaction_hooks/backends/postgis/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /transaction_hooks/backends/sqlite3/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /transaction_hooks/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.3' 2 | -------------------------------------------------------------------------------- /transaction_hooks/backends/postgresql_psycopg2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | coverage erase 3 | tox 4 | coverage html 5 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = transaction_hooks 3 | omit = transaction_hooks/test/* 4 | branch = 1 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | django_transaction_hooks.egg-info/ 2 | htmlcov/ 3 | .tox/ 4 | doc/_build/ 5 | dist/ 6 | .coverage 7 | -------------------------------------------------------------------------------- /transaction_hooks/test/settings.py: -------------------------------------------------------------------------------- 1 | SECRET_KEY = 'required' 2 | 3 | INSTALLED_APPS = ['transaction_hooks.test'] 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CHANGES.rst 3 | include LICENSE.txt 4 | include MANIFEST.in 5 | include README.rst 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============================ 2 | django-transaction-hooks 3 | ============================ 4 | 5 | This project has been merged into Django and is now core functionality in 6 | all supported versions of Django. Use the version built-in to Django; this 7 | standalone app is unmaintained. 8 | -------------------------------------------------------------------------------- /transaction_hooks/backends/mysql/base.py: -------------------------------------------------------------------------------- 1 | from django.db.backends.mysql import base 2 | 3 | from transaction_hooks.mixin import TransactionHooksDatabaseWrapperMixin 4 | 5 | 6 | class DatabaseWrapper(TransactionHooksDatabaseWrapperMixin, 7 | base.DatabaseWrapper): 8 | pass 9 | -------------------------------------------------------------------------------- /transaction_hooks/backends/sqlite3/base.py: -------------------------------------------------------------------------------- 1 | from django.db.backends.sqlite3 import base 2 | 3 | from transaction_hooks.mixin import TransactionHooksDatabaseWrapperMixin 4 | 5 | 6 | class DatabaseWrapper(TransactionHooksDatabaseWrapperMixin, 7 | base.DatabaseWrapper): 8 | pass 9 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # This script exists so this dir is on sys.path when running pytest in tox. 3 | import pytest 4 | import os 5 | import sys 6 | 7 | os.environ.setdefault( 8 | 'DJANGO_SETTINGS_MODULE', 'transaction_hooks.test.settings_sqlite3') 9 | 10 | sys.exit(pytest.main()) 11 | -------------------------------------------------------------------------------- /transaction_hooks/backends/postgis/base.py: -------------------------------------------------------------------------------- 1 | from django.contrib.gis.db.backends.postgis import base 2 | 3 | from transaction_hooks.mixin import TransactionHooksDatabaseWrapperMixin 4 | 5 | 6 | class DatabaseWrapper(TransactionHooksDatabaseWrapperMixin, 7 | base.DatabaseWrapper): 8 | pass 9 | -------------------------------------------------------------------------------- /transaction_hooks/test/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.encoding import python_2_unicode_compatible 3 | 4 | 5 | @python_2_unicode_compatible 6 | class Thing(models.Model): 7 | num = models.IntegerField() 8 | 9 | def __str__(self): 10 | return "Thing %d" % self.num 11 | -------------------------------------------------------------------------------- /transaction_hooks/backends/postgresql_psycopg2/base.py: -------------------------------------------------------------------------------- 1 | from django.db.backends.postgresql_psycopg2 import base 2 | 3 | from transaction_hooks.mixin import TransactionHooksDatabaseWrapperMixin 4 | 5 | 6 | class DatabaseWrapper(TransactionHooksDatabaseWrapperMixin, 7 | base.DatabaseWrapper): 8 | pass 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Thanks for your interest in contributing! 5 | 6 | Django-transaction-hooks has been merged into Django 1.9 and is core 7 | functionality in all supported Django versions. Thus, this package is 8 | unmaintained, should not be used, and will receive no updates. 9 | 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Requirements for running django-transaction-hooks tests 2 | 3 | Django>=1.6.1 4 | 5 | tox>=1.8.0 6 | virtualenv>=1.11 7 | pytest-django>=2.6.0 8 | pytest>=2.5.1 9 | py>=1.4.19 10 | coverage>=3.7.1 11 | psycopg2>=2.5.2 12 | MySQL-Python>=1.2.5 13 | 14 | Sphinx>=1.2 15 | Jinja2>=2.7.2 16 | Pygments>=1.6 17 | docutils>=0.11 18 | MarkupSafe>=0.18 19 | -------------------------------------------------------------------------------- /transaction_hooks/test/settings_sqlite3.py: -------------------------------------------------------------------------------- 1 | from .settings import * # noqa 2 | 3 | import os 4 | 5 | DATABASES = { 6 | 'default': { 7 | 'ENGINE': 'transaction_hooks.backends.sqlite3', 8 | 'TEST_NAME': os.path.join( 9 | os.path.dirname(os.path.abspath('__file__')), 10 | 'testdb.sqlite', 11 | ) 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Carl Meyer 2 | Aymeric Augustin 3 | Ian Lee 4 | Anssi Kääriäinen 5 | Andreas Pelme 6 | Marek Malek 7 | Niels Sandholt Busch 8 | Bertrand Bordage 9 | -------------------------------------------------------------------------------- /transaction_hooks/test/settings_mysql.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from .settings import * # noqa 4 | 5 | DATABASES = { 6 | 'default': { 7 | 'ENGINE': 'transaction_hooks.backends.mysql', 8 | 'NAME': 'dtc', 9 | }, 10 | } 11 | 12 | if 'DTC_MYSQL_USERNAME' in os.environ: 13 | DATABASES['default'].update( 14 | { 15 | 'USER': os.environ['DTC_MYSQL_USERNAME'], 16 | 'PASSWORD': '', 17 | 'HOST': 'localhost', 18 | } 19 | ) 20 | -------------------------------------------------------------------------------- /transaction_hooks/test/settings_postgis.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | try: 4 | from psycopg2cffi import compat 5 | compat.register() 6 | except ImportError: 7 | pass 8 | 9 | from .settings import * # noqa 10 | 11 | DATABASES = { 12 | 'default': { 13 | 'ENGINE': 'transaction_hooks.backends.postgis', 14 | 'NAME': 'dtc', 15 | }, 16 | } 17 | 18 | 19 | if 'DTC_PG_USERNAME' in os.environ: 20 | DATABASES['default'].update( 21 | { 22 | 'USER': os.environ['DTC_PG_USERNAME'], 23 | 'PASSWORD': '', 24 | 'HOST': 'localhost', 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /transaction_hooks/test/settings_pg.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | try: 4 | from psycopg2cffi import compat 5 | compat.register() 6 | except ImportError: 7 | pass 8 | 9 | from .settings import * # noqa 10 | 11 | DATABASES = { 12 | 'default': { 13 | 'ENGINE': 'transaction_hooks.backends.postgresql_psycopg2', 14 | 'NAME': 'dtc', 15 | }, 16 | } 17 | 18 | 19 | if 'DTC_PG_USERNAME' in os.environ: 20 | DATABASES['default'].update( 21 | { 22 | 'USER': os.environ['DTC_PG_USERNAME'], 23 | 'PASSWORD': '', 24 | 'HOST': 'localhost', 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{27,33,34,py}-django17-{sqlite3,pg,postgis,mysql}, 4 | py{27,33,34,35,py}-django18-{sqlite3,pg,postgis,mysql}, 5 | 6 | [testenv] 7 | deps = 8 | pytest-django==3.1.2 9 | pytest==3.0.5 10 | py==1.4.31 11 | coverage==4.2 12 | django17: Django>=1.7,<1.8 13 | django18: Django>=1.8,<1.9 14 | {pg,postgis}-py{26,27,33,34,35}: psycopg2==2.6 15 | {pg,postgis}-pypy: psycopg2cffi==2.6.1 16 | mysql-py{26,27,py}: MySQL-python==1.2.5 17 | mysql-py{33,34,35}: https://github.com/clelland/MySQL-for-Python-3/tarball/master 18 | setenv = 19 | sqlite3: DJANGO_SETTINGS_MODULE = transaction_hooks.test.settings_sqlite3 20 | pg: DJANGO_SETTINGS_MODULE = transaction_hooks.test.settings_pg 21 | postgis: DJANGO_SETTINGS_MODULE = transaction_hooks.test.settings_postgis 22 | mysql: DJANGO_SETTINGS_MODULE = transaction_hooks.test.settings_mysql 23 | commands = 24 | coverage run -a runtests.py transaction_hooks/test --tb short 25 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | CHANGES 2 | ======= 3 | 4 | 0.3 (2020.03.15) 5 | ---------------- 6 | 7 | * Formally deprecate and archive the project; it is included as part of all 8 | Django versions since 1.9. Use the version included in Django and report any 9 | bugs or issues to Django; this standalone app is not maintained. 10 | 11 | * Drop support for Python 2.6, Python 3.2, and Django 1.6. 12 | 13 | 14 | 0.2 (2014.11.29) 15 | ---------------- 16 | 17 | * Add built-in PostGIS backend. Merge of GH-16, fixes GH-14. Thanks Bertrand 18 | Bordage. 19 | 20 | 21 | 0.1.4 (2014.05.27) 22 | ------------------ 23 | 24 | * Make it possible to execute a transaction within an on-commit hook (except on 25 | SQLite). Merge of GH-9, fixes GH-8. Thanks Marek Malek. 26 | 27 | 28 | 0.1.3 (2014.01.24) 29 | ------------------- 30 | 31 | * Fix failure when mixing-in with database backends that perform queries in 32 | ``__init__`` (e.g. PostGIS backend when ``POSTGIS_VERSION`` setting is not 33 | set). Merge of GH-6, fixes GH-5. Thanks Niels Sandholt Busch. 34 | 35 | 36 | 0.1.2 (2014.01.21) 37 | ------------------ 38 | 39 | * Fix bug where running queries in an ``on_commit`` hook under Postgres caused 40 | an "autocommit cannot be used inside a transaction" error. (GH-4). 41 | 42 | 43 | 0.1.1 (2014.01.18) 44 | ------------------ 45 | 46 | * Clear run-on-commit hooks even if one raises an exception. Thanks akaariai. 47 | 48 | 49 | 0.1 (2014.01.18) 50 | ---------------- 51 | 52 | * Initial working version; support for SQLite3, PostgreSQL, MySQL. 53 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os.path import join 2 | from setuptools import setup, find_packages 3 | 4 | 5 | long_description = ( 6 | open('README.rst').read() + open('CHANGES.rst').read()) 7 | 8 | 9 | def get_version(): 10 | with open(join('transaction_hooks', '__init__.py')) as f: 11 | for line in f: 12 | if line.startswith('__version__ ='): 13 | return line.split('=')[1].strip().strip('"\'') 14 | 15 | 16 | setup( 17 | name='django-transaction-hooks', 18 | version=get_version(), 19 | description=( 20 | 'Django database backends that permit registering ' 21 | 'post-transaction-commit hooks' 22 | ), 23 | long_description=long_description, 24 | author='Carl Meyer', 25 | author_email='carl@oddbird.net', 26 | url='https://github.com/carljm/django-transaction-hooks/', 27 | packages=find_packages(), 28 | install_requires=['django>=1.6.1'], 29 | classifiers=[ 30 | 'Development Status :: 7 - Inactive', 31 | 'Environment :: Web Environment', 32 | 'Intended Audience :: Developers', 33 | 'License :: OSI Approved :: BSD License', 34 | 'Operating System :: OS Independent', 35 | 'Programming Language :: Python', 36 | 'Programming Language :: Python :: 2.6', 37 | 'Programming Language :: Python :: 2.7', 38 | 'Programming Language :: Python :: 3', 39 | 'Programming Language :: Python :: 3.2', 40 | 'Programming Language :: Python :: 3.3', 41 | 'Framework :: Django', 42 | ], 43 | zip_safe=False, 44 | ) 45 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Carl Meyer and contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | * Neither the name of the author nor the names of other 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | env: 4 | global: 5 | - DTC_MYSQL_USERNAME=travis 6 | - DTC_PG_USERNAME=postgres 7 | # Keep this list up to date using `tox -l` 8 | matrix: 9 | - TOXENV=py27-django17-sqlite3 10 | - TOXENV=py27-django17-pg 11 | - TOXENV=py27-django17-postgis 12 | - TOXENV=py27-django17-mysql 13 | - TOXENV=py27-django18-sqlite3 14 | - TOXENV=py27-django18-pg 15 | - TOXENV=py27-django18-postgis 16 | - TOXENV=py27-django18-mysql 17 | - TOXENV=py33-django17-sqlite3 18 | - TOXENV=py33-django17-pg 19 | - TOXENV=py33-django17-postgis 20 | - TOXENV=py33-django17-mysql 21 | - TOXENV=py33-django18-sqlite3 22 | - TOXENV=py33-django18-pg 23 | - TOXENV=py33-django18-postgis 24 | - TOXENV=py33-django18-mysql 25 | - TOXENV=py34-django17-sqlite3 26 | - TOXENV=py34-django17-pg 27 | - TOXENV=py34-django17-postgis 28 | - TOXENV=py34-django17-mysql 29 | - TOXENV=py34-django18-sqlite3 30 | - TOXENV=py34-django18-pg 31 | - TOXENV=py34-django18-postgis 32 | - TOXENV=py34-django18-mysql 33 | - TOXENV=py35-django18-sqlite3 34 | - TOXENV=py35-django18-pg 35 | - TOXENV=py35-django18-postgis 36 | - TOXENV=py35-django18-mysql 37 | - TOXENV=pypy-django17-sqlite3 38 | - TOXENV=pypy-django17-pg 39 | - TOXENV=pypy-django17-postgis 40 | - TOXENV=pypy-django17-mysql 41 | - TOXENV=pypy-django18-sqlite3 42 | - TOXENV=pypy-django18-pg 43 | - TOXENV=pypy-django18-postgis 44 | - TOXENV=pypy-django18-mysql 45 | 46 | install: travis_retry pip install tox coveralls 47 | 48 | before_script: 49 | - mysql -e 'create database dtc;' 50 | - psql -c 'create database dtc;' -U postgres 51 | - psql -c 'create extension postgis;' -U postgres dtc 52 | 53 | script: tox -e $TOXENV 54 | 55 | after_success: coveralls 56 | -------------------------------------------------------------------------------- /transaction_hooks/mixin.py: -------------------------------------------------------------------------------- 1 | class TransactionHooksDatabaseWrapperMixin(object): 2 | """ 3 | A ``DatabaseWrapper`` mixin to implement transaction-committed hooks. 4 | 5 | To use, create a package for your custom database backend and place a 6 | ``base.py`` module within it. Import whatever ``DatabaseWrapper`` you want 7 | to subclass (under some other name), and then create a ``DatabaseWrapper`` 8 | class which inherits from both this mixin and the parent 9 | ``DatabaseWrapper`` (in that order). 10 | 11 | For an example, see ``backends/postgresql_psycopg2/base.py``. 12 | 13 | """ 14 | def __init__(self, *a, **kw): 15 | # a list of no-argument functions to run when the transaction commits; 16 | # each entry is an (sids, func) tuple, where sids is a list of the 17 | # active savepoint IDs when this function was registered 18 | self.run_on_commit = [] 19 | # Should we run the on-commit hooks the next time set_autocommit(True) 20 | # is called? 21 | self.run_commit_hooks_on_set_autocommit_on = False 22 | 23 | super(TransactionHooksDatabaseWrapperMixin, self).__init__(*a, **kw) 24 | 25 | def on_commit(self, func): 26 | if self.in_atomic_block: 27 | # transaction in progress; save for execution on commit 28 | self.run_on_commit.append((self.savepoint_ids[:], func)) 29 | else: 30 | # no transaction in progress; execute immediately 31 | func() 32 | 33 | def run_and_clear_commit_hooks(self): 34 | self.validate_no_atomic_block() 35 | try: 36 | while self.run_on_commit: 37 | sids, func = self.run_on_commit.pop(0) 38 | func() 39 | finally: 40 | self.run_on_commit = [] 41 | 42 | def commit(self, *a, **kw): 43 | super(TransactionHooksDatabaseWrapperMixin, self).commit(*a, **kw) 44 | 45 | # Atomic has not had a chance yet to restore autocommit on this 46 | # connection, so on databases that handle autocommit correctly, we need 47 | # to wait to run the hooks until it calls set_autocommit(True) 48 | if self.features.autocommits_when_autocommit_is_off: 49 | self.run_and_clear_commit_hooks() 50 | else: 51 | self.run_commit_hooks_on_set_autocommit_on = True 52 | 53 | def set_autocommit(self, autocommit): 54 | super(TransactionHooksDatabaseWrapperMixin, self).set_autocommit( 55 | autocommit) 56 | 57 | if autocommit and self.run_commit_hooks_on_set_autocommit_on: 58 | self.run_and_clear_commit_hooks() 59 | self.run_commit_hooks_on_set_autocommit_on = False 60 | 61 | def savepoint_rollback(self, sid, *a, **kw): 62 | super(TransactionHooksDatabaseWrapperMixin, self).savepoint_rollback( 63 | sid, *a, **kw) 64 | 65 | # remove any callbacks registered while this savepoint was active 66 | self.run_on_commit = list(filter( 67 | lambda x: sid not in x[0], self.run_on_commit)) 68 | 69 | def rollback(self, *a, **kw): 70 | super(TransactionHooksDatabaseWrapperMixin, self).rollback(*a, **kw) 71 | 72 | self.run_on_commit = [] 73 | 74 | def connect(self, *a, **kw): 75 | super(TransactionHooksDatabaseWrapperMixin, self).connect(*a, **kw) 76 | 77 | self.run_on_commit = [] 78 | 79 | def close(self, *a, **kw): 80 | super(TransactionHooksDatabaseWrapperMixin, self).close(*a, **kw) 81 | 82 | self.run_on_commit = [] 83 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 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. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-transaction-hooks.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-transaction-hooks.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /doc/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 = _build 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-transaction-hooks.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-transaction-hooks.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-transaction-hooks" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-transaction-hooks" 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 | -------------------------------------------------------------------------------- /transaction_hooks/test/test_basic.py: -------------------------------------------------------------------------------- 1 | from django.db import connection 2 | from django.db.transaction import atomic 3 | import pytest 4 | 5 | from .models import Thing 6 | 7 | 8 | class Tracker(object): 9 | """Simulate the pattern of creating a DB object and notifying about it.""" 10 | def __init__(self): 11 | self.notified = [] 12 | 13 | def notify(self, id_): 14 | if id_ == 'error': 15 | raise ForcedError() 16 | self.notified.append(id_) 17 | 18 | def do(self, num): 19 | """Create a Thing instance and notify about it.""" 20 | Thing.objects.create(num=num) 21 | connection.on_commit(lambda: self.notify(num)) 22 | 23 | def assert_done(self, nums): 24 | self.assert_notified(nums) 25 | assert sorted(t.num for t in Thing.objects.all()) == sorted(nums) 26 | 27 | def assert_notified(self, nums): 28 | assert self.notified == nums 29 | 30 | 31 | @pytest.fixture 32 | def track(): 33 | """Return a new ``Tracker`` instance.""" 34 | return Tracker() 35 | 36 | 37 | class ForcedError(Exception): 38 | pass 39 | 40 | 41 | @pytest.mark.usefixtures('transactional_db') 42 | class TestConnectionOnCommit(object): 43 | """ 44 | Tests for connection.on_commit(). 45 | 46 | Creation/checking of database objects in parallel with callback tracking is 47 | to verify that the behavior of the two match in all tested cases. 48 | 49 | """ 50 | def test_executes_immediately_if_no_transaction(self, track): 51 | track.do(1) 52 | track.assert_done([1]) 53 | 54 | def test_delays_execution_until_after_transaction_commit(self, track): 55 | with atomic(): 56 | track.do(1) 57 | track.assert_notified([]) 58 | track.assert_done([1]) 59 | 60 | def test_does_not_execute_if_transaction_rolled_back(self, track): 61 | try: 62 | with atomic(): 63 | track.do(1) 64 | raise ForcedError() 65 | except ForcedError: 66 | pass 67 | 68 | track.assert_done([]) 69 | 70 | def test_executes_only_after_final_transaction_committed(self, track): 71 | with atomic(): 72 | with atomic(): 73 | track.do(1) 74 | track.assert_notified([]) 75 | track.assert_notified([]) 76 | track.assert_done([1]) 77 | 78 | def test_discards_hooks_from_rolled_back_savepoint(self, track): 79 | with atomic(): 80 | # one successful savepoint 81 | with atomic(): 82 | track.do(1) 83 | # one failed savepoint 84 | try: 85 | with atomic(): 86 | track.do(2) 87 | raise ForcedError() 88 | except ForcedError: 89 | pass 90 | # another successful savepoint 91 | with atomic(): 92 | track.do(3) 93 | 94 | # only hooks registered during successful savepoints execute 95 | track.assert_done([1, 3]) 96 | 97 | def test_no_hooks_run_from_failed_transaction(self, track): 98 | """If outer transaction fails, no hooks from within it run.""" 99 | try: 100 | with atomic(): 101 | with atomic(): 102 | track.do(1) 103 | raise ForcedError() 104 | except ForcedError: 105 | pass 106 | 107 | track.assert_done([]) 108 | 109 | def test_inner_savepoint_rolled_back_with_outer(self, track): 110 | with atomic(): 111 | try: 112 | with atomic(): 113 | with atomic(): 114 | track.do(1) 115 | raise ForcedError() 116 | except ForcedError: 117 | pass 118 | track.do(2) 119 | 120 | track.assert_done([2]) 121 | 122 | def test_no_savepoints_atomic_merged_with_outer(self, track): 123 | with atomic(): 124 | with atomic(): 125 | track.do(1) 126 | try: 127 | with atomic(savepoint=False): 128 | raise ForcedError() 129 | except ForcedError: 130 | pass 131 | 132 | track.assert_done([]) 133 | 134 | def test_inner_savepoint_does_not_affect_outer(self, track): 135 | with atomic(): 136 | with atomic(): 137 | track.do(1) 138 | try: 139 | with atomic(): 140 | raise ForcedError() 141 | except ForcedError: 142 | pass 143 | 144 | track.assert_done([1]) 145 | 146 | def test_runs_hooks_in_order_registered(self, track): 147 | with atomic(): 148 | track.do(1) 149 | with atomic(): 150 | track.do(2) 151 | track.do(3) 152 | 153 | track.assert_done([1, 2, 3]) 154 | 155 | def test_hooks_cleared_after_successful_commit(self, track): 156 | with atomic(): 157 | track.do(1) 158 | with atomic(): 159 | track.do(2) 160 | 161 | track.assert_done([1, 2]) # not [1, 1, 2] 162 | 163 | def test_hooks_cleared_after_rollback(self, track): 164 | try: 165 | with atomic(): 166 | track.do(1) 167 | raise ForcedError() 168 | except ForcedError: 169 | pass 170 | 171 | with atomic(): 172 | track.do(2) 173 | 174 | track.assert_done([2]) 175 | 176 | @pytest.mark.skipif( 177 | not connection.features.test_db_allows_multiple_connections, 178 | reason='DB backend does not allow reconnect' 179 | ) 180 | def test_hooks_cleared_on_reconnect(self, track): 181 | with atomic(): 182 | track.do(1) 183 | connection.close() 184 | 185 | connection.connect() 186 | 187 | with atomic(): 188 | track.do(2) 189 | 190 | track.assert_done([2]) 191 | 192 | def test_error_in_hook_doesnt_prevent_clearing_hooks(self, track): 193 | try: 194 | with atomic(): 195 | connection.on_commit(lambda: track.notify('error')) 196 | except ForcedError: 197 | pass 198 | 199 | with atomic(): 200 | track.do(1) 201 | 202 | track.assert_done([1]) 203 | 204 | def test_db_query_in_hook(self, track): 205 | with atomic(): 206 | Thing.objects.create(num=1) 207 | connection.on_commit( 208 | lambda: [track.notify(t.num) for t in Thing.objects.all()]) 209 | 210 | track.assert_done([1]) 211 | 212 | # On databases that don't work with autocommit off (SQLite), Atomic doesn't 213 | # ever call set_autocommit(True) after committing a transaction (it sets 214 | # ``connection.autocommit = True`` directly instead), so we have to run 215 | # hooks immediately on commit instead of waiting for autocommit to be 216 | # restored. If a hook tries to create an internal transaction of its own, 217 | # this fails with an error. Need a better solution here; could be fixed if 218 | # transaction-hooks is merged into Django. 219 | @pytest.mark.xfail( 220 | connection.features.autocommits_when_autocommit_is_off, 221 | reason="Can't open transaction in a hook on SQLite", 222 | ) 223 | def test_transaction_in_hook(self, track): 224 | def on_commit(): 225 | with atomic(): 226 | t = Thing.objects.create(num=1) 227 | track.notify(t.num) 228 | 229 | with atomic(): 230 | connection.on_commit(on_commit) 231 | 232 | track.assert_done([1]) 233 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-transaction-hooks documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Jan 17 17:22:49 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = [] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'django-transaction-hooks' 44 | copyright = u'2014, Carl Meyer' 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 | 50 | parent_dir = os.path.dirname(os.path.dirname(__file__)) 51 | 52 | def get_version(): 53 | with open(os.path.join(parent_dir, 'transaction_hooks', '__init__.py')) as f: 54 | for line in f: 55 | if line.startswith('__version__ ='): 56 | return line.split('=')[1].strip().strip('"\'') 57 | 58 | 59 | # The full version, including alpha/beta/rc tags. 60 | release = get_version() 61 | # The short X.Y version. 62 | version = release 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | #language = None 67 | 68 | # There are two options for replacing |today|: either, you set today to some 69 | # non-false value, then it is used: 70 | #today = '' 71 | # Else, today_fmt is used as the format for a strftime call. 72 | #today_fmt = '%B %d, %Y' 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | exclude_patterns = ['_build'] 77 | 78 | # The reST default role (used for this markup: `text`) to use for all documents. 79 | #default_role = None 80 | 81 | # If true, '()' will be appended to :func: etc. cross-reference text. 82 | #add_function_parentheses = True 83 | 84 | # If true, the current module name will be prepended to all description 85 | # unit titles (such as .. function::). 86 | #add_module_names = True 87 | 88 | # If true, sectionauthor and moduleauthor directives will be shown in the 89 | # output. They are ignored by default. 90 | #show_authors = False 91 | 92 | # The name of the Pygments (syntax highlighting) style to use. 93 | pygments_style = 'sphinx' 94 | 95 | # A list of ignored prefixes for module index sorting. 96 | #modindex_common_prefix = [] 97 | 98 | 99 | # -- Options for HTML output --------------------------------------------------- 100 | 101 | # The theme to use for HTML and HTML Help pages. See the documentation for 102 | # a list of builtin themes. 103 | html_theme = 'default' 104 | 105 | # Theme options are theme-specific and customize the look and feel of a theme 106 | # further. For a list of options available for each theme, see the 107 | # documentation. 108 | #html_theme_options = {} 109 | 110 | # Add any paths that contain custom themes here, relative to this directory. 111 | #html_theme_path = [] 112 | 113 | # The name for this set of Sphinx documents. If None, it defaults to 114 | # " v documentation". 115 | #html_title = None 116 | 117 | # A shorter title for the navigation bar. Default is the same as html_title. 118 | #html_short_title = None 119 | 120 | # The name of an image file (relative to this directory) to place at the top 121 | # of the sidebar. 122 | #html_logo = None 123 | 124 | # The name of an image file (within the static path) to use as favicon of the 125 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 126 | # pixels large. 127 | #html_favicon = None 128 | 129 | # Add any paths that contain custom static files (such as style sheets) here, 130 | # relative to this directory. They are copied after the builtin static files, 131 | # so a file named "default.css" will overwrite the builtin "default.css". 132 | html_static_path = ['_static'] 133 | 134 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 135 | # using the given strftime format. 136 | #html_last_updated_fmt = '%b %d, %Y' 137 | 138 | # If true, SmartyPants will be used to convert quotes and dashes to 139 | # typographically correct entities. 140 | #html_use_smartypants = True 141 | 142 | # Custom sidebar templates, maps document names to template names. 143 | #html_sidebars = {} 144 | 145 | # Additional templates that should be rendered to pages, maps page names to 146 | # template names. 147 | #html_additional_pages = {} 148 | 149 | # If false, no module index is generated. 150 | #html_domain_indices = True 151 | 152 | # If false, no index is generated. 153 | #html_use_index = True 154 | 155 | # If true, the index is split into individual pages for each letter. 156 | #html_split_index = False 157 | 158 | # If true, links to the reST sources are added to the pages. 159 | #html_show_sourcelink = True 160 | 161 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 162 | #html_show_sphinx = True 163 | 164 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 165 | #html_show_copyright = True 166 | 167 | # If true, an OpenSearch description file will be output, and all pages will 168 | # contain a tag referring to it. The value of this option must be the 169 | # base URL from which the finished HTML is served. 170 | #html_use_opensearch = '' 171 | 172 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 173 | #html_file_suffix = None 174 | 175 | # Output file base name for HTML help builder. 176 | htmlhelp_basename = 'django-transaction-hooksdoc' 177 | 178 | 179 | # -- Options for LaTeX output -------------------------------------------------- 180 | 181 | latex_elements = { 182 | # The paper size ('letterpaper' or 'a4paper'). 183 | #'papersize': 'letterpaper', 184 | 185 | # The font size ('10pt', '11pt' or '12pt'). 186 | #'pointsize': '10pt', 187 | 188 | # Additional stuff for the LaTeX preamble. 189 | #'preamble': '', 190 | } 191 | 192 | # Grouping the document tree into LaTeX files. List of tuples 193 | # (source start file, target name, title, author, documentclass [howto/manual]). 194 | latex_documents = [ 195 | ('index', 'django-transaction-hooks.tex', u'django-transaction-hooks Documentation', 196 | u'Carl Meyer', 'manual'), 197 | ] 198 | 199 | # The name of an image file (relative to this directory) to place at the top of 200 | # the title page. 201 | #latex_logo = None 202 | 203 | # For "manual" documents, if this is true, then toplevel headings are parts, 204 | # not chapters. 205 | #latex_use_parts = False 206 | 207 | # If true, show page references after internal links. 208 | #latex_show_pagerefs = False 209 | 210 | # If true, show URL addresses after external links. 211 | #latex_show_urls = False 212 | 213 | # Documents to append as an appendix to all manuals. 214 | #latex_appendices = [] 215 | 216 | # If false, no module index is generated. 217 | #latex_domain_indices = True 218 | 219 | 220 | # -- Options for manual page output -------------------------------------------- 221 | 222 | # One entry per manual page. List of tuples 223 | # (source start file, name, description, authors, manual section). 224 | man_pages = [ 225 | ('index', 'django-transaction-hooks', u'django-transaction-hooks Documentation', 226 | [u'Carl Meyer'], 1) 227 | ] 228 | 229 | # If true, show URL addresses after external links. 230 | #man_show_urls = False 231 | 232 | 233 | # -- Options for Texinfo output ------------------------------------------------ 234 | 235 | # Grouping the document tree into Texinfo files. List of tuples 236 | # (source start file, target name, title, author, 237 | # dir menu entry, description, category) 238 | texinfo_documents = [ 239 | ('index', 'django-transaction-hooks', u'django-transaction-hooks Documentation', 240 | u'Carl Meyer', 'django-transaction-hooks', 'One line description of project.', 241 | 'Miscellaneous'), 242 | ] 243 | 244 | # Documents to append as an appendix to all manuals. 245 | #texinfo_appendices = [] 246 | 247 | # If false, no module index is generated. 248 | #texinfo_domain_indices = True 249 | 250 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 251 | #texinfo_show_urls = 'footnote' 252 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to django-transaction-hooks! 2 | ==================================== 3 | 4 | A better alternative to the transaction signals Django `will never have`_. 5 | 6 | Sometimes you need to fire off an action related to the current database 7 | transaction, but only if the transaction successfully commits. Examples: a 8 | `Celery`_ task, an email notification, or a cache invalidation. 9 | 10 | Doing this correctly while accounting for savepoints that might be individually 11 | rolled back, closed/dropped connections, and idiosyncrasies of various 12 | databases, `is non-trivial`_. Transaction signals just make it easier to do it 13 | wrong. 14 | 15 | ``django-transaction-hooks`` does the heavy lifting so you don't have to. 16 | 17 | .. _will never have: https://code.djangoproject.com/ticket/14051 18 | .. _Celery: http://www.celeryproject.org/ 19 | .. _is non-trivial: https://github.com/aaugustin/django-transaction-signals 20 | 21 | 22 | Prerequisites 23 | ------------- 24 | 25 | ``django-transaction-hooks`` supports `Django`_ 1.6.x through 1.8.x on Python 26 | 2.6, 2.7, 3.2, 3.3 and 3.4. 27 | 28 | ``django-transaction-hooks`` has been merged into Django 1.9 and is now a 29 | `built-in feature`_, so this third-party library should not be used with Django 30 | 1.9+. 31 | 32 | SQLite3, PostgreSQL (+ PostGIS), and MySQL are currently the only databases 33 | with built-in support; you can experiment with whether it works for your 34 | favorite database backend with just `a few lines of code`_. 35 | 36 | .. _Django: http://www.djangoproject.com/ 37 | .. _built-in feature: https://docs.djangoproject.com/en/1.9/topics/db/transactions/#performing-actions-after-commit 38 | 39 | 40 | Installation 41 | ------------ 42 | 43 | ``django-transaction-hooks`` is available on `PyPI`_. Install it with:: 44 | 45 | pip install django-transaction-hooks 46 | 47 | .. _PyPI: https://pypi.python.org/pypi/django-transaction-hooks/ 48 | 49 | 50 | Setup 51 | ----- 52 | 53 | ``django-transaction-hooks`` is implemented via custom database backends. (`Why 54 | backends?`_) 55 | 56 | For example, to use the PosgreSQL backend, set the ``ENGINE`` in your 57 | ``DATABASES`` setting to ``transaction_hooks.backends.postgresql_psycopg2`` (in 58 | place of ``django.db.backends.postgresql_psycopg2``). For example:: 59 | 60 | DATABASES = { 61 | 'default': { 62 | 'ENGINE': 'transaction_hooks.backends.postgresql_psycopg2', 63 | 'NAME': 'foo', 64 | }, 65 | } 66 | 67 | MySQL, SQLite, and PostGIS are similarly supported, via 68 | ``transaction_hooks.backends.mysql``, ``transaction_hooks.backends.sqlite3``, 69 | and ``transaction_hooks.backends.postgis``. 70 | 71 | .. _a few lines of code: 72 | .. _the mixin: 73 | 74 | Using the mixin 75 | ~~~~~~~~~~~~~~~ 76 | 77 | If you're currently using Django's built-in database backend for SQLite, 78 | Postgres, PostGIS, or MySQL, you can skip this section; just use the 79 | appropriate backend from ``transaction_hooks.backends`` as outlined above. 80 | 81 | Not using one of those? No worries - all the magic happens in a mixin, so 82 | making it happen with your favorite database backend may not be hard (no 83 | guarantees it'll work right, though.) 84 | 85 | You'll need to create your own custom backend that inherits both from 86 | ``transaction_hooks.mixin.TransactionHooksDatabaseWrapperMixin`` and from the 87 | database backend you're currently using. To do this, make a Python package (a 88 | directory with an ``__init__.py`` file in it) somewhere, and then put a 89 | ``base.py`` module inside that package. Its contents should look something like 90 | this:: 91 | 92 | from django.db.backends.postgresql_psycopg2 import base 93 | from transaction_hooks.mixin import TransactionHooksDatabaseWrapperMixin 94 | 95 | class DatabaseWrapper(TransactionHooksDatabaseWrapperMixin, 96 | base.DatabaseWrapper): 97 | pass 98 | 99 | Obviously you'll want to replace ``django.db.backends.postgresql_psycopg2`` 100 | with whatever existing backend you are currently using. 101 | 102 | Then set your database ``ENGINE`` (as above) to the Python dotted path to the 103 | package containing that ``base.py`` module. For example, if you put the above 104 | code in ``myproject/mybackend/base.py``, your ``ENGINE`` setting would be 105 | ``myproject.mybackend``. 106 | 107 | 108 | Usage 109 | ----- 110 | 111 | Pass any function (that takes no arguments) to ``connection.on_commit``:: 112 | 113 | from django.db import connection 114 | 115 | def do_something(): 116 | # send a mail, fire off a Celery task, what-have-you. 117 | 118 | connection.on_commit(do_something) 119 | 120 | You can also wrap your thing up in a lambda:: 121 | 122 | connection.on_commit(lambda: some_celery_task.delay('arg1')) 123 | 124 | The function you pass in will be called immediately after a hypothetical 125 | database write made at the same point in your code is successfully 126 | committed. If that hypothetical database write is instead rolled back, your 127 | function will be discarded and never called. 128 | 129 | If you register a callback while there is no transaction active, it will be 130 | executed immediately. 131 | 132 | 133 | Notes 134 | ~~~~~ 135 | 136 | .. warning:: 137 | 138 | This code is new, not yet battle-tested, and probably has bugs. If you find 139 | one, please `report it`_. 140 | 141 | .. _report it: https://github.com/carljm/django-transaction-hooks/blob/master/CONTRIBUTING.rst 142 | 143 | 144 | Use autocommit and transaction.atomic 145 | ''''''''''''''''''''''''''''''''''''' 146 | 147 | ``django-transaction-hooks`` is only built and tested to work correctly in 148 | `autocommit mode`_, which is the default in Django 1.6+, and with the 149 | `transaction.atomic`_ / `ATOMIC_REQUESTS`_ transaction API. If you set 150 | autocommit off on your connection and/or use lower-level transaction APIs 151 | directly, ``django-transaction-hooks`` likely won't work as you expect. 152 | 153 | For instance, commit hooks are not run until autocommit is restored on the 154 | connection following the commit (because otherwise any queries done in a commit 155 | hook would open an implicit transaction, preventing the connection from going 156 | back into autocommit mode). Also, even though with autocommit off you'd 157 | generally be in an implicit transaction outside of any ``atomic`` block, 158 | callback hooks registered outside an ``atomic`` block will still run 159 | immediately, not on commit. And there are probably more gotchas here. 160 | 161 | Use autocommit mode and `transaction.atomic`_ (or `ATOMIC_REQUESTS`_) and 162 | you'll be happier. 163 | 164 | 165 | .. _autocommit mode: https://docs.djangoproject.com/en/stable/topics/db/transactions/#managing-autocommit 166 | .. _transaction.atomic: https://docs.djangoproject.com/en/stable/topics/db/transactions/#django.db.transaction.atomic 167 | .. _ATOMIC_REQUESTS: https://docs.djangoproject.com/en/stable/topics/db/transactions/#tying-transactions-to-http-requests 168 | 169 | 170 | Order of execution 171 | '''''''''''''''''' 172 | 173 | On-commit hooks for a given transaction are executed in the order they were 174 | registered. 175 | 176 | 177 | Exception handling 178 | '''''''''''''''''' 179 | 180 | If one on-commit hook within a given transaction raises an uncaught 181 | exception, no later-registered hooks in that same transaction will run. (This 182 | is, of course, the same behavior as if you'd executed the hooks sequentially 183 | yourself without ``on_commit()``.) 184 | 185 | 186 | Timing of execution 187 | ''''''''''''''''''' 188 | 189 | Your hook functions are executed *after* a successful commit, so if they fail, 190 | it will not cause the transaction to roll back. They are executed conditionally 191 | upon the success of the transaction, but they are not *part* of the 192 | transaction. For the intended use cases (mail notifications, Celery tasks, 193 | etc), this is probably fine. If it's not (if your follow-up action is so 194 | critical that its failure should mean the failure of the transaction itself), 195 | then you don't want ``django-transaction-hooks``. (Instead, you may want 196 | `two-phase commit`_.) 197 | 198 | .. _two-phase commit: http://en.wikipedia.org/wiki/Two-phase_commit_protocol 199 | 200 | 201 | Use with South 202 | '''''''''''''' 203 | 204 | If you use `South`_, you will probably need to set the 205 | `SOUTH_DATABASE_ADAPTERS`_ setting when you switch to a custom database backend 206 | (e.g. to ``{'default': 'south.db.postgresql_psycopg2'}``, if you are using 207 | PostgreSQL). 208 | 209 | .. _South: https://south.readthedocs.io 210 | .. _SOUTH_DATABASE_ADAPTERS: https://south.readthedocs.io/en/latest/settings.html#south-database-adapters 211 | 212 | 213 | Use in tests 214 | '''''''''''' 215 | 216 | Django's `TestCase`_ class wraps each test in a transaction and rolls back that 217 | transaction after each test, in order to provide test isolation. This means 218 | that no transaction is ever actually committed, thus your ``on_commit`` hooks 219 | will never be run. If you need to test the results of an ``on_commit`` hook, 220 | you may need to use `TransactionTestCase`_ instead. 221 | 222 | .. _TestCase: https://docs.djangoproject.com/en/dev/topics/testing/tools/#django.test.TestCase 223 | .. _TransactionTestCase: https://docs.djangoproject.com/en/dev/topics/testing/tools/#transactiontestcase 224 | 225 | 226 | Savepoints 227 | '''''''''' 228 | 229 | Savepoints (i.e. nested ``transaction.atomic`` blocks) are handled 230 | correctly. That is, an ``on_commit`` hook registered after a savepoint (in a 231 | nested ``atomic`` block) will be called after the outer transaction is 232 | committed, but not if a rollback to that savepoint or any previous savepoint 233 | occurred during the transaction. 234 | 235 | 236 | .. _why backends?: 237 | 238 | Why database backends? 239 | '''''''''''''''''''''' 240 | 241 | Yeah, it's a bit of a pain. But since all transaction state is stored on the 242 | database connection object, this is the only way it can be done without 243 | monkeypatching. And I hate monkeypatching. 244 | 245 | (The worst bit about a custom database backend is that if you need two 246 | different ones, they can be hard or impossible to compose together. In this 247 | case, `the mixin`_ should make that less painful.) 248 | 249 | If this turns out to be really popular, it might be possible to get something 250 | like it into the Django core backends, which would remove that issue entirely. 251 | 252 | 253 | Why no rollback hook? 254 | ''''''''''''''''''''' 255 | 256 | A rollback hook is even harder to implement robustly than a commit hook, since 257 | a variety of things can cause an implicit rollback. For instance, your database 258 | connection was dropped because your process was killed without a chance to 259 | shutdown gracefully: your rollback hook will never run. 260 | 261 | The solution is simple: instead of doing something during the atomic block 262 | (transaction) and then undoing it if the transaction fails, use ``on_commit`` 263 | to delay doing it in the first place until after the transaction succeeds. It's 264 | a lot easier to undo something you never did in the first place! 265 | 266 | 267 | Contributing 268 | ------------ 269 | 270 | See the `contributing docs`_. 271 | 272 | .. _contributing docs: https://github.com/carljm/django-transaction-hooks/blob/master/CONTRIBUTING.rst 273 | --------------------------------------------------------------------------------