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

Commit

Permalink
Ensure versioning fields are set, but not if any fields are deferred.
Browse files Browse the repository at this point in the history
Fixes #124.
  • Loading branch information
brki committed Dec 13, 2016
1 parent 87b0a3c commit 6954acd
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 37 deletions.
54 changes: 17 additions & 37 deletions versions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import datetime
import uuid
from collections import namedtuple
import re

from django.db.models.sql.datastructures import Join
from django.apps.registry import apps
Expand Down Expand Up @@ -902,13 +901,9 @@ def create_versioned_many_related_manager(superclass, rel):
class VersionedManyRelatedManager(many_related_manager_klass):
def __init__(self, *args, **kwargs):
super(VersionedManyRelatedManager, self).__init__(*args, **kwargs)
# Additional core filters are: version_start_date <= t & (version_end_date > t | version_end_date IS NULL)
# but we cannot work with the Django core filters, since they don't support ORing filters, which
# is a thing we need to consider the "version_end_date IS NULL" case;
# So, we define our own set of core filters being applied when versioning
try:
version_start_date_field = self.through._meta.get_field('version_start_date')
version_end_date_field = self.through._meta.get_field('version_end_date')
_ = self.through._meta.get_field('version_start_date')
_ = self.through._meta.get_field('version_end_date')
except FieldDoesNotExist as e:
fields = [f.name for f in self.through._meta.get_fields()]
print(str(e) + "; available fields are " + ", ".join(fields))
Expand Down Expand Up @@ -1188,9 +1183,24 @@ class Meta:

def __init__(self, *args, **kwargs):
super(Versionable, self).__init__(*args, **kwargs)

# _querytime is for library-internal use.
self._querytime = QueryTime(time=None, active=False)

# Ensure that the versionable field values are set.
# If there are any deferred fields, then this instance is being initialized
# from data in the database, and thus these values will already be set (unless
# the fields are deferred, in which case they should not be set here).
if not self.get_deferred_fields():
if not getattr(self, 'version_start_date', None):
setattr(self, 'version_start_date', get_utc_now())
if not getattr(self, 'version_birth_date', None):
setattr(self, 'version_birth_date', self.version_start_date)
if not getattr(self, self.VERSION_IDENTIFIER_FIELD, None):
setattr(self, self.VERSION_IDENTIFIER_FIELD, self.uuid())
if not getattr(self, self.OBJECT_IDENTIFIER_FIELD, None):
setattr(self, self.OBJECT_IDENTIFIER_FIELD, getattr(self, self.VERSION_IDENTIFIER_FIELD))

def delete(self, using=None):
using = using or router.db_for_write(self.__class__, instance=self)
assert self._get_pk_val() is not None, \
Expand Down Expand Up @@ -1490,33 +1500,3 @@ def matches_querytime(instance, querytime):

return (instance.version_start_date <= querytime.time
and (instance.version_end_date is None or instance.version_end_date > querytime.time))


class VersionedManyToManyModel(object):
"""
This class is used for holding signal handlers required for proper versioning
"""

@staticmethod
def post_init_initialize(sender, instance, **kwargs):
"""
This is the signal handler post-initializing the intermediate many-to-many model.
:param sender: The model class that just had an instance created.
:param instance: The actual instance of the model that's just been created.
:param kwargs: Required by Django definition
:return: None
"""
if isinstance(instance, sender) and isinstance(instance, Versionable):
ident = Versionable.uuid()
now = get_utc_now()
if not hasattr(instance, 'version_start_date') or instance.version_start_date is None:
instance.version_start_date = now
if not hasattr(instance, 'version_birth_date') or instance.version_birth_date is None:
instance.version_birth_date = now
if not hasattr(instance, 'id') or not bool(instance.id):
instance.id = ident
if not hasattr(instance, 'identity') or not bool(instance.identity):
instance.identity = ident


post_init.connect(VersionedManyToManyModel.post_init_initialize)
1 change: 1 addition & 0 deletions versions_tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class B(Versionable):
# - VersionNavigationAsOfTest
# - VersionRestoreTest
# - DetachTest
# - DeferredFieldsTest
@python_2_unicode_compatible
class City(Versionable):
name = CharField(max_length=200)
Expand Down
64 changes: 64 additions & 0 deletions versions_tests/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2734,3 +2734,67 @@ def test_detach_with_relations(self):
t = Team.objects.current.get(pk=t_pk)
self.assertEqual({p, p2}, set(t.player_set.all()))
self.assertEqual([], list(t2.player_set.all()))


class DeferredFieldsTest(TestCase):

def setUp(self):
self.c1 = City.objects.create(name="Porto")
self.team1 = Team.objects.create(name="Tigers", city=self.c1)

def test_simple_defer(self):
limited = City.objects.current.only('name').get(pk=self.c1.pk)
deferred_fields = set(Versionable.VERSIONABLE_FIELDS)
deferred_fields.remove('id')
self.assertSetEqual(deferred_fields, set(limited.get_deferred_fields()))
for field_name in deferred_fields:
self.assertNotIn(field_name, limited.__dict__ )

deferred_fields = ['version_start_date', 'version_end_date']
deferred = City.objects.current.defer(*deferred_fields).get(pk=self.c1.pk)
self.assertSetEqual(set(deferred_fields), set(deferred.get_deferred_fields()))
for field_name in deferred_fields:
self.assertNotIn(field_name, deferred.__dict__ )

# Accessing deferred fields triggers queries:
with self.assertNumQueries(2):
self.assertEquals(self.c1.version_start_date, deferred.version_start_date)
self.assertEquals(self.c1.version_end_date, deferred.version_end_date)
# If already fetched, no query is made:
with self.assertNumQueries(0):
self.assertEquals(self.c1.version_start_date, deferred.version_start_date)

def test_deferred_foreign_key_field(self):

team_full = Team.objects.current.get(pk=self.team1.pk)
self.assertIn('city_id', team_full.__dict__ )
team_light = Team.objects.current.only('name').get(pk=self.team1.pk)
self.assertNotIn('city_id', team_light.__dict__ )
with self.assertNumQueries(2):
# One query to get city_id, and one query to get the related City object.
self.assertEquals(self.c1.name, team_light.city.name)

def test_reverse_foreign_key_access(self):
city = City.objects.current.only('name').get(identity=self.c1.identity)
with self.assertNumQueries(2):
# One query to get the identity, one query to get the related objects.
self.assertSetEqual({self.team1.pk}, {o.pk for o in city.team_set.all()})

def test_many_to_many_access(self):
player1 = Player.objects.create(name='Raaaaaow', team=self.team1)
player2 = Player.objects.create(name='Pssshh', team=self.team1)
award1 = Award.objects.create(name='Fastest paws')
award1.players.add(player2)
award2 = Award.objects.create(name='Frighteningly fast')
award2.players.add(player1, player2)

player2_light = Player.objects.current.only('name').get(identity=player2.identity)
with self.assertNumQueries(1):
# Many-to-many fields use the id field, which is always fetched, so only one query
# should be made to get the related objects.
self.assertSetEqual({award1.pk, award2.pk}, {o.pk for o in player2_light.awards.all()})

# And from the other direction:
award2_light = Award.objects.current.only('name').get(identity=award2.identity)
with self.assertNumQueries(1):
self.assertSetEqual({player1.pk, player2.pk}, {o.pk for o in award2_light.players.all()})

0 comments on commit 6954acd

Please sign in to comment.