Skip to content

Commit

Permalink
Merge branch 'release/0.8'
Browse files Browse the repository at this point in the history
* release/0.8: (26 commits)
  bump 0.8
  updates demo requirements
  fixes tests
  fixes stupid mistake during merge (missed import)
  fixes django 1.8dev compatibility
  updates CHANGES
  django 1.7 compatibility
  apply some pep8 rules
  fixes python 3.2 compat
  django 1.7 AppConfig
  updates RTD theme
  updates README
  updates gitignore
  updates travis config. removed old unised matrix
  drop suppport for python 3.2
  remove sample-data=utils dependency
  fixes test. wrong decorator stops to work after 1.7
  updates gitignore
  fixes minor issue in Makefile that prevents tests to run
  Update install.rst
  ...
  • Loading branch information
saxix committed Sep 20, 2014
2 parents 058aa7d + 4109568 commit 2806b05
Show file tree
Hide file tree
Showing 48 changed files with 616 additions and 210 deletions.
2 changes: 2 additions & 0 deletions .gitignore
@@ -1,3 +1,5 @@
˜*
~build
.idea
.tox
.*
Expand Down
42 changes: 9 additions & 33 deletions .travis.yml
Expand Up @@ -2,47 +2,23 @@ language: python
services:
- MySQL
- PostgreSQL
python:
- 2.7
- 3.2
- 3.3

env:
- DJANGO="1.4.x" DBENGINE=mysql
- DJANGO="1.4.x" DBENGINE=pg

- DJANGO="1.5.x" DBENGINE=mysql
- DJANGO="1.5.x" DBENGINE=pg

- DJANGO="1.6.x" DBENGINE=mysql
- DJANGO="1.6.x" DBENGINE=pg

- DJANGO="dev" DBENGINE=mysql
- DJANGO="dev" DBENGINE=pg
- TESTENV=p27d14pg
- TESTENV=p27d15pg
- TESTENV=p27d16pg
- TESTENV=p27d17pg
- TESTENV=p27dtrunkpg
- TESTENV=p33d16pg
- TESTENV=p27d16mysql


install:
- make install-deps

script:
- make init-db ci

matrix:
exclude:
- python: 3.2
env: DJANGO="1.4.x" DBENGINE=mysql
- python: 3.2
env: DJANGO="1.4.x" DBENGINE=pg
- python: 3.2
env: DJANGO="1.5.x" DBENGINE=mysql
- python: 3.2
env: DJANGO="1.5.x" DBENGINE=pg
- python: 3.2
env: DJANGO="dev" DBENGINE=mysql
- python: 3.2
env: DJANGO="dev" DBENGINE=pg
- python: 3.2
env: DJANGO="1.6.x" DBENGINE=mysql
- make init-db
- tox -e $TESTENV


after_success:
Expand Down
9 changes: 9 additions & 0 deletions CHANGES
@@ -1,5 +1,14 @@
Release 0.8
-----------
* django 1.7 compatibility
* fixes typo in ``delete_selected_confirmation.html`` template
* python 3.2/3.3 compatibility

Release 0.7.1
-------------

* backward compatibility updates. Do not check for concurrency if `0` is passed as version value
(ie. no value provided by the form)


Release 0.7
Expand Down
25 changes: 14 additions & 11 deletions Makefile
@@ -1,11 +1,12 @@
VERSION=2.0.0
BUILDDIR='~build'
PYTHONPATH := ${PWD}/demo/:${PWD}
DJANGO_14=django==1.4.10
DJANGO_15=django==1.5.5
DJANGO_16=django==1.6.1
DJANGO_14='django>=1.4,<1.5'
DJANGO_15='django>=1.5,<1.6'
DJANGO_16='django>=1.6,>1.7'
DJANGO_17='https://www.djangoproject.com/download/1.7c1/tarball/'
DJANGO_DEV=git+git://github.com/django/django.git
DBNAME=concurrency
DBENGINE?=pg



Expand All @@ -15,7 +16,8 @@ mkbuilddir:

