Keeping track of changes since the last save in django models - python

A couple of times I've run into a situation, when at save time I need to know which model fields are going to be updated and act accordingly.
The most obvious solution to this is to take the primary key field and retrieve a copy of the model from the database:
class MyModel(models.Model):
def save(self, force_insert=False, force_update=False, using=None):
if self.id is not None:
unsaved_copy = MyModel.objects.get(id=self.id)
# Do your comparisons here
super(MyModel, self).save(force_insert, force_update, using)
That works perfectly fine, however, it hits the database for every instance of the model you are saving (might be quite inconvenient if you are doing a lot of such saves).
It is obvious, that if one can "remember" the old field values at the start of model instance's lifetime (__init__), there should be no need to retrieve a copy of the model from the database. So I came up with this little hack:
class MyModel(models.Model):
def __init__(self, *args, **kwargs):
super(MyModel, self).__init__(*args, **kwargs)
self.unsaved = {}
for field in self._meta.fields:
self.unsaved[field.name] = getattr(self, field.name, None)
def save(self, force_insert=False, force_update=False, using=None):
for name, value in self.unsaved.iteritems():
print "Field:%s Old:%s New:%s" % (name, value, getattr(self, name, None))
# old values can be accessed through the self.unsaved member
super(MyModel, self).save(force_insert, force_update, using)
This seems to work, however it makes use of the non-public interface of django.db.models.Model.
Perhaps someone knows a cleaner way to do it?

I think your solution looks reasonable.
Alternatively you could have a Manager method called get_and_copy() (or something) that hung a copy of the original object off of what is returned. You could then use another Manager method, save_and_check() which took advantage of the copied original.
FWIW: If you are playing with contrib/admin templates there is a context variable called original which is a copy of the original object.
Update: I looked more closely at what admin is doing. In class ModelAdmin (located in django/contrib/admin/options.py) there is a method called construct_change_message(). It is being driven by formset.changed_data and formset.changed_objects, so django/forms/models.py class BaseModelFormSet is where the action is. See the method save_existing_objects(). Also look at the method _existing_object(). It's a little more complicated than what I mentioned before because they are dealing with the possibility of multiple objects, but they are basically caching the results of the query set on first access.

This will not work for fixtures. loaddata command uses models.Model.base_save. Probably the cleanest method would be to use descriptors for fields, but one has to figure out how to inset them properly.

Related

Django ModelChoiceField choices not updating

Well, I'm stuck in that problem for quite long now.
Went to read some question / answers and blog, and at this point I don't understand why this is not working.
I'm gonna make my example as simple as possible.
Let's say I have a ModelMultipleChoiceField :
myfield = ModelMultipleChoiceField(
queryset=SomeObject.objects.none(),
label='',
widget=forms.CheckboxSelectMultiple(
attrs={
'class': 'mtlz-checkbox-group',
'label': 'some label: '
}
),
required=False
)
I set my queryset to none cause I need to compute the result dynamically. Note that this is in a ModelForm and that this field is a field of my object that I needed to custom (with some custom widget).
Well now i'm changing the queryset in the __init__() method :
def __init__(self, *args, **kwargs):
super(EquipeForm, self).__init__(*args, **kwargs)
self.base_fields['myfield'].queryset = self.method()
Here self.method() is a method that's computing my queryset, and it's working fine.
So, whatever, the choices were not getting updated except when I refresh (just pressing f5, not cache and stuff). Continuing my reading, I read that self.base_fields['myfield'].widget.choices were cached and so I had to force the "refresh" too in my init :
def __init__(self, *args, **kwargs):
super(EquipeForm, self).__init__(*args, **kwargs)
self.base_fields['myfield'].queryset = self.method()
self.base_fields['myfield'].widget.choices = self.base_fields['myfield'].choices
Using a pdb, I saw the choices were updated, and looks like the widget choices too. But still, when I first come on my form, the last choices are displayed and seemed to be cache. If I just press f5, again, it's now the right choices displayed.
In a last try I declared the all field in the __init__() method but it's just the same.
So what am I missing? Is there any other cache involved as my choices seem to change in my __init__() but are always one turn late ?
Does that come from my custom widget (which herit from a normal widget) ?
For information, it's on django 1.11.
EDIT:
the self.method():
def method(self):
ids = []
if not self.instance.attribute:
for obj in SomeObject.objects.exclude(id=self.instance.id):
ids += obj.members.all().filter(
some_condiftion=False
).values_list('id', flat=True)
return SomeOtherObject.objects.filter(is_superuser=False) \
.exclude(id__in=ids).order_by('name')
SomeObject.members is a manytomany fields related to SomeOtherObject. That's why I have a ModelMultipleChoiceField.
Thanks in advance for your help
The problem is that you are updating self.base_fields, which is the fields dict of the class, rather than self.fields, which is the copy on the instance.
Since fields is already created by the time you update base_fields, it uses the old version of the choices; the next time you render the page, it will use the version created this time.

How do I easily create a calculated field in a Django model?

