Reputation: 383
In Django, I'm trying to create a base model that can be used to track the different version of some other model's data in a transparent way. Something like:
class Warehouse(models.Model):
version_number = models.IntegerField()
objects = CustomModelManagerFilteringOnVersionNumber()
class Meta:
abstract=True
def save(self, *args, **kwargs):
# handling here version number incrementation
# never overwrite data but create separate records,
# don't delete but mark as deleted, etc.
pass
class SomeData(Warehouse):
id = models.IntegerField( primary_key=True )
name = models.CharField(unique=True)
The problem I have is that SomeData.name
is actually not unique, the tuple ('version_number', 'name' )
is.
I know I can use the Meta
class in SomeData
with unique_together
but I was wondering whether this could be done in a more transparent way. That is, dynamically modifying/creating this unique_together
field.
Final note: maybe handling this with model inheritance is not the correct approach but it looked pretty appealing to me if I can handle this field uniqueness problem.
EDIT: Obviously the word 'dynamic' in this context is misleading. I do want the uniqueness to be preserved at the database level. The idea is to have different version of the data.
A give here a small example with the above model (assuming an abstract model inheritance, so that everything is in the same table):
The database could contain:
version_number | id | name | #comment 0 | 1 | bob | first insert 0 | 2 | nicky | first insert 1 | 1 | bobby | bob changed is name on day x.y.z 0 | 3 | malcom| first insert 1 | 3 | malco | name change 2 | 3 | momo | again...
and my custom model manager would filter the data (taking the max on version number for every unique id) so that: SomeData.objects.all() would return
id | name 1 | bobby 2 | nicky 3 | momo
and would also offer others method that can return data as in version n-1.
Obviously I will use a timestamp instead of a version number so that I can retrieve the data at a given date but the principle remains the same.
Now the problem is that when I do ./manage.py makemigrations with the models above, it will enforce uniqueness on SomeData.name and on SomeData.id when what I need is uniqueness on ('SomeData.id','version_number') and ('SomeData.name', 'version_number'). Explicitly, I need to append some fields from the base models into the fields of the inherited models declared as unique in the db and this any times the manage.py command is run and/or the production server runs.
Upvotes: 3
Views: 1525
Reputation: 556
Just to add to @Bob's answer you may need to use the __metaclass__
class as following
class ModelBaseMeta(BaseModel):
def __new__(cls, name, bases, attrs):
...
...
class YourAbstractClass(models.Model, metaclass=ModelBaseMeta):
class Meta:
abstract = True
Instead of putting the __metaclass__
definition directly inside the abstract class.
Upvotes: 0
Reputation: 383
Ok, I ended subclassing the ModelBase from django.db.models.base so that I can insert into 'unique_together' any fields declared as unique. Here is my current code. It does not yet implements the manager and save methods but the db uniqueness constraints are handled correctly.
from django.db.models.options import normalize_together
from django.db.models.base import ModelBase
from django.db.models.fields import Field
class WarehouseManager( models.Manager ):
def get_queryset( self ):
"""Default queryset is filtered to reflect the current status of the db."""
qs = super( WarehouseManager, self ).\
get_queryset().\
filter( wh_version = 0 )
class WarehouseModel( models.Model ):
class Meta:
abstract = True
class __metaclass__(ModelBase):
def __new__(cls, name, bases, attrs):
super_new = ModelBase.__new__
meta = attrs.get( 'Meta', None )
try:
if attrs['Meta'].abstract == True:
return super_new(cls, name, bases, attrs )
except:
pass
if meta is not None:
ut = getattr( meta, 'unique_together', () )
ut = normalize_together( ut )
attrs['Meta'].unique_together = tuple( k+('wh_version',) for k in ut )
unique_togethers = ()
for fname,f in attrs.items():
if fname.startswith( 'wh_' ) or not isinstance( f, Field ):
continue
if f.primary_key:
if not isinstance( f, models.AutoField ):
raise AttributeError( "Warehouse inherited models cannot "
"define a primary_key=True field which is not an "
"django.db.models.AutoField. Use unique=True instead." )
continue
if f.unique:
f._unique = False
unique_togethers += ( (fname,'wh_version'), )
if unique_togethers:
if 'Meta' in attrs:
attrs['Meta'].unique_together += unique_togethers
else:
class DummyMeta: pass
attrs['Meta'] = DummyMeta
attrs['Meta'].unique_together = unique_togethers
return super_new(cls, name, bases, attrs )
wh_date = models.DateTimeField(
editable=False,
auto_now=True,
db_index=True
)
wh_version = models.IntegerField(
editable=False,
default=0,
db_index=True,
)
def save( self, *args, **kwargs ):
# handles here special save behavior...
return super( WarehouseModel, self ).save( *args, **kwargs )
objects = WarehouseManager()
class SomeData( WarehouseModel ):
pid = models.IntegerField( unique=True )
class SomeOtherData( WarehouseModel ):
class Meta:
unique_together = ('pid','chars')
pid = models.IntegerField()
chars = models.CharField()
Upvotes: 3
Reputation: 13763
By "dynamically modifying/creating this unique_together
field", I assume you mean you want to enforce this at the code level, not at the database level.
You can do it in the save()
method of your model:
from django.core.exceptions import ValidationError
def save(self, *args, **kwargs):
if SomeData.objects.filter(name=self.name, version_number=self.version_number).exclude(pk=self.pk).exists():
raise ValidationError('Such thing exists')
return super(SomeData, self).save(*args, **kwargs)
Hope it helps!
Upvotes: 2