Ivan Zakrevsky avatar Ivan Zakrevsky committed 4aa3ba1

Added ForeignKey supports, some improvements, added tests, empty diffs not saved for now...

Comments (0)

Files changed (5)

versioning/models.py

 
 from . import _registry
 from .managers import RevisionManager
-from .utils import dmp, diff_split_by_fields
+from .utils import dmp, diff_split_by_fields, get_field_data
 
 try:
     str = unicode  # Python 2.* compatible
         for changeset in next_changes:
             diffs = diff_split_by_fields(changeset.delta)
             for key, diff in diffs.items():
-                model2, field = key.split('.')
-                if model2 != model.__name__ or field not in fields:
+                model_name, field_name = key.split('.')
+                if model_name != model.__name__ or field_name not in fields:
                     continue
-                content = force_unicode(getattr(content_object, field))
+                content = get_field_data(content_object, field_name)
                 patch = dmp.patch_fromText(diff)
                 content = dmp.patch_apply(patch, content)[0]
-                fobj = content_object._meta.get_field(field)
-                if content == 'None' and fobj.null:
-                    content = None
-                if fobj.get_internal_type() in ('BooleanField',
-                                                'NullBooleanField', ):
+                fobj = content_object._meta.get_field(field_name)
+                if content == 'None':
+                    if fobj.null:
+                        content = None
+                    else:
+                        continue
+                elif fobj.get_internal_type() in ('BooleanField',
+                                                  'NullBooleanField', ):
                     if content == 'True':
                         content = True
                     elif content == 'False':
                         content = False
-                content = fobj.to_python(content)
-                setattr(content_object, field, content)
+                elif fobj.get_internal_type() == 'ForeignKey':
+                    rel_model = fobj.rel.to
+                    try:
+                        #content = rel_model._meta.pk.to_python(content)
+                        content = rel_model.objects.get(pk=content)
+                    except rel_model.DoesNotExist:
+                        continue
+                else:
+                    content = fobj.to_python(content)
+                setattr(content_object, field_name, content)
             changeset.reverted = True
             changeset.save()
 
                 # after the change
                 next_rev = copy.copy(old)
             for key, diff in diffs.items():
-                model2, field = key.split('.')
-                if model2 != model.__name__ or field not in fields:
+                model_name, field_name = key.split('.')
+                if model_name != model.__name__ or field_name not in fields:
                     continue
                 patches = dmp.patch_fromText(diff)
-                setattr(old, field,
-                        dmp.patch_apply(patches,
-                                        force_unicode(getattr(old, field)))[0])
+                setattr(
+                    old,
+                    field_name,
+                    dmp.patch_apply(
+                        patches,
+                        get_field_data(old, field_name)
+                    )[0]
+                )
 
         result = []
-        for field in fields:
-            result.append("<b>{0}</b>".format(field))
-            diffs = dmp.diff_main(force_unicode(getattr(old, field)),
-                                  force_unicode(getattr(next_rev, field)))
+        for field_name in fields:
+            result.append("<b>{0}</b>".format(field_name))
+            diffs = dmp.diff_main(
+                get_field_data(old, field_name),
+                get_field_data(next_rev, field_name)
+            )
             result.append(dmp.diff_prettyHtml(diffs))
         return "<br />\n".join(result)
 

versioning/signals.py

 from __future__ import absolute_import, unicode_literals
 from .middleware import get_request
 from .models import Revision
-from .utils import obj_diff
+from .utils import obj_diff, obj_is_changed
 
 
 def pre_save(instance, **kwargs):
         original = model._default_manager.get(pk=instance.pk)
     except model.DoesNotExist:
         original = model()
+
+    if not obj_is_changed(instance, original):
+        instance.revision_info = {}
+        return
+
     info['delta'] = obj_diff(instance, original)
     request = get_request()
     if request:

versioning/tests.py