No, I don't want a property. I really don't. What I want is what I asked.
Due to subclassing requirements, I'm looking for a way to generate one field from a set of two others and store this computation in the database. Not a property in Python, not an SQL calculation, a pre-calculated field that is updated on save and stored, as is, in the database.
For example:
class Help(models.Model):
title = models.TextField()
body = models.TextField()
class SoftwareHelp(Help):
about_software = models.ForeignKey('Software')
Regardless of what a user enters in the title field, I want it to say "Help for " once save is clicked. In reality, the code has more fields, but this explains the principle.
I know its possible to do this by overriding the save() method, but wanted to make sure I wasn't saving to the database twice, and want to know if there is another better way.
I think the easiest way is to override the save method. I don't see any reason why it should save to the database twice.
class SoftwareHelp(Help):
about_software = models.ForeignKey('Software')
def save(self, *args, **kwargs):
self.about_software = 'Help for %s' % self.title
return super(SoftwareHelp, self).save(*args, **kwargs)

Dynamically add properties to a django model

I have a Django model where a lot of fields are choices. So I had to write a lot of "is_something" properties of the class to check whether the instance value is equal to some choice value. Something along the lines of:
class MyModel(models.Model):
some_choicefield = models.IntegerField(choices=SOME_CHOICES)
#property
def is_some_value(self):
return self.some_choicefield == SOME_CHOICES.SOME_CHOICE_VALUE
# a lot of these...
In order to automate this and spare me a lot of redundant code, I thought about patching the instance at creation, with a function that adds a bunch of methods that do the checks.
The code became as follows (I'm assuming there's a "normalize" function that makes the label of the choice a usable function name):
def dynamic_add_checks(instance, field):
if hasattr(field, 'choices'):
choices = getattr(field, 'choices')
for (value,label) in choices:
def fun(instance):
return getattr(instance, field.name) == value
normalized_func_name = "is_%s_%s" % (field.name, normalize(label))
setattr(instance, normalized_func_name, fun(instance))
class MyModel(models.Model):
def __init__(self, *args, **kwargs):
super(MyModel).__init__(*args, **kwargs)
dynamic_add_checks(self, self._meta.get_field('some_choicefield')
some_choicefield = models.IntegerField(choices=SOME_CHOICES)
Now, this works but I have the feeling there is a better way to do it. Perhaps at class creation time (with metaclasses or in the new method)? Do you have any thoughts/suggestions about that?
Well I am not sure how to do this in your way, but in such cases I think the way to go is to simply create a new model, where you keep your choices, and change the field to ForeignKey. This is simpler to code and manage.
You can find a lot of information at a basic level in Django docs: Models: Relationships. In there, there are many links to follow expanding on various topics. Beyong that, I believe it just needs a bit of imagination, and maybe trial and error in the beginning.
I came across a similar problem where I needed to write large number of properties at runtime to provide backward compatibility while changing model fields. There are 2 standard ways to handle this -
First is to use a custom metaclass in your models, which inherits from models default metaclass.
Second, is to use class decorators. Class decorators sometimes provides an easy alternative to metaclasses, unless you have to do something before the creation of class, in which case you have to go with metaclasses.
I bet you know Django fields with choices provided will automatically have a display function.
Say you have a field defined like this:
category = models.SmallIntegerField(choices=CHOICES)
You can simply call a function called get_category_display() to access the display value. Here is the Django source code of this feature:
https://github.com/django/django/blob/baff4dd37dabfef1ff939513fa45124382b57bf8/django/db/models/base.py#L962
https://github.com/django/django/blob/baff4dd37dabfef1ff939513fa45124382b57bf8/django/db/models/fields/init.py#L704
So we can follow this approach to achieve our dynamically set property goal.
Here is my scenario, a little bit different from yours but down to the end it's the same:
I have two classes, Course and Lesson, class Lesson has a ForeignKey field of Course, and I want to add a property name cached_course to class Lesson which will try to get Course from cache first, and fallback to database if cache misses:
Here is a typical solution:
from django.db import models
class Course(models.Model):
# some fields
class Lesson(models.Model):
course = models.ForeignKey(Course)
#property
def cached_course(self):
key = key_func()
course = cache.get(key)
if not course:
course = get_model_from_db()
cache.set(key, course)
return course
Turns out I have so many ForeignKey fields to cache, so here is the code following the similar approach of Django get_FIELD_display feature:
from django.db import models
from django.utils.functional import curry
class CachedForeignKeyField(models.ForeignKey):
def contribute_to_class(self, cls, name, **kwargs):
super(models.ForeignKey, self).contribute_to_class(cls, name, **kwargs)
setattr(cls, "cached_%s" % self.name,
property(curry(cls._cached_FIELD, field=self)))
class BaseModel(models.Model):
def _cached_FIELD(self, field):
value = getattr(self, field.attname)
Model = field.related_model
return cache.get_model(Model, pk=value)
class Meta:
abstract = True
class Course(BaseModel):
# some fields
class Lesson(BaseModel):
course = CachedForeignKeyField(Course)
By customizing CachedForeignKeyField, and overwrite the contribute_to_class method, along with BaseModel class with a _cached_FIELD method, every CachedForeignKeyField will automatically have a cached_FIELD property accordingly.
Too good to be true, bravo!

Transparently storing Django model field as JSON data

Say I have an object, "Order," a field of which, "items," holds a list of order items. The list of items will never be searched or individually selected in the database so I just want to store it in a DB field as a JSON string.
I'm trying to figure out the best way to embed this functionality so it's fairly transparent to anyone using the model. I think saving the model is pretty easy - just override the save method and serialize the "items" list into an internal "_items" field, and then write that to the db. I'm confused about how to deserialize, though. Having looked into possibly some kind of classmethod for creation, or creating a custom manger, or something to do with signals, I've thoroughly confused myself. I'm sure this has been solved a hundred times over and I'm curious what people consider to be best practice.
Example classes:
class OrderItem():
def __init__(self, desc="", qty=0):
self.desc = desc
self.qty = qty
class Order(Model):
user = ForeignKey(User)
_items = TextField()
def save(self, *args, **kwargs):
self._items = jsonpickle.encode(self.items)
super(Order, self).save(*args, **kwargs)
Example usage:
order = Order()
order.items = [OrderItem("widget", 5)]
order.save()
This would create a record in the DB in which
_items = [{"desc":"widget", "qty":5}]
Now I want to be able to later select the object
order = Order.objects.get(id=whatever)
and have order.items be the unpacked array of items, not the stored JSON string.
EDIT:
The solution turned out to be quite simple, and I'm posting here in case it helps any other newbies. Based on Daniel's suggestion, I went with this custom model field:
class JSONField(with_metaclass(SubfieldBase, TextField)):
def db_type(self, connection):
return 'JSONField'
def to_python(self, value):
if isinstance(value, basestring):
return jsonpickle.decode(value)
else:
return value
def get_prep_value(self, value):
return jsonpickle.encode(value)
A much better approach is to subclass TextField and override the relevant methods to do the serialization/deserialization transparently as required. In fact there are a number of implementations of this already: here's one, for example.

Django remove bulk-delete

This is a very simple question: Is there any good way to disable calling a bulk-delete (through querysets of course) on all models in an entire Django project?
The reasoning for this is under the premise that completely deleting data is almost always a poor choice, and an accidental bulk-delete can be detrimental.
Like comments says on your first post, you have to create a subclass for each of these elements:
The model manager
Queryset class
BaseModel
After some search, a great example can be found here, all credits to Akshay Shah, the blog author. Before looking to the code, be aware of that:
However, it inevitably leads to data corruption. The problem is simple: using a Boolean to store deletion status makes it impossible to enforce uniqueness constraints in your database.
from django.db import models
from django.db.models.query import QuerySet
class SoftDeletionQuerySet(QuerySet):
def delete(self):
# Bulk delete bypasses individual objects' delete methods.
return super(SoftDeletionQuerySet, self).update(alive=False)
def hard_delete(self):
return super(SoftDeletionQuerySet, self).delete()
def alive(self):
return self.filter(alive=True)
def dead(self):
return self.exclude(alive=True)
class SoftDeletionManager(models.Manager):
def __init__(self, *args, **kwargs):
self.alive_only = kwargs.pop('alive_only', True)
super(SoftDeletionManager, self).__init__(*args, **kwargs)
def get_queryset(self):
if self.alive_only:
return SoftDeletionQuerySet(self.model).filter(alive=True)
return SoftDeletionQuerySet(self.model)
def hard_delete(self):
return self.get_queryset().hard_delete()
class SoftDeletionModel(models.Model):
alive = models.BooleanField(default=True)
objects = SoftDeletionManager()
all_objects = SoftDeletionManager(alive_only=False)
class Meta:
abstract = True
def delete(self):
self.alive = False
self.save()
def hard_delete(self):
super(SoftDeletionModel, self).delete()
Basically, it adds an alive field to check if the row has been deleted or not, and update it when the delete() method is called.
Of course, this method works only on project where you can manipulate the code base.
There are nice off-the-shelf applications that allow for restoring deleted models (if that is what you are interested in), here are ones I used:
Django softdelete: https://github.com/scoursen/django-softdelete I used it more
Django reversion: https://github.com/etianen/django-reversion this one is updated more often, and allows you to revert to any version of your model (not only after delete, but as well after update).
If you really want to forbid bulk deletes, I'd discourage you from this approach as it will:
Break expectations about applicaiton behaviour. If I call MyModel.objects.all().delete() I want table to be empty afterwards.
Break existing applications.
If you want do do it please follow advice from comment:
I'm guessing this would involve subclassing QuerySet and changing the delete method to your liking, subclassing the default manager and have it use your custom query set, subclassing model - create an abstract model and have it use your custom manager and then finally have all your models subclass your custom abstract model.

Categories

Resources