Skip to content
This repository has been archived by the owner on Feb 7, 2019. It is now read-only.

Commit

Permalink
Merge remote-tracking branch 'origin/master' into prefetch-related-fk…
Browse files Browse the repository at this point in the history
…-historic-versions
  • Loading branch information
brki committed Sep 22, 2016
2 parents e5668d9 + 580940f commit 5b75d37
Show file tree
Hide file tree
Showing 10 changed files with 33 additions and 112 deletions.
8 changes: 0 additions & 8 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,8 @@ language: python
python: "2.7"

env:
- TOX_ENV=py27-django16-pg
- TOX_ENV=py27-django16-sqlite
- TOX_ENV=py27-django17-pg
- TOX_ENV=py27-django17-sqlite
- TOX_ENV=py27-django18-pg
- TOX_ENV=py27-django18-sqlite
- TOX_ENV=py34-django16-pg
- TOX_ENV=py34-django16-sqlite
- TOX_ENV=py34-django17-pg
- TOX_ENV=py34-django17-sqlite
- TOX_ENV=py34-django18-pg
- TOX_ENV=py34-django18-sqlite

Expand Down
4 changes: 2 additions & 2 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ Copyright 2014 Swisscom, Sophia Engineering
This software contains code derived from DirtyVersion available at
https://github.com/cordmata/dirtyversion