+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+from django.db import models
+from django.contrib.auth.models import User
+from django.test import TestCase
+
+import versioning
+from .models import Revision
+
+
+class TestFkModel(models.Model):
+    attr_text = models.TextField(blank=True)
+
+    class Meta:
+        db_table = 'versioning_testfkmodel'
+
+
+class TestModel(models.Model):
+    attr_text = models.TextField(blank=True)
+    attr_bool = models.NullBooleanField(blank=True)
+    attr_int = models.IntegerField(blank=True, null=True)
+    attr_fk = models.ForeignKey(TestFkModel, blank=True, null=True)
+
+    class Meta:
+        db_table = 'versioning_testmodel'
+
+versioning.register(
+    TestModel,
+    ['attr_text', 'attr_int', 'attr_bool', 'attr_fk', ]
+)
+
+
+class VersioningForAdminTest(TestCase):
+
+    def setUp(self):
+        self.admin = User.objects.create_superuser(
+            username='admin',
+            email="admin@mailinator.com",
+            password="adminpwd"
+        )
+        response = self.client.login(username='admin', password='adminpwd')
+        self.assertTrue(response)
+
+    def test_diffs(self):
+        obj_fk_1 = TestFkModel.objects.create(
+            attr_text="техт",
+        )
+        obj_fk_2 = TestFkModel.objects.create(
+            attr_text="техт",
+        )
+        obj_1 = TestModel(
+            attr_text="строка первая\nстрока вторая\nстрока третья",
+            attr_fk=obj_fk_1,
+            attr_int=1
+        )
+        obj_1.revision_info = {
+            'editor': self.admin,
+            'comment': 'comment 1',
+        }
+        obj_1.save()
+        self.assertEqual(Revision.objects.get_for_object(obj_1).count(), 1)
+
+        obj_1.save()
+        self.assertEqual(Revision.objects.get_for_object(obj_1).count(), 1)
+
+        obj_2 = TestModel.objects.get(pk=obj_1.pk)
+        obj_2.attr_text = "строка первая\nстрока измененная вторая\nстрока третья"
+        obj_2.attr_bool = True
+        obj_2.revision_info = {
+            'editor': self.admin,
+            'comment': 'comment 1',
+        }
+        obj_2.save()
+        self.assertEqual(Revision.objects.get_for_object(obj_1).count(), 2)
+
+        obj_3 = TestModel.objects.get(pk=obj_1.pk)
+        obj_3.attr_text = "строка первая\nстрока измененная снова вторая\nстрока третья"
+        obj_3.attr_bool = False
+        obj_3.attr_int = 3
+        obj_3.attr_fk = obj_fk_2
+        obj_3.revision_info = {
+            'editor': self.admin,
+            'comment': 'comment 1',
+        }
+        obj_3.save()
+        self.assertEqual(Revision.objects.get_for_object(obj_1).count(), 3)
+
+        rev_1 = Revision.objects.get_for_object(obj_1).order_by('pk')[0]
+        self.assertEqual(rev_1.revision, 1)
+        rev_1.reapply()
+
+        obj_4 = TestModel.objects.get(pk=obj_1.pk)
+        self.assertEqual(obj_4.attr_text, obj_1.attr_text)
+        self.assertEqual(obj_4.attr_bool, obj_1.attr_bool)
+        self.assertEqual(obj_4.attr_fk, obj_1.attr_fk)
+        self.assertEqual(Revision.objects.get_for_object(obj_1).count(), 4)

versioning/utils.py

 from __future__ import absolute_import, unicode_literals
 import sys
 from difflib import SequenceMatcher
+from django.db import models
 from django.utils.encoding import force_unicode
 
 #from django.utils.encoding import smart_unicode
     return dmp.patch_toText(patch)
 
 
+def get_field_data(obj, field):
+    """Returns field's data"""
+    data = getattr(obj, field)
+    if isinstance(data, models.Model):
+        data = getattr(data, 'pk', None)
+    return force_unicode(data)
+
+
 def obj_diff(obj1, obj2):
     """Create a 'diff' from obj1 to obj2."""
     model = obj1.__class__
     fields = _registry[model]
     lines = []
     for field in fields:
-        original_data = force_unicode(getattr(obj2, field))
-        new_data = force_unicode(getattr(obj1, field))
+        original_data = get_field_data(obj2, field)
+        new_data = get_field_data(obj1, field)
         #data_diff = unified_diff(original_data.splitlines(),
         #                         new_data.splitlines(), context=3)
         data_diff = diff(new_data, original_data)
     return "\n".join(lines)
 
 
+def obj_is_changed(obj1, obj2):
+    """Returns True, if watched attributes of obj1 deffer from obj2."""
+    model = obj1.__class__
+    fields = _registry[model]
+    for field in fields:
+        original_data = get_field_data(obj2, field)
+        new_data = get_field_data(obj1, field)
+        if original_data != new_data:
+            return True
+    return False
+
+
 def diff_split_by_fields(txt):
     """Returns dictionary object, key is fieldname, value is it's diff"""
     result = {}

versioning/views.py

 
     def get_queryset(self):
         """Returns queryset for current request. Also, check permissions."""
-        content_type=self.kwargs.get('content_type', None)
-        object_id=self.kwargs.get('object_id', None)
+        content_type = self.kwargs.get('content_type', None)
+        object_id = self.kwargs.get('object_id', None)
         content_type = get_object_or_404(ContentType, pk=content_type)
         try:
             content_obj = content_type.get_object_for_this_type(pk=object_id)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.