install-deps:
pip install -q \
-r requirements.pip python-coveralls
-r requirements.pip python-coveralls \
django_extensions


locale:
Expand All @@ -25,17 +27,17 @@ locale:


init-db:
@sh -c "if [ '${DBENGINE}' = 'mysql' ]; then mysql -e 'DROP DATABASE IF EXISTS ${DBNAME};'; fi"
@sh -c "if [ '${DBENGINE}' = 'mysql' ]; then mysql -e 'DROP DATABASE IF EXISTS concurrency;'; fi"
@sh -c "if [ '${DBENGINE}' = 'mysql' ]; then pip install MySQL-python; fi"
@sh -c "if [ '${DBENGINE}' = 'mysql' ]; then mysql -e 'CREATE DATABASE IF NOT EXISTS ${DBNAME};'; fi"
@sh -c "if [ '${DBENGINE}' = 'mysql' ]; then mysql -e 'CREATE DATABASE IF NOT EXISTS concurrency;'; fi"

@sh -c "if [ '${DBENGINE}' = 'pg' ]; then psql -c 'DROP DATABASE IF EXISTS ${DBNAME};' -U postgres; fi"
@sh -c "if [ '${DBENGINE}' = 'pg' ]; then psql -c 'CREATE DATABASE ${DBNAME};' -U postgres; fi"
@sh -c "if [ '${DBENGINE}' = 'pg' ]; then psql -c 'DROP DATABASE IF EXISTS concurrency;' -U postgres; fi"
@sh -c "if [ '${DBENGINE}' = 'pg' ]; then psql -c 'CREATE DATABASE concurrency;' -U postgres; fi"
@sh -c "if [ '${DBENGINE}' = 'pg' ]; then pip install -q psycopg2; fi"


test:
demo/manage.py test concurrency --settings=${DJANGO_SETTINGS_MODULE} -v2
py.test -vvv


coverage: mkbuilddir
Expand All @@ -46,6 +48,7 @@ ci: init-db install-deps
@sh -c "if [ '${DJANGO}' = '1.4.x' ]; then pip install ${DJANGO_14}; fi"
@sh -c "if [ '${DJANGO}' = '1.5.x' ]; then pip install ${DJANGO_15}; fi"
@sh -c "if [ '${DJANGO}' = '1.6.x' ]; then pip install ${DJANGO_16}; fi"
@sh -c "if [ '${DJANGO}' = '1.7.x' ]; then pip install ${DJANGO_17}; fi"
@sh -c "if [ '${DJANGO}' = 'dev' ]; then pip install ${DJANGO_DEV}; fi"
@pip install coverage
@python -c "from __future__ import print_function;import django;print('Django version:', django.get_version())"
Expand All @@ -65,7 +68,7 @@ clonedigger: mkbuilddir

docs: mkbuilddir
mkdir -p ${BUILDDIR}/docs
sphinx-build -aE docs/source ${BUILDDIR}/docs
sphinx-build -aE docs/ ${BUILDDIR}/docs
ifdef BROWSE
firefox ${BUILDDIR}/docs/index.html
endif
Expand Down
26 changes: 24 additions & 2 deletions README.rst
Expand Up @@ -12,13 +12,12 @@ Django Concurrency

django-concurrency is an optimistic lock [1]_ implementation for Django.

Tested with: 1.4.x, 1.5.x, 1.6.x, trunk.
Tested with: 1.4.x, 1.5.x, 1.6.x, 1.7 trunk.

It prevents users from doing concurrent editing in Django both from UI and from a
django command.



How it works
------------
sample code::
Expand All @@ -38,6 +37,18 @@ Now if you try::
you will get a ``RecordModifiedError`` on ``b.save()``


Similar projects
----------------

Other projects that handle concurrent editing are `django-optimistic-lock`_ and `django-locking`_ anyway concurrency is "a batteries included" optimistic lock management system, here some features not available elsewhere:

* can be applied to any model; not only your code (ie. django.contrib.auth.Group)
* works with django 1.4 and 1.5
* handle `list-editable`_ ChangeList. (handle `#11313 <https://code.djangoproject.com/ticket/11313>`_)
* manage concurrency conflicts in admin's actions
* can intercept changes performend out of the django app (ie using pgAdmin, phpMyAdmin, Toads) (using `TriggerVersionField_`


Links
~~~~~

Expand Down Expand Up @@ -76,6 +87,17 @@ Links
:target: https://requires.io/github/saxix/django-concurrency/requirements/?branch=develop
:alt: Requirements Status

.. |wheel| image:: https://pypip.in/wheel/blackhole/badge.png

_list-editable: https://django-concurrency.readthedocs.org/en/latest/admin.html#list-editable

.. _list-editable: https://django-concurrency.readthedocs.org/en/latest/admin.html#list-editable

.. _django-locking: https://github.com/stdbrouw/django-locking

.. _django-optimistic-lock: https://github.com/gavinwahl/django-optimistic-lock

.. _TriggerVersionField: https://django-concurrency.readthedocs.org/en/latest/fields.html#triggerversionfield

.. [1] http://en.wikipedia.org/wiki/Optimistic_concurrency_control
5 changes: 3 additions & 2 deletions concurrency/__init__.py
Expand Up @@ -2,7 +2,7 @@
import datetime
import os

VERSION = __version__ = (0, 8, 0, 'alpha', 0)
VERSION = __version__ = (0, 8, 0, 'final', 0)
__author__ = 'sax'


Expand Down Expand Up @@ -41,7 +41,8 @@ def get_git_changeset():
repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
git_log = subprocess.Popen('git log --pretty=format:%ct --quiet -1 HEAD',
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=True, cwd=repo_dir, universal_newlines=True)
shell=True, cwd=repo_dir,
universal_newlines=True)
timestamp = git_log.communicate()[0]
try:
timestamp = datetime.datetime.utcfromtimestamp(int(timestamp))
Expand Down
6 changes: 3 additions & 3 deletions concurrency/admin.py
@@ -1,4 +1,4 @@
## -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import operator
import re
Expand All @@ -7,8 +7,8 @@
from django.contrib import admin, messages
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.db.models import Q
from django.forms.formsets import (ManagementForm, TOTAL_FORM_COUNT, INITIAL_FORM_COUNT,
MAX_NUM_FORM_COUNT)
from django.forms.formsets import (ManagementForm, TOTAL_FORM_COUNT,
INITIAL_FORM_COUNT, MAX_NUM_FORM_COUNT)
from django.forms.models import BaseModelFormSet
from django.utils.safestring import mark_safe
from django.contrib.admin import helpers
Expand Down
4 changes: 3 additions & 1 deletion concurrency/api.py
Expand Up @@ -77,11 +77,13 @@ def apply_concurrency_check(model, fieldname, versionclass):
:type versionclass: concurrency.fields.VersionField subclass
"""
if hasattr(model, '_concurrencymeta'):
raise ImproperlyConfigured("%s is already under concurrency management" % model)
return
# raise ImproperlyConfigured("%s is already under concurrency management" % model)

logger.debug('Applying concurrency check to %s' % model)

ver = versionclass()
# import ipdb; ipdb.set_trace()
ver.contribute_to_class(model, fieldname)
model._concurrencymeta._field = ver

Expand Down
1 change: 1 addition & 0 deletions concurrency/db/backends/common.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-


class TriggerMixin(object):
def drop_triggers(self):
for trigger_name in self.list_triggers():
Expand Down
2 changes: 0 additions & 2 deletions concurrency/db/backends/mysql/base.py
Expand Up @@ -20,5 +20,3 @@ def drop_trigger(self, trigger_name):
cursor = self.cursor()
result = cursor.execute("DROP TRIGGER IF EXISTS %s;" % trigger_name)
return result


7 changes: 4 additions & 3 deletions concurrency/db/backends/mysql/creation.py
Expand Up @@ -16,17 +16,18 @@ class MySQLCreation(DatabaseCreation):
FOR EACH ROW SET NEW.{field.column} = OLD.{field.column}+1;
"""


