diff --git a/.travis.yml b/.travis.yml index 85a822a..bba299f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/NOTICE b/NOTICE index e4850e1..feaa652 100644 --- a/NOTICE +++ b/NOTICE @@ -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/) diff --git a/README.rst b/README.rst index da794d5..c056948 100644 --- a/README.rst +++ b/README.rst @@ -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 ======== @@ -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 ============= diff --git a/docs/doc/historization_with_cleanerversion.rst b/docs/doc/historization_with_cleanerversion.rst index 5fc65fc..2e756e8 100644 --- a/docs/doc/historization_with_cleanerversion.rst +++ b/docs/doc/historization_with_cleanerversion.rst @@ -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: @@ -746,9 +746,9 @@ Postgresql specific =================== Django creates `extra indexes `_ -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. diff --git a/setup.py b/setup.py index 06049af..529ca20 100644 --- a/setup.py +++ b/setup.py @@ -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', diff --git a/tox.ini b/tox.ini index 37c0bdc..b49afcd 100644 --- a/tox.ini +++ b/tox.ini @@ -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 = diff --git a/versions/admin.py b/versions/admin.py index a4a5d23..2fbc637 100644 --- a/versions/admin.py +++ b/versions/admin.py @@ -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): @@ -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), diff --git a/versions/models.py b/versions/models.py index ab38de4..5a8d98b 100644 --- a/versions/models.py +++ b/versions/models.py @@ -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 @@ -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) @@ -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): """ @@ -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 @@ -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)) @@ -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): @@ -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 @@ -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") @@ -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 @@ -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() diff --git a/versions_tests/tests/test_models.py b/versions_tests/tests/test_models.py index d4d5ff7..ec2ef64 100644 --- a/versions_tests/tests/test_models.py +++ b/versions_tests/tests/test_models.py @@ -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) diff --git a/versions_tests/tests/test_utils.py b/versions_tests/tests/test_utils.py index b90fc5c..903c019 100644 --- a/versions_tests/tests/test_utils.py +++ b/versions_tests/tests/test_utils.py @@ -1,5 +1,4 @@ from unittest import skipUnless -from django import VERSION from django.db import connection from django.test import TestCase, TransactionTestCase from django.db import IntegrityError @@ -7,10 +6,7 @@ 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') @@ -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