How to override save method to change data in Django? - django

Model:
ATTN_TYPE_CHOICES = (
('N', 'Entry'),
('X', 'Exit'),
('L', 'Leave'),
)
class Attn(Timestamp):
emp_id = models.CharField(
max_length=10
)
date = models.DateField()
time = models.TimeField(
default=time(00, 00)
)
type = models.CharField(
max_length=1,
choices=ATTN_TYPE_CHOICES,
default='N'
)
#property
def late(self):
return type == 'N' and self.time > LATE_LIMIT
def save(self, *args, **kwargs):
try:
Attn.objects.get(emp_id=self.emp_id, date=self.date, type='N')
except Attn.DoesNotExist:
pass
else:
try:
exit = Attn.objects.get(emp_id=self.emp_id, date=self.date, type='X')
except Attn.DoesNotExist:
self.type = 'X'
else:
exit.delete()
super(Attn, self).save(*args, **kwargs)
class Meta:
unique_together = ('emp_id', 'date', 'type')
I will create objects thrice. The first time is simple. The type will be N. The second time I want the save method to check if type N already exists, if it does, then change the type to 'X' and save second object. Third time, I want it to check for N and then for X. But this time it will find X and will delete the existing entry for X before saving the new entry with type X.
For some reason, the code seems to get stuck at the unique_together and doesn't let me save data from the admin panel. Should I try and catch the Integrityerror for this problem?

Try editing the save method like this,
def save(self, *args, **kwargs):
try:
Attn.objects.get(emp_id=self.emp_id, date=self.date, type='N')
try:
exit = Attn.objects.get(emp_id=self.emp_id, date=self.date, type=='X')
exit.delete()
except Attn.DoesNotExist:
self.type = 'X'
else:
self.type = 'X'
except Attn.DoesNotExist:
self.type = 'N'
return super(Attn, self).save(*args, **kwargs)
Remove the unique_together constraint, its not needed now, you are explicitly overriding the save method and restricting the app to save objects with the conditions above..
EDIT
From the docs,
The ValidationError raised during model validation when the constraint is violated has the unique_together error code.
That means, if the unique_together constraint is violated then, the ValidationError is raised in the model validation itself. Django never even try to reach near the save method, if the constraint is failed. Thus, django-admin raises error before committing the object to the database.

Related

How to Set DateTimeFields in Django Models to None Value (If current time is greater than this fields with auto detect)

Below is my codes
class Job(models.Model):
pin_till = models.DateTimeField("Pin Job Post", null=True,blank=True)
Here i wanna auto setup pin_till_date field to None if date is not None and than pin_till_date < now(). Depend on current time and auto check this field to None.
How to archives this method ah? with #property function, signals or just define in save function?
You can Override Save()
class Job(models.Model):
def save(self, *args, **kwargs):
if self.pin_till < now(): # Or what ever be the condition
# Use your conditon and update
self.pin_till = None
super(Job, self).save(*args, **kwargs) #Save the modified value

How to order by variable field?

