Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

We’re showing branches in this repository, but you can also compare across forks.

base fork: red56/django-immutablemodel
base: b7cc4d76af
...
head fork: red56/django-immutablemodel
compare: 5bfb3ce2ba
  • 14 commits
  • 7 files changed
  • 0 commit comments
  • 1 contributor
10 NEWS.txt
View
@@ -22,3 +22,13 @@ Release notes
---
* Version by Tim Diggins (red56) calling it django-immutablemodel and making immutability after save by default
+
+0.3.1
+---
+
+* Fixing problem with abstract models
+
+0.3.2
+---
+
+* Allowing for full inheritance of immutability options within Meta
3  TODO.txt
View
@@ -1 +1,2 @@
-Tests for admin stuff.
+Inheriting options from parent abstract classes?
+Tests for admin stuff.
2  immutablemodel/admin.py
View
@@ -13,7 +13,7 @@ def reload_obj():
immutable_lock_field_name = obj._meta.immutable_lock_field
if not getattr(reload_obj(), immutable_lock_field_name, False):
return self.readonly_fields + tuple([immutable_lock_field_name])
- return self.readonly_fields + tuple(obj._meta.immutable_fields)
+ return self.readonly_fields + tuple(obj._meta.immutable_admin_fields)
else:
return self.readonly_fields
100 immutablemodel/models.py
View
@@ -26,7 +26,12 @@ def get_default_for(self, model_class):
class CantDeleteImmutableException(Exception): pass
-class _Undefined: pass
+class __Undefined(object):
+ def __len__(self):
+ return False
+ def __repr__(self):
+ return u"__Undefined()"
+UNDEFINED = __Undefined()
class PK_FIELD: pass
@@ -46,36 +51,58 @@ def __new__(cls, name, bases, attrs):
if not parents:
# If this isn't a **sub**class of ImmutableMeta (ie. probably ImmutableModel itself), don't do anything special.
return super_new(cls, name, bases, attrs)
-
- immutability_options = ImmutableModelMeta.extract_options(attrs.get('Meta', {}))
+ if 'Meta' in attrs:
+ meta = attrs.get('Meta')
+ else:
+ meta = ImmutableModelMeta.meta_from_bases(bases)
+ immutability_options = ImmutableModelMeta.immutable_options_from_meta(meta)
+ if meta:
+ stripped = ImmutableModelMeta.strip_immutability_options(meta)
registered_model = models.base.ModelBase.__new__(cls, name, bases, attrs)
- ImmutableModelMeta.reinject_options(immutability_options, registered_model)
- ImmutableModelMeta.check_options(registered_model)
+ if meta:
+ ImmutableModelMeta.reattach_stripped(meta, stripped)
+ ImmutableModelMeta.check_and_reinject_options(immutability_options, registered_model)
return registered_model
+ @staticmethod
+ def meta_from_bases(bases):
+ for b in bases:
+ if issubclass(b, ImmutableModel) and b is not ImmutableModel:
+ return getattr(b, "Meta")
+
@staticmethod
- def extract_options(meta):
- if getattr(meta, "immutable", _Undefined) is not _Undefined:
- raise ValueError("immutable is not an option for ImmutableModels - use immutable_fields instead")
+ def immutable_options_from_meta(meta):
immutability_options = {}
for opt_name in IMMUTABLEFIELD_OPTIONS:
- value = getattr(meta, opt_name, _Undefined)
- if value is not _Undefined:
- delattr(meta, opt_name)
+ value = getattr(meta, opt_name, UNDEFINED)
immutability_options[opt_name] = value
return immutability_options
@staticmethod
- def reinject_options(immutability_options, registered_model):
+ def strip_immutability_options(meta):
+ if "immutable" in dir(meta):
+ raise ValueError("immutable is not an option for ImmutableModels - use immutable_fields instead")
+ stripped = {}
+ for opt_name in IMMUTABLEFIELD_OPTIONS:
+ if opt_name in meta.__dict__:
+ stripped[opt_name] = getattr(meta, opt_name)
+ delattr(meta, opt_name)
+ return stripped
+
+ @staticmethod
+ def reattach_stripped(meta, stripped):
+ for k,v in stripped.iteritems():
+ setattr(meta, k, v)
+
+ @staticmethod
+ def check_and_reinject_options(immutability_options, model):
for opt_name, value in immutability_options.iteritems():
- if value is _Undefined and getattr(registered_model._meta, opt_name, _Undefined) is _Undefined:
- #only want to use default when registered_model doesn't have a value yet
- value = IMMUTABLEFIELD_OPTIONS[opt_name].get_default_for(registered_model)
- if value is not _Undefined:
- setattr(registered_model._meta, opt_name, value)
+ if value is UNDEFINED and getattr(model._meta, opt_name, UNDEFINED) is UNDEFINED:
+ #only want to use default when registered_model doesn't have a value yet
+ value = IMMUTABLEFIELD_OPTIONS[opt_name].get_default_for(model)
+ if value is not UNDEFINED:
+ setattr(model._meta, opt_name, value)
- @staticmethod
- def check_options(model):
if not isinstance(model._meta.immutable_fields, list):
raise TypeError('immutable_fields attribute in %s must be '
'a list' % model)
@@ -83,24 +110,31 @@ def check_options(model):
raise TypeError('mutable_fields attribute in %s must be '
'a list' % model)
- if model._meta.mutable_fields and model._meta.immutable_fields:
- raise ValueError('You can specify either mutable_fields OR immutable_fields in %s (not both)' % model)
- if model._meta.immutable_fields:
- model._meta.mutable_fields = [f.name for f in model._meta.fields if f.name not in model._meta.immutable_fields]
+ if immutability_options['mutable_fields'] and immutability_options["immutable_fields"]:
+ we_found = ("We found:\n" +
+ ("mutable_fields: %s\n" % "mutable_fields")+
+ ("immutable_fields: %s\n" % immutability_options["immutable_fields"])
+ )
+ raise ValueError('You can specify either mutable_fields OR immutable_fields in %s (not both).\n%s' % (model, we_found))
- # we'll keep immutable_fields, but just as the reverse of mutable fields:
- model._meta.immutable_fields = [f.name for f in model._meta.fields if f.name not in model._meta.mutable_fields]
+ if immutability_options["immutable_fields"]:
+ model._meta.mutable_fields = [f.name for f in model._meta.fields if f.name not in immutability_options["immutable_fields"]]
+ # we'll make immutable_admin_fields as the reverse of mutable fields:
+ model._meta.immutable_admin_fields = [f.name for f in model._meta.fields if f.name not in model._meta.mutable_fields]
- if model._meta.immutable_lock_field is PK_FIELD:
- model._meta.immutable_lock_field = model._meta.pk.name
- elif (isinstance(model._meta.immutable_lock_field, basestring) or
- model._meta.immutable_lock_field is None
- ):
+ if model._meta.abstract:
+ # ignore immutable_lock_field in abstract models
pass
else:
- raise TypeError('immutable_lock_field attribute in '
- '%s must be a string (or None, or omitted)' % model)
-
+ if model._meta.immutable_lock_field is PK_FIELD:
+ model._meta.immutable_lock_field = model._meta.pk.name
+ elif (isinstance(model._meta.immutable_lock_field, basestring) or
+ model._meta.immutable_lock_field is None
+ ):
+ pass
+ else:
+ raise TypeError('immutable_lock_field attribute in '
+ '%s must be a string (or None, or omitted)' % model)
if not isinstance(model._meta.immutable_quiet, bool):
raise TypeError('immutable_quiet attribute in %s must '
4 setup.py
View
@@ -8,7 +8,7 @@
setup(
name = 'django-immutablemodel',
- version = '0.3',
+ version = '0.3.2',
description="A base class for Django to allow immutable fields on Models",
long_description=README + '\n\n' + NEWS,
classifiers=[
@@ -20,7 +20,7 @@
'Topic :: Database'
],
keywords='django model fields immutable frozen',
- author='Tim Diggins',
+ author='Rob Madole, Helder Silva, Tim Diggins',
author_email='tim@red56.co.uk',
url='https://github.com/red56/django-immutablemodel',
packages = [ 'immutablemodel' ],
71 tests/test_immutablemodel.py
View
@@ -61,6 +61,29 @@ def test__delete(self):
0,
len(SimpleNoSignOffField.objects.all()),
)
+class Case03_HavingMutableField_Test(TestCase):
+ def setUp(self):
+ self.obj = HavingMutableField.objects.create(
+ special_id=1,
+ name='Vader',
+ )
+
+ def test__simple(self):
+ self.assertEqual(self.obj.special_id, 1)
+ self.assertEqual(self.obj.name, 'Vader')
+
+ self.obj.special_id = 1000
+ self.obj.name = 'Luke'
+
+ self.obj.save()
+
+ db_object = HavingMutableField.objects.all()[0]
+ # Should stay the same, it's immutable. Except the name.
+ self.assertEqual(self.obj.special_id, 1)
+ self.assertEqual(self.obj.name, 'Luke')
+ self.assertEqual(db_object.special_id, 1)
+ self.assertEqual(db_object.name, 'Luke')
+
class Case03_CanCreateModelSignOffFieldTest(TestCase):
def setUp(self):
@@ -310,3 +333,51 @@ def test__delete_locked(self):
0,
len(NoisySignOffField.objects.all()),
)
+
+class Case07_InheritenceTests(TestCase):
+
+ def test01_defaults_work_for_abstract(self):
+ c = ChildModel(parent_field="parent", child_field="child")
+ c.save()
+ c.child_field="other"
+ c.parent_field="other"
+ c.save()
+ db_object = ChildModel.objects.all()[0]
+ for t, name in [(c, 'c'), (db_object, 'db_object')]:
+ self.assertEqual(t.child_field, "child", "expecting %s.child_field" % name)
+ self.assertEqual(t.parent_field, "parent", "expecting %s.parent_field" % name)
+
+ def test02_can_inherit_attributes(self):
+ c = InheritingModel(child_field="child", mutable_field="whatever", special_id=1)
+ c.save()
+ c.child_field="other"
+ c.special_id=47
+ c.mutable_field="other"
+ c.save()
+ db_object = InheritingModel.objects.get(pk=c.id)
+ for t, name in [(c, 'c'), (db_object, 'db_object')]:
+ self.assertEqual(t.child_field, "child", "expecting %s.child_field" % name)
+ self.assertEqual(t.special_id, 1, "expecting %s.special_id" % name)
+ self.assertEqual(t.mutable_field, "other", "expecting %s.mutable_field" % name)
+
+ def test03_can_inherit_attributes_from_inherited_meta(self):
+ self.assertEqual(['mutable_field'], AbstractModelWithAttrs._meta.mutable_fields)
+ self.assertEqual(['mutable_field'], NoisyAbstractModelWithAttrs._meta.mutable_fields)
+
+ def test04_can_inherit_attributes_repeatedly(self):
+ c = NoisyInheritingModel(child_field="child", mutable_field="whatever", special_id=1)
+ c.save()
+ def changeme():
+ c.child_field="other"
+ c.special_id=47
+ self.assertRaises(Exception, changeme,
+ 'expecting exception to be raised because immutable quiet should be inherited'
+ )
+ c.mutable_field="other"
+ c.save()
+ db_object = NoisyInheritingModel.objects.all()[0]
+ for t, name in [(c, 'c'), (db_object, 'db_object')]:
+ self.assertEqual(t.child_field, "child", "expecting %s.child_field" % name)
+ self.assertEqual(t.special_id, 1, "expecting %s.special_id" % name)
+ self.assertEqual(t.mutable_field, "other", "expecting %s.mutable_field" % name)
+
40 tests/testapp/models.py
View
@@ -4,6 +4,15 @@
class NoMeta(ImmutableModel):
name = models.CharField(max_length=50)
+
+class HavingMutableField(ImmutableModel):
+ special_id = models.IntegerField()
+ name = models.CharField(max_length=50)
+
+ class Meta:
+ mutable_fields = ['name']
+
+
class SimpleNoSignOffField(ImmutableModel):
special_id = models.IntegerField()
name = models.CharField(max_length=50)
@@ -65,3 +74,34 @@ class Meta:
immutable_quiet = True
immutable_is_deletable = False
+
+class AbstractModel(ImmutableModel):
+ parent_field = models.CharField(max_length=50)
+
+ class Meta:
+ abstract = True
+
+class ChildModel(AbstractModel):
+ child_field = models.CharField(max_length=50)
+
+
+class AbstractModelWithAttrs(ImmutableModel):
+ mutable_field = models.CharField(max_length=50)
+ special_id = models.PositiveIntegerField()
+
+ class Meta:
+ abstract = True
+ mutable_fields = ['mutable_field']
+ immutable_is_deletable = False
+
+class InheritingModel(AbstractModelWithAttrs):
+ child_field = models.CharField(max_length=50)
+
+class NoisyAbstractModelWithAttrs(AbstractModelWithAttrs):
+ class Meta(AbstractModelWithAttrs.Meta):
+ immutable_quiet = False
+ abstract = True
+
+class NoisyInheritingModel(NoisyAbstractModelWithAttrs):
+ child_field = models.CharField(max_length=50)
+

No commit comments for this range

Something went wrong with that request. Please try again.