def _create_trigger(self, field):
import MySQLdb as Database
from warnings import filterwarnings, resetwarnings

filterwarnings('ignore', message='Trigger does not exist', category=Database.Warning)
filterwarnings('ignore', message='Trigger does not exist',
category=Database.Warning)

opts = field.model._meta
trigger_name = get_trigger_name(field, opts)

stm = self.sql.format(trigger_name=trigger_name, opts=opts, field=field)
stm = self.sql.format(trigger_name=trigger_name,
opts=opts, field=field)
cursor = self.connection._clone().cursor()
try:
cursor.execute(stm)
Expand Down
2 changes: 1 addition & 1 deletion concurrency/db/backends/postgresql_psycopg2/base.py
Expand Up @@ -20,7 +20,7 @@ def list_triggers(self):
return [m[1] for m in cursor.fetchall()]

def drop_trigger(self, trigger_name):
if not trigger_name in self.list_triggers():
if trigger_name not in self.list_triggers():
return []
cursor = self.cursor()
table_name = re.sub('^concurrency_(.*)_[ui]', '\\1', trigger_name)
Expand Down
2 changes: 1 addition & 1 deletion concurrency/db/backends/sqlite3/base.py
Expand Up @@ -11,7 +11,7 @@ def __init__(self, *args, **kwargs):

def list_triggers(self):
cursor = self.cursor()
result = cursor.execute("select name from sqlite_master where type = 'trigger';")
result = cursor.execute("select name from sqlite_master where type='trigger';")
return [m[0] for m in result.fetchall()]

def drop_trigger(self, trigger_name):
Expand Down
3 changes: 1 addition & 2 deletions concurrency/fields.py
Expand Up @@ -146,7 +146,7 @@ def _do_update(model_instance, base_qs, using, pk_val, values, update_fields, fo
break

if values:
if model_instance._concurrencymeta.enabled:
if model_instance._concurrencymeta.enabled and old_version:
filter_kwargs = {'pk': pk_val, version_field.attname: old_version}
updated = base_qs.filter(**filter_kwargs)._update(values) >= 1
if not updated:
Expand Down Expand Up @@ -208,7 +208,6 @@ def pre_save(self, model_instance, add):
# always returns the same value
return int(getattr(model_instance, self.attname, 0))


@staticmethod
def _increment_version_number(obj):
old_value = get_revision_of_object(obj)
Expand Down
Expand Up @@ -6,7 +6,7 @@
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=app_label %}">{{ app_label|capfirst|escape }}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_label|capfirst|escape }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; {% trans 'Delete multiple objects' %}
</div>
Expand Down
3 changes: 2 additions & 1 deletion concurrency/templatetags/concurrency.py
Expand Up @@ -14,7 +14,8 @@ def identity(obj):
returns a string representing "<pk>,<version>" of the passed object
"""
if hasattr(obj, '_concurrencymeta'):
return mark_safe("{0},{1}".format(unlocalize(obj.pk), get_revision_of_object(obj)))
return mark_safe("{0},{1}".format(unlocalize(obj.pk),
get_revision_of_object(obj)))
else:
return mark_safe(unlocalize(obj.pk))

Expand Down
12 changes: 6 additions & 6 deletions demo/demoproject/demoapp/models.py
Expand Up @@ -3,18 +3,18 @@


class DemoModel(models.Model):
version = fields.IntegerVersionField()
# version = fields.IntegerVersionField()
char = models.CharField(max_length=255)
integer = models.IntegerField()

class Meta:
app_label = 'demoapp'


class ProxyDemoModel(DemoModel):
class Meta:
app_label = 'demoapp'
proxy = True
#
# class ProxyDemoModel(DemoModel):
# class Meta:
# app_label = 'demoapp'
# proxy = True


def proxy_factory(name):
Expand Down
2 changes: 1 addition & 1 deletion demo/demoproject/requirements.pip
@@ -1,3 +1,3 @@
django-import-export==0.1.5
django-import-export==0.2.4
diff-match-patch==20121119
django-extensions

0 comments on commit 2806b05

Please sign in to comment.