I create a custom MultilingualCharField and I want order the instances by it, in the right language. I prefer to do so in the model (tell me if this wasn't a good idea), is it possible?
class Myclass(models.Model):
name = MultilingualCharField(max_length=32, unique=True)
...
def __str__(self):
name_traslated={'name_it': self.name_it, 'name_en': self.name_en}
name_verbose=_('name_it')
return name_traslated[name_verbose]
class Meta:
#name_traslated={'name_it': self.name_it, 'name_en': self.name_en}
name_verbose=_('name_it')
ordering = [name_verbose]
#ordering = [name_traslated[name_verbose]]
__str__ is working but ordering is not: it gives TypeError: 'class Meta' got invalid attribute(s): name_verbose
My MultilingualCharField create two columns: name_it and name_en and I want to order the istances on one of these. If you need it here's the code (from Web Development with Django Cookbook):
class MultilingualCharField(models.CharField):
def __init__(self, verbose_name=None, **kwargs):
self._blank = kwargs.get("blank", False)
self._editable = kwargs.get("editable", True)
#super(MultilingualCharField, self).__init__(verbose_name, **kwargs)
super().__init__(verbose_name, **kwargs)
def contribute_to_class(self, cls, name, virtual_only=False):
# generate language specific fields dynamically
if not cls._meta.abstract:
for lang_code, lang_name in settings.LANGUAGES:
if lang_code == settings.LANGUAGE_CODE:
_blank = self._blank
else:
_blank = True
localized_field = models.CharField(string_concat(
self.verbose_name, " (%s)" % lang_code),
name=self.name,
primary_key=self.primary_key,
max_length=self.max_length,
unique=self.unique,
blank=_blank,
null=False,
# we ignore the null argument!
db_index=self.db_index,
rel=self.rel,
default=self.default or "",
editable=self._editable,
serialize=self.serialize,
choices=self.choices,
help_text=self.help_text,
db_column=None,
db_tablespace=self.db_tablespace
)
localized_field.contribute_to_class(cls,
"%s_%s" % (name, lang_code),)
def translated_value(self):
language = get_language()
val = self.__dict__["%s_%s" % (name, language)]
if not val:
val = self.__dict__["%s_%s" % (name, settings.LANGUAGE_CODE)]
return val
setattr(cls, name, property(translated_value))
Thank you
I have a model with a field name. My custom MultilingualCharField create in the database a field for each language (name_en, name_it etc). I don't have a field name in the database, but only in the model.
So, here what I did (I think can be useful even if you don't use a custom field but a field for each language in the model):
In mymodel.py:
class MyClass(models.Model):
...
class Meta:
ordering = [_('name_it')]
This works in form etc but gives an error in admin (TypeError: expected string or bytes-like object), so here's my Admin.py:
class MyClassAdmin(admin.ModelAdmin):
def get_ordering(self, request):
if get_language_from_request(request)=='it':
return ['name_it']
else:
return ['name_en']
If you find an error or a better way please tell me

Django Admin - How to pull model default field values from other tables

I am trying to get a field value automatically pulled from different table. Below is my case: I have two tables: sales and returns. While entering a new return, when a "sales_id" is selected, want to show "sales_quantity" automatically populated, non-editable (and also if possible, want to constraint the "return_quantity" <= "sales_quantity").
class T_Sales(models.Model):
product = models.ForeignKey(P_Product)
sales_quantity = models.IntegerField()
def __unicode__(self):
return str(self.id)
class T_Return(models.Model):
sales_id = models.ForeignKey(T_Sales)
#sales_quantity = models.IntegerField(default=T_Sales.sales_quantity)
return_quantity = models.IntegerField()
def __unicode__(self):
return self.description
Creating property in your model will do the job. sales_id won't be null, so there will never be an issue when trying to get that value.
class T_Sales(models.Model):
product = models.ForeignKey(P_Product)
sales_quantity = models.IntegerField()
def __unicode__(self):
return str(self.id)
class T_Return(models.Model):
sales_id = models.ForeignKey(T_Sales)
return_quantity = models.IntegerField()
def __unicode__(self):
return self.description
#property
def sales_quantity(self):
return self.sales_id.sales_quantity
Pros of that method:
value is not stored in your database twice
value will be automatically updated if there is change on T_Sales object.
Cons of that method:
It will trigger separate query when fetching that field, unless use select_related in your queryset.
it is hard (but possible) to print that field in detail view inside django admin (you can always show it only on list).
If you wan't to create some validation, you can override model's clean method and do your comparsion here, if there is something wrong, you should throw ValidationError. Example:
from django.core.exceptions import ValidationError
class T_Return(models.Model):
# ......
def clean(self):
if self.return_quantity > self.sales_quantity:
raise ValidationError("You can't return more than it was bought")
return super(T_Return, self).clean()
You can also assign error to return_quantity field, just pass dict in form 'field_name': error into validation error:
raiseValidationError({'return_quantity': _('You can't return more than it was bought.')}
You can't assign that error to sales_quantity.
You can create separate field in T_Return model and copy value from T_Sales on save:
class T_Sales(models.Model):
product = models.ForeignKey(P_Product)
sales_quantity = models.IntegerField()
def __unicode__(self):
return str(self.id)
class T_Return(models.Model):
sales_id = models.ForeignKey(T_Sales)
sales_quantity = models.IntegerField(editable=False)
return_quantity = models.IntegerField()
def __unicode__(self):
return self.description
def save(self, *args, **kwargs):
if not self.sales_quantity:
self.sales_quantity = self.sales_id.sales_quantity
supr(T_Return, self).save(*args, **kwargs)
Pros of that method:
It will trigger additional query to T_Sales only on (first) save.
It is easy to display value in admin's detail view
Cons of that method:
- You're storing value in database twice
- If value in T_Sales object will change, value in T_Return won't be changed automatically (that can be fixed by simple trigger on save of T_Sales, but only inside django ORM)
If you wan't to create some validation, you can override model's clean method and do your comparsion here, if there is something wrong, you should throw ValidationError. Example:
from django.core.exceptions import ValidationError
class T_Return(models.Model):
# ......
def clean(self):
if self.return_quantity > self.sales_quantity:
raise ValidationError("You can't return more than it was bought")
return super(T_Return, self).clean()
You can also assign error to return_quantity or to sales_quantity field, just pass dict in form 'field_name': error into validation error:
raiseValidationError({'return_quantity': _('You can't return more than it was bought.')}
Assigning that errot both to return_quantity and sales_quantity will show that error twice.

When saving, how can you check if a field has changed?

In my model I have :
class Alias(MyBaseModel):
remote_image = models.URLField(
max_length=500, null=True,
help_text='''
A URL that is downloaded and cached for the image.
Only used when the alias is made
'''
)
image = models.ImageField(
upload_to='alias', default='alias-default.png',
help_text="An image representing the alias"
)
def save(self, *args, **kw):
if (not self.image or self.image.name == 'alias-default.png') and self.remote_image :
try :
data = utils.fetch(self.remote_image)
image = StringIO.StringIO(data)
image = Image.open(image)
buf = StringIO.StringIO()
image.save(buf, format='PNG')
self.image.save(
hashlib.md5(self.string_id).hexdigest() + ".png", ContentFile(buf.getvalue())
)
except IOError :
pass
Which works great for the first time the remote_image changes.
How can I fetch a new image when someone has modified the remote_image on the alias? And secondly, is there a better way to cache a remote image?
Essentially, you want to override the __init__ method of models.Model so that you keep a copy of the original value. This makes it so that you don't have to do another DB lookup (which is always a good thing).
class Person(models.Model):
name = models.CharField()
__original_name = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__original_name = self.name
def save(self, force_insert=False, force_update=False, *args, **kwargs):
if self.name != self.__original_name:
# name changed - do something here
super().save(force_insert, force_update, *args, **kwargs)
self.__original_name = self.name
I use following mixin:
from django.forms.models import model_to_dict
class ModelDiffMixin(object):
"""
A model mixin that tracks model fields' values and provide some useful api
to know what fields have been changed.
"""
def __init__(self, *args, **kwargs):
super(ModelDiffMixin, self).__init__(*args, **kwargs)
self.__initial = self._dict
#property
def diff(self):
d1 = self.__initial
d2 = self._dict
diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]
return dict(diffs)
#property
def has_changed(self):
return bool(self.diff)
#property
def changed_fields(self):
return self.diff.keys()
def get_field_diff(self, field_name):
"""
Returns a diff for field if it's changed and None otherwise.
"""
return self.diff.get(field_name, None)
def save(self, *args, **kwargs):
"""
Saves model and set initial state.
"""
super(ModelDiffMixin, self).save(*args, **kwargs)
self.__initial = self._dict
#property
def _dict(self):
return model_to_dict(self, fields=[field.name for field in
self._meta.fields])
Usage:
>>> p = Place()
>>> p.has_changed
False
>>> p.changed_fields
[]
>>> p.rank = 42
>>> p.has_changed
True
>>> p.changed_fields
['rank']
>>> p.diff
{'rank': (0, 42)}
>>> p.categories = [1, 3, 5]
>>> p.diff
{'categories': (None, [1, 3, 5]), 'rank': (0, 42)}
>>> p.get_field_diff('categories')
(None, [1, 3, 5])
>>> p.get_field_diff('rank')
(0, 42)
>>>
Note
Please note that this solution works well in context of current request only. Thus it's suitable primarily for simple cases. In concurrent environment where multiple requests can manipulate the same model instance at the same time, you definitely need a different approach.
Best way is with a pre_save signal. May not have been an option back in '09 when this question was asked and answered, but anyone seeing this today should do it this way:
#receiver(pre_save, sender=MyModel)
def do_something_if_changed(sender, instance, **kwargs):
try:
obj = sender.objects.get(pk=instance.pk)
except sender.DoesNotExist:
pass # Object is new, so field hasn't technically changed, but you may want to do something else here.
else:
if not obj.some_field == instance.some_field: # Field has changed
# do something
And now for direct answer: one way to check if the value for the field has changed is to fetch original data from database before saving instance. Consider this example:
class MyModel(models.Model):
f1 = models.CharField(max_length=1)
def save(self, *args, **kw):
if self.pk is not None:
orig = MyModel.objects.get(pk=self.pk)
if orig.f1 != self.f1:
print 'f1 changed'
super(MyModel, self).save(*args, **kw)
The same thing applies when working with a form. You can detect it at the clean or save method of a ModelForm:
class MyModelForm(forms.ModelForm):
def clean(self):
cleaned_data = super(ProjectForm, self).clean()
#if self.has_changed(): # new instance or existing updated (form has data to save)
if self.instance.pk is not None: # new instance only
if self.instance.f1 != cleaned_data['f1']:
print 'f1 changed'
return cleaned_data
class Meta:
model = MyModel
exclude = []
Since Django 1.8 released, you can use from_db classmethod to cache old value of remote_image. Then in save method you can compare old and new value of field to check if the value has changed.
#classmethod
def from_db(cls, db, field_names, values):
new = super(Alias, cls).from_db(db, field_names, values)
# cache value went from the base
new._loaded_remote_image = values[field_names.index('remote_image')]
return new
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
if (self._state.adding and self.remote_image) or \
(not self._state.adding and self._loaded_remote_image != self.remote_image):
# If it is first save and there is no cached remote_image but there is new one,
# or the value of remote_image has changed - do your stuff!
Note that field change tracking is available in django-model-utils.
https://django-model-utils.readthedocs.org/en/latest/index.html
If you are using a form, you can use Form's changed_data (docs):
class AliasForm(ModelForm):
def save(self, commit=True):
if 'remote_image' in self.changed_data:
# do things
remote_image = self.cleaned_data['remote_image']
do_things(remote_image)
super(AliasForm, self).save(commit)
class Meta:
model = Alias
I am a bit late to the party but I found this solution also:
Django Dirty Fields
Another late answer, but if you're just trying to see if a new file has been uploaded to a file field, try this: (adapted from Christopher Adams's comment on the link http://zmsmith.com/2010/05/django-check-if-a-field-has-changed/ in zach's comment here)
Updated link: https://web.archive.org/web/20130101010327/http://zmsmith.com:80/2010/05/django-check-if-a-field-has-changed/
def save(self, *args, **kw):
from django.core.files.uploadedfile import UploadedFile
if hasattr(self.image, 'file') and isinstance(self.image.file, UploadedFile) :
# Handle FileFields as special cases, because the uploaded filename could be
# the same as the filename that's already there even though there may
# be different file contents.
# if a file was just uploaded, the storage model with be UploadedFile
# Do new file stuff here
pass
There is an attribute __dict__ which have all the fields as the keys and value as the field values. So we can just compare two of them
Just change the save function of model to the function below
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
if self.pk is not None:
initial = A.objects.get(pk=self.pk)
initial_json, final_json = initial.__dict__.copy(), self.__dict__.copy()
initial_json.pop('_state'), final_json.pop('_state')
only_changed_fields = {k: {'final_value': final_json[k], 'initial_value': initial_json[k]} for k in initial_json if final_json[k] != initial_json[k]}
print(only_changed_fields)
super(A, self).save(force_insert=False, force_update=False, using=None, update_fields=None)
Example Usage:
class A(models.Model):
name = models.CharField(max_length=200, null=True, blank=True)
senior = models.CharField(choices=choices, max_length=3)
timestamp = models.DateTimeField(null=True, blank=True)
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
if self.pk is not None:
initial = A.objects.get(pk=self.pk)
initial_json, final_json = initial.__dict__.copy(), self.__dict__.copy()
initial_json.pop('_state'), final_json.pop('_state')
only_changed_fields = {k: {'final_value': final_json[k], 'initial_value': initial_json[k]} for k in initial_json if final_json[k] != initial_json[k]}
print(only_changed_fields)
super(A, self).save(force_insert=False, force_update=False, using=None, update_fields=None)
yields output with only those fields that have been changed
{'name': {'initial_value': '1234515', 'final_value': 'nim'}, 'senior': {'initial_value': 'no', 'final_value': 'yes'}}
As of Django 1.8, there's the from_db method, as Serge mentions. In fact, the Django docs include this specific use case as an example:
https://docs.djangoproject.com/en/dev/ref/models/instances/#customizing-model-loading
Below is an example showing how to record the initial values of fields that are loaded from the database
This works for me in Django 1.8
def clean(self):
if self.cleaned_data['name'] != self.initial['name']:
# Do something
Very late to the game, but this is a version of Chris Pratt's answer that protects against race conditions while sacrificing performance, by using a transaction block and select_for_update()
#receiver(pre_save, sender=MyModel)
#transaction.atomic
def do_something_if_changed(sender, instance, **kwargs):
try:
obj = sender.objects.select_for_update().get(pk=instance.pk)
except sender.DoesNotExist:
pass # Object is new, so field hasn't technically changed, but you may want to do something else here.
else:
if not obj.some_field == instance.some_field: # Field has changed
# do something
You can use django-model-changes to do this without an additional database lookup:
from django.dispatch import receiver
from django_model_changes import ChangesMixin
class Alias(ChangesMixin, MyBaseModel):
# your model
#receiver(pre_save, sender=Alias)
def do_something_if_changed(sender, instance, **kwargs):
if 'remote_image' in instance.changes():
# do something
The optimal solution is probably one that does not include an additional database read operation prior to saving the model instance, nor any further django-library. This is why laffuste's solutions is preferable. In the context of an admin site, one can simply override the save_model-method, and invoke the form's has_changed method there, just as in Sion's answer above. You arrive at something like this, drawing on Sion's example setting but using changed_data to get every possible change:
class ModelAdmin(admin.ModelAdmin):
fields=['name','mode']
def save_model(self, request, obj, form, change):
form.changed_data #output could be ['name']
#do somethin the changed name value...
#call the super method
super(self,ModelAdmin).save_model(request, obj, form, change)
Override save_model:
https://docs.djangoproject.com/en/1.10/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_model
Built-in changed_data-method for a Field:
https://docs.djangoproject.com/en/1.10/ref/forms/api/#django.forms.Form.changed_data
While this doesn't actually answer your question, I'd go about this in a different way.
Simply clear the remote_image field after successfully saving the local copy. Then in your save method you can always update the image whenever remote_image isn't empty.
If you'd like to keep a reference to the url, you could use an non-editable boolean field to handle the caching flag rather than remote_image field itself.
I had this situation before my solution was to override the pre_save() method of the target field class it will be called only if the field has been changed
useful with FileField
example:
class PDFField(FileField):
def pre_save(self, model_instance, add):
# do some operations on your file
# if and only if you have changed the filefield
disadvantage:
not useful if you want to do any (post_save) operation like using the created object in some job (if certain field has changed)
I have extended the mixin of #livskiy as follows:
class ModelDiffMixin(models.Model):
"""
A model mixin that tracks model fields' values and provide some useful api
to know what fields have been changed.
"""
_dict = DictField(editable=False)
def __init__(self, *args, **kwargs):
super(ModelDiffMixin, self).__init__(*args, **kwargs)
self._initial = self._dict
#property
def diff(self):
d1 = self._initial
d2 = self._dict
diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]
return dict(diffs)
#property
def has_changed(self):
return bool(self.diff)
#property
def changed_fields(self):
return self.diff.keys()
def get_field_diff(self, field_name):
"""
Returns a diff for field if it's changed and None otherwise.
"""
return self.diff.get(field_name, None)
def save(self, *args, **kwargs):
"""
Saves model and set initial state.
"""
object_dict = model_to_dict(self,
fields=[field.name for field in self._meta.fields])
for field in object_dict:
# for FileFields
if issubclass(object_dict[field].__class__, FieldFile):
try:
object_dict[field] = object_dict[field].path
except :
object_dict[field] = object_dict[field].name
# TODO: add other non-serializable field types
self._dict = object_dict
super(ModelDiffMixin, self).save(*args, **kwargs)
class Meta:
abstract = True
and the DictField is:
class DictField(models.TextField):
__metaclass__ = models.SubfieldBase
description = "Stores a python dict"
def __init__(self, *args, **kwargs):
super(DictField, self).__init__(*args, **kwargs)
def to_python(self, value):
if not value:
value = {}
if isinstance(value, dict):
return value
return json.loads(value)
def get_prep_value(self, value):
if value is None:
return value
return json.dumps(value)
def value_to_string(self, obj):
value = self._get_val_from_obj(obj)
return self.get_db_prep_value(value)
it can be used by extending it in your models
a _dict field will be added when you sync/migrate and that field will store the state of your objects
improving #josh answer for all fields:
class Person(models.Model):
name = models.CharField()
def __init__(self, *args, **kwargs):
super(Person, self).__init__(*args, **kwargs)
self._original_fields = dict([(field.attname, getattr(self, field.attname))
for field in self._meta.local_fields if not isinstance(field, models.ForeignKey)])
def save(self, *args, **kwargs):
if self.id:
for field in self._meta.local_fields:
if not isinstance(field, models.ForeignKey) and\
self._original_fields[field.name] != getattr(self, field.name):
# Do Something
super(Person, self).save(*args, **kwargs)
just to clarify, the getattr works to get fields like person.name with strings (i.e. getattr(person, "name")
My take on #iperelivskiy's solution: on large scale, creating the _initial dict for every __init__ is expensive, and most of the time - unnecessary. I have changed the mixin slightly such that it records changes only when you explicitly tell it to do so (by calling instance.track_changes):
from typing import KeysView, Optional
from django.forms import model_to_dict
class TrackChangesMixin:
_snapshot: Optional[dict] = None
def track_changes(self):
self._snapshot = self.as_dict
#property
def diff(self) -> dict:
if self._snapshot is None:
raise ValueError("track_changes wasn't called, can't determine diff.")
d1 = self._snapshot
d2 = self.as_dict
diffs = [(k, (v, d2[k])) for k, v in d1.items() if str(v) != str(d2[k])]
return dict(diffs)
#property
def has_changed(self) -> bool:
return bool(self.diff)
#property
def changed_fields(self) -> KeysView:
return self.diff.keys()
#property
def as_dict(self) -> dict:
return model_to_dict(self, fields=[field.name for field in self._meta.fields])
I have found this package django-lifecycle.
It uses django signals to define #hook decorator, which is very robust and reliable. I used it and it is a bliss.
How about using David Cramer's solution:
http://cramer.io/2010/12/06/tracking-changes-to-fields-in-django/
I've had success using it like this:
#track_data('name')
class Mode(models.Model):
name = models.CharField(max_length=5)
mode = models.CharField(max_length=5)
def save(self, *args, **kwargs):
if self.has_changed('name'):
print 'name changed'
# OR #
#classmethod
def post_save(cls, sender, instance, created, **kwargs):
if instance.has_changed('name'):
print "Hooray!"
A modification to #ivanperelivskiy's answer:
#property
def _dict(self):
ret = {}
for field in self._meta.get_fields():
if isinstance(field, ForeignObjectRel):
# foreign objects might not have corresponding objects in the database.
if hasattr(self, field.get_accessor_name()):
ret[field.get_accessor_name()] = getattr(self, field.get_accessor_name())
else:
ret[field.get_accessor_name()] = None
else:
ret[field.attname] = getattr(self, field.attname)
return ret
This uses django 1.10's public method get_fields instead. This makes the code more future proof, but more importantly also includes foreign keys and fields where editable=False.
For reference, here is the implementation of .fields
#cached_property
def fields(self):
"""
Returns a list of all forward fields on the model and its parents,
excluding ManyToManyFields.
Private API intended only to be used by Django itself; get_fields()
combined with filtering of field properties is the public API for
obtaining this field list.
"""
# For legacy reasons, the fields property should only contain forward
# fields that are not private or with a m2m cardinality. Therefore we
# pass these three filters as filters to the generator.
# The third lambda is a longwinded way of checking f.related_model - we don't
# use that property directly because related_model is a cached property,
# and all the models may not have been loaded yet; we don't want to cache
# the string reference to the related_model.
def is_not_an_m2m_field(f):
return not (f.is_relation and f.many_to_many)
def is_not_a_generic_relation(f):
return not (f.is_relation and f.one_to_many)
def is_not_a_generic_foreign_key(f):
return not (
f.is_relation and f.many_to_one and not (hasattr(f.remote_field, 'model') and f.remote_field.model)
)
return make_immutable_fields_list(
"fields",
(f for f in self._get_fields(reverse=False)
if is_not_an_m2m_field(f) and is_not_a_generic_relation(f) and is_not_a_generic_foreign_key(f))
)
as an extension of SmileyChris' answer, you can add a datetime field to the model for last_updated, and set some sort of limit for the max age you'll let it get to before checking for a change
The mixin from #ivanlivski is great.
I've extended it to
Ensure it works with Decimal fields.
Expose properties to simplify usage
The updated code is available here:
https://github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py
To help people new to Python or Django, I'll give a more complete example.
This particular usage is to take a file from a data provider and ensure the records in the database reflect the file.
My model object:
class Station(ModelDiffMixin.ModelDiffMixin, models.Model):
station_name = models.CharField(max_length=200)
nearby_city = models.CharField(max_length=200)
precipitation = models.DecimalField(max_digits=5, decimal_places=2)
# <list of many other fields>
def is_float_changed (self,v1, v2):
''' Compare two floating values to just two digit precision
Override Default precision is 5 digits
'''
return abs (round (v1 - v2, 2)) > 0.01
The class that loads the file has these methods:
class UpdateWeather (object)
# other methods omitted
def update_stations (self, filename):
# read all existing data
all_stations = models.Station.objects.all()
self._existing_stations = {}
# insert into a collection for referencing while we check if data exists
for stn in all_stations.iterator():
self._existing_stations[stn.id] = stn
# read the file. result is array of objects in known column order
data = read_tabbed_file(filename)
# iterate rows from file and insert or update where needed
for rownum in range(sh.nrows):
self._update_row(sh.row(rownum));
# now anything remaining in the collection is no longer active
# since it was not found in the newest file
# for now, delete that record
# there should never be any of these if the file was created properly
for stn in self._existing_stations.values():
stn.delete()
self._num_deleted = self._num_deleted+1
def _update_row (self, rowdata):
stnid = int(rowdata[0].value)
name = rowdata[1].value.strip()
# skip the blank names where data source has ids with no data today
if len(name) < 1:
return
# fetch rest of fields and do sanity test
nearby_city = rowdata[2].value.strip()
precip = rowdata[3].value
if stnid in self._existing_stations:
stn = self._existing_stations[stnid]
del self._existing_stations[stnid]
is_update = True;
else:
stn = models.Station()
is_update = False;
# object is new or old, don't care here
stn.id = stnid
stn.station_name = name;
stn.nearby_city = nearby_city
stn.precipitation = precip
# many other fields updated from the file
if is_update == True:
# we use a model mixin to simplify detection of changes
# at the cost of extra memory to store the objects
if stn.has_changed == True:
self._num_updated = self._num_updated + 1;
stn.save();
else:
self._num_created = self._num_created + 1;
stn.save()
Here is another way of doing it.
class Parameter(models.Model):
def __init__(self, *args, **kwargs):
super(Parameter, self).__init__(*args, **kwargs)
self.__original_value = self.value
def clean(self,*args,**kwargs):
if self.__original_value == self.value:
print("igual")
else:
print("distinto")
def save(self,*args,**kwargs):
self.full_clean()
return super(Parameter, self).save(*args, **kwargs)
self.__original_value = self.value
key = models.CharField(max_length=24, db_index=True, unique=True)
value = models.CharField(max_length=128)
As per documentation: validating objects
"The second step full_clean() performs is to call Model.clean(). This method should be overridden to perform custom validation on your model.
This method should be used to provide custom model validation, and to modify attributes on your model if desired. For instance, you could use it to automatically provide a value for a field, or to do validation that requires access to more than a single field:"
If you do not find interest in overriding save method, you can do
model_fields = [f.name for f in YourModel._meta.get_fields()]
valid_data = {
key: new_data[key]
for key in model_fields
if key in new_data.keys()
}
for (key, value) in valid_data.items():
if getattr(instance, key) != value:
print ('Data has changed')
setattr(instance, key, value)
instance.save()
Sometimes I want to check for changes on the same specific fields on multiple models that share those fields, so I define a list of those fields and use a signal. In this case, geocoding addresses only if something has changed, or if the entry is new:
from django.db.models.signals import pre_save
from django.dispatch import receiver
#receiver(pre_save, sender=SomeUserProfileModel)
#receiver(pre_save, sender=SomePlaceModel)
#receiver(pre_save, sender=SomeOrganizationModel)
#receiver(pre_save, sender=SomeContactInfoModel)
def geocode_address(sender, instance, *args, **kwargs):
input_fields = ['address_line', 'address_line_2', 'city', 'state', 'postal_code', 'country']
try:
orig = sender.objects.get(id=instance.id)
if orig:
changes = 0
for field in input_fields:
if not (getattr(instance, field)) == (getattr(orig, field)):
changes += 1
if changes > 0:
# do something here because at least one field changed...
my_geocoder_function(instance)
except:
# do something here because there is no original, or pass.
my_geocoder_function(instance)
Writing it once and attaching with "#receiver" sure beats overriding multiple model save methods, but perhaps some others have better ideas.

Inline Form Validation in Django

I would like to make an entire inline formset within an admin change form compulsory. So in my current scenario when I hit save on an Invoice form (in Admin) the inline Order form is blank. I'd like to stop people creating invoices with no orders associated.
Anyone know an easy way to do that?
Normal validation like (required=True) on the model field doesn't appear to work in this instance.
The best way to do this is to define a custom formset, with a clean method that validates that at least one invoice order exists.
class InvoiceOrderInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
# get forms that actually have valid data
count = 0
for form in self.forms:
try:
if form.cleaned_data:
count += 1
except AttributeError:
# annoyingly, if a subform is invalid Django explicity raises
# an AttributeError for cleaned_data
pass
if count < 1:
raise forms.ValidationError('You must have at least one order')
class InvoiceOrderInline(admin.StackedInline):
formset = InvoiceOrderInlineFormset
class InvoiceAdmin(admin.ModelAdmin):
inlines = [InvoiceOrderInline]
Daniel's answer is excellent and it worked for me on one project, but then I realized due to the way Django forms work, if you are using can_delete and check the delete box while saving, it's possible to validate w/o any orders (in this case).
I spent a while trying to figure out how to prevent that from happening. The first situation was easy - don't include the forms that are going to get deleted in the count. The second situation was trickier...if all the delete boxes are checked, then clean wasn't being called.
The code isn't exactly straightforward, unfortunately. The clean method is called from full_clean which is called when the error property is accessed. This property is not accessed when a subform is being deleted, so full_clean is never called. I'm no Django expert, so this might be a terrible way of doing it, but it seems to work.
Here's the modified class:
class InvoiceOrderInlineFormset(forms.models.BaseInlineFormSet):
def is_valid(self):
return super(InvoiceOrderInlineFormset, self).is_valid() and \
not any([bool(e) for e in self.errors])
def clean(self):
# get forms that actually have valid data
count = 0
for form in self.forms:
try:
if form.cleaned_data and not form.cleaned_data.get('DELETE', False):
count += 1
except AttributeError:
# annoyingly, if a subform is invalid Django explicity raises
# an AttributeError for cleaned_data
pass
if count < 1:
raise forms.ValidationError('You must have at least one order')
class MandatoryInlineFormSet(BaseInlineFormSet):
def is_valid(self):
return super(MandatoryInlineFormSet, self).is_valid() and \
not any([bool(e) for e in self.errors])
def clean(self):
# get forms that actually have valid data
count = 0
for form in self.forms:
try:
if form.cleaned_data and not form.cleaned_data.get('DELETE', False):
count += 1
except AttributeError:
# annoyingly, if a subform is invalid Django explicity raises
# an AttributeError for cleaned_data
pass
if count < 1:
raise forms.ValidationError('You must have at least one of these.')
class MandatoryTabularInline(admin.TabularInline):
formset = MandatoryInlineFormSet
class MandatoryStackedInline(admin.StackedInline):
formset = MandatoryInlineFormSet
class CommentInlineFormSet( MandatoryInlineFormSet ):
def clean_rating(self,form):
"""
rating must be 0..5 by .5 increments
"""
rating = float( form.cleaned_data['rating'] )
if rating < 0 or rating > 5:
raise ValidationError("rating must be between 0-5")
if ( rating / 0.5 ) != int( rating / 0.5 ):
raise ValidationError("rating must have .0 or .5 decimal")
def clean( self ):
super(CommentInlineFormSet, self).clean()
for form in self.forms:
self.clean_rating(form)
class CommentInline( MandatoryTabularInline ):
formset = CommentInlineFormSet
model = Comment
extra = 1
#Daniel Roseman solution is fine but i have some modification with some less code to do this same.
class RequiredFormSet(forms.models.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(RequiredFormSet, self).__init__(*args, **kwargs)
self.forms[0].empty_permitted = False
class InvoiceOrderInline(admin.StackedInline):
model = InvoiceOrder
formset = RequiredFormSet
class InvoiceAdmin(admin.ModelAdmin):
inlines = [InvoiceOrderInline]
try this it also works :)
The situation became a little bit better but still needs some work around. Django provides validate_min and min_num attributes nowadays, and if min_num is taken from Inline during formset instantiation, validate_min can be only passed as init formset argument. So my solution looks something like this:
class MinValidatedInlineMixIn:
validate_min = True
def get_formset(self, *args, **kwargs):
return super().get_formset(validate_min=self.validate_min, *args, **kwargs)
class InvoiceOrderInline(MinValidatedInlineMixIn, admin.StackedInline):
model = InvoiceOrder
min_num = 1
validate_min = True
class InvoiceAdmin(admin.ModelAdmin):
inlines = [InvoiceOrderInline]