This software was tested on Django 1.6 & 1.7 (https://www.djangoproject.com/)
This software was tested on Django 1.8 (https://www.djangoproject.com/)

This software is written in Python 2.7 (https://www.python.org/)
This software is written for Python 2.7 and Python 3.4 (https://www.python.org/)
11 changes: 9 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ relational database. It allows to keep track of modifications on an object over

CleanerVersion therefore enables a Django-based Datawarehouse, which was the initial idea of this package.


Features
========

Expand Down Expand Up @@ -50,9 +49,17 @@ Prerequisites
This code was tested with the following technical components

* Python 2.7 & 3.4
* Django 1.6, 1.7 & 1.8
* Django 1.8
* PostgreSQL 9.3.4 & SQLite3

Older Django versions
=====================
CleanerVersion was originally written for Django 1.6.

Old packages compatible with older Django releases:

* Django 1.6 and 1.7: https://pypi.python.org/pypi/CleanerVersion/1.5.4


Documentation
=============
Expand Down
8 changes: 4 additions & 4 deletions docs/doc/historization_with_cleanerversion.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Historization with CleanerVersion
*********************************

Disclaimer: This documentation as well as the CleanerVersion application code have been written to work against Django
1.6.x, 1.7.x and 1.8.x. The documentation may not be accurate anymore when using more recent versions of Django.
1.8.x. The documentation may not be accurate anymore when using more recent versions of Django.

.. _cleanerversion-quick-starter:

Expand Down Expand Up @@ -746,9 +746,9 @@ Postgresql specific
===================

Django creates `extra indexes <https://docs.djangoproject.com/en/1.7/ref/databases/#indexes-for-varchar-and-text-columns>`_
for CharFields that are used for like queries (e.g. WHERE foo like 'fish%'). Since Django 1.6 and 1.7 do not support
native database UUID fields, the UUID fields that are used for the id and identity columns of Versionable models have these extra
indexes created. In fact, these fields will never be compared using the like operator. Leaving these indexes would create a
for CharFields that are used for like queries (e.g. WHERE foo like 'fish%'). Since Django 1.6 (the version CleanerVersion originally
targeted) did not have native database UUID fields, the UUID fields that are used for the id and identity columns of Versionable models
have these extra indexes created. In fact, these fields will never be compared using the like operator. Leaving these indexes would create a
performance penalty for inserts and updates, especially for larger tables. ``versions.util.postgresql`` has a function
``remove_uuid_id_like_indexes`` that can be used to remove these extra indexes.

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
install_requires=['django'],
package_data={'versions': ['static/js/*.js','templates/versions/*.html']},
classifiers=[
'Development Status :: 4 - Beta',
'Development Status :: 5 - Production/Stable',
'Framework :: Django',
'Intended Audience :: Developers',
'Programming Language :: Python :: 2.7',
Expand Down
4 changes: 1 addition & 3 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@

[tox]
envlist =
py{27,34}-django{16,17,18}-{sqlite,pg}
py{27,34}-django{18}-{sqlite,pg}

[testenv]
deps =
coverage
django16: django>=1.6,<1.7
django17: django>=1.7,<1.8
django18: django>=1.8,<1.9
pg: psycopg2
commands =
Expand Down
4 changes: 1 addition & 3 deletions versions/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from django.utils.encoding import force_text
from django.utils.text import capfirst
from django.template.response import TemplateResponse
from django import VERSION
from datetime import datetime

class DateTimeFilterForm(forms.Form):
Expand Down Expand Up @@ -239,8 +238,7 @@ def history_view(self, request, object_id, extra_context=None):
content_type=get_content_type_for_model(model)
).select_related().order_by('action_time')

ctx = self.admin_site.each_context() if VERSION < (1, 8) \
else self.admin_site.each_context(request)
ctx = self.admin_site.each_context(request)

context = dict(ctx,
title=('Change history: %s') % force_text(obj),
Expand Down
93 changes: 10 additions & 83 deletions versions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,8 @@
from collections import namedtuple
import re

from django import VERSION

if VERSION[:2] >= (1, 8):
from django.db.models.sql.datastructures import Join
if VERSION[:2] >= (1, 7):
from django.apps.registry import apps
from django.db.models.sql.datastructures import Join
from django.apps.registry import apps
from django.core.exceptions import SuspiciousOperation, ObjectDoesNotExist
from django.db import transaction
from django.db.models.base import Model
Expand Down Expand Up @@ -292,29 +288,18 @@ def as_sql(self, qn, connection):
to be able to add time restrictions for those tables based on the VersionedQuery's
querytime value.
:param qn: In Django 1.7 & 1.8 this is a compiler; in 1.6, it's an instance-method
:param qn: In Django 1.7 & 1.8 this is a compiler
:param connection: A DB connection
:return: A tuple consisting of (sql_string, result_params)
"""
# self.children is an array of VersionedExtraWhere-objects
for child in self.children:
if isinstance(child, VersionedExtraWhere) and not child.params:
try:
# Django 1.7 & 1.8 handles compilers as objects
_query = qn.query
except AttributeError:
# Django 1.6 handles compilers as instancemethods
_query = qn.__self__.query
_query = qn.query
query_time = _query.querytime.time
apply_query_time = _query.querytime.active
alias_map = _query.alias_map
# In Django 1.6 & 1.7, use the join_map to know, what *table* gets joined to which
# *left-hand sided* table
# In Django 1.8, use the Join objects in alias_map
if hasattr(_query, 'join_map'):
self._set_child_joined_alias_using_join_map(child, _query.join_map, alias_map)
else:
self._set_child_joined_alias(child, alias_map)
self._set_child_joined_alias(child, alias_map)
if apply_query_time:
# Add query parameters that have not been added till now
child.set_as_of(query_time)
Expand All @@ -323,32 +308,6 @@ def as_sql(self, qn, connection):
child.sqls = []
return super(VersionedWhereNode, self).as_sql(qn, connection)

@staticmethod
def _set_child_joined_alias_using_join_map(child, join_map, alias_map):
"""
Set the joined alias on the child, for Django <= 1.7.x.
:param child:
:param join_map:
:param alias_map:
"""
for lhs, table, join_cols in join_map:
if lhs is None:
continue
if lhs == child.alias:
relevant_alias = child.related_alias
elif lhs == child.related_alias:
relevant_alias = child.alias
else:
continue

join_info = alias_map[relevant_alias]
if join_info.join_type is None:
continue

if join_info.lhs_alias in [child.alias, child.related_alias]:
child.set_joined_alias(relevant_alias)
break

@staticmethod
def _set_child_joined_alias(child, alias_map):
"""
Expand Down Expand Up @@ -582,23 +541,6 @@ def _clone(self, *args, **kwargs):
:param kwargs: Same as the original QuerySet._clone params
:return: Just as QuerySet._clone, this method returns a clone of the original object
"""
if VERSION[:2] == (1, 6):
klass = kwargs.pop('klass', None)
# This patch was taken from Django 1.7 and is applied only in case we're using Django 1.6 and
# ValuesListQuerySet objects. Since VersionedQuerySet is not a subclass of ValuesListQuerySet, a new type
# inheriting from both is created and used as class.
# https://github.com/django/django/blob/1.7/django/db/models/query.py#L943
if klass and not issubclass(self.__class__, klass):
base_queryset_class = getattr(self, '_base_queryset_class', self.__class__)
class_bases = (klass, base_queryset_class)
class_dict = {
'_base_queryset_class': base_queryset_class,
'_specialized_queryset_class': klass,
}
kwargs['klass'] = type(klass.__name__, class_bases, class_dict)
else:
kwargs['klass'] = klass

clone = super(VersionedQuerySet, self)._clone(**kwargs)
clone.querytime = self.querytime
return clone
Expand Down Expand Up @@ -796,7 +738,7 @@ def create_versioned_many_to_many_intermediary_model(field, cls, field_name):
# declared apps' models inside a __fake__ module.
# This means that the models can be already loaded and registered by their original module, when we
# reach this point of the application and therefore there is no need to load them a second time.
if VERSION[:2] >= (1, 7) and cls.__module__ == '__fake__':
if cls.__module__ == '__fake__':
try:
# Check the apps for an already registered model
return apps.get_registered_model(cls._meta.app_label, str(name))
Expand Down Expand Up @@ -943,10 +885,7 @@ def clear(self, **kwargs):
with transaction.atomic(using=db, savepoint=False):
cloned_pks = [obj.clone().pk for obj in queryset]
update_qs = self.current.filter(pk__in=cloned_pks)
if VERSION[:2] == (1, 6):
update_qs.update(**{rel_field.name: None})
else:
self._clear(update_qs, bulk)
self._clear(update_qs, bulk)

if 'remove' in dir(manager_cls):
def remove(self, *objs):
Expand Down Expand Up @@ -987,10 +926,7 @@ def __init__(self, *args, **kwargs):
version_start_date_field = self.through._meta.get_field('version_start_date')
version_end_date_field = self.through._meta.get_field('version_end_date')
except FieldDoesNotExist as e:
if VERSION[:2] >= (1, 8):
fields = [f.name for f in self.through._meta.get_fields()]
else:
fields = self.through._meta.get_all_field_names()
fields = [f.name for f in self.through._meta.get_fields()]
print(str(e) + "; available fields are " + ", ".join(fields))
raise e
# FIXME: this probably does not work when auto-referencing
Expand Down Expand Up @@ -1025,12 +961,8 @@ def _remove_items_at(self, timestamp, source_field_name, target_field_name, *obj
old_ids = set()
for obj in objs:
if isinstance(obj, self.model):
# The Django 1.7-way is preferred
if hasattr(self, 'target_field'):
fk_val = self.target_field.get_foreign_related_value(obj)[0]
# But the Django 1.6.x -way is supported for backward compatibility
elif hasattr(self, '_get_fk_val'):
fk_val = self._get_fk_val(obj, target_field_name)
else:
raise TypeError("We couldn't find the value of the foreign key, this might be due to the "
"use of an unsupported version of Django")
Expand Down Expand Up @@ -1173,13 +1105,7 @@ def get_current_m2m_diff(self, instance, new_objects):

filter = Q(**{relation_manager.source_field.attname: instance.pk})
qs = self.through.objects.current.filter(filter)
try:
# Django 1.7
target_name = relation_manager.target_field.attname
except AttributeError:
# Django 1.6
target_name = relation_manager.through._meta.get_field_by_name(
relation_manager.target_field_name)[0].attname
target_name = relation_manager.target_field.attname
current_ids = set(qs.values_list(target_name, flat=True))

being_removed = current_ids - new_ids
Expand Down Expand Up @@ -1511,6 +1437,7 @@ def restore(self, **kwargs):
latest = cls.objects.current_version(self, check_db=True)
if latest and latest != self:
latest.delete()
restored.version_start_date = latest.version_end_date

self.save()
restored.save()
Expand Down
3 changes: 3 additions & 0 deletions versions_tests/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2580,6 +2580,9 @@ def test_restore_previous_version(self):
self.assertSetEqual(set(previous.awards.all()), set(self.awards.values()))
self.assertEqual(self.forty_niners, previous.team)

# There should be no overlap of version periods.
self.assertEquals(previous.version_end_date, restored.version_start_date)

def test_restore_with_required_foreignkey(self):
team = Team.objects.create(name="Flying Pigs")
mascot_v1 = Mascot.objects.create(name="Curly", team=team)
Expand Down
8 changes: 2 additions & 6 deletions versions_tests/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
from unittest import skipUnless
from django import VERSION
from django.db import connection
from django.test import TestCase, TransactionTestCase
from django.db import IntegrityError
from versions_tests.models import ChainStore, Color
from versions.util.postgresql import get_uuid_like_indexes_on_table


AT_LEAST_17 = VERSION[:2] >= (1, 7)


@skipUnless(AT_LEAST_17 and connection.vendor == 'postgresql', "Postgresql-specific test")
@skipUnless(connection.vendor == 'postgresql', "Postgresql-specific test")
class PostgresqlVersionUniqueTests(TransactionTestCase):
def setUp(self):
self.red = Color.objects.create(name='red')
Expand Down Expand Up @@ -90,7 +86,7 @@ def test_identity_unique(self):
c.save()


@skipUnless(AT_LEAST_17 and connection.vendor == 'postgresql', "Postgresql-specific test")
@skipUnless(connection.vendor == 'postgresql', "Postgresql-specific test")
class PostgresqlUuidLikeIndexesTest(TestCase):
def test_no_like_indexes_on_uuid_columns(self):
# Django creates like indexes on char columns. In Django 1.7.x and below, there is no
Expand Down

0 comments on commit 5b75d37

Please sign in to comment.