modify unique_together constraint to save new object and delete old - django

I have a model with a unique_together constraint on four fields. Instead of raising the normal Validation Error, however, I want to delete the old object and replace it with the newer one (or maybe update the old one? would also work). I'm a little at a loss as to how to go about doing this, or if there's maybe a better way to go about achieving this behavior.
EDIT:
Any downside to modifying the save method to just check the database for an instance with those four fields and deleting it if I find one?

overriding the save method is OK, but it will fetch the database every time, possibly causing performance loss. It will be better, and more pythonic, if you handle the ValidationError:
try:
YourModel.objects.create(
unique_together_field_1,
unique_together_field_2,
unique_together_field_3,
unique_together_field_4,
...
)
except YourModel.ValidationError:
# update or replace the existing model
EDIT
You can use this code in the model's manager:
class YourModelManager(models.Manager):
def create(**kwargs):
try:
super(YourModelManager, self).create(**kwargs)
except YourModel.ValidationError:
# handle here the error
# kwargs is a dictionary with the model fields
and in the model:
class YourModel(models.Model):
unique_together_field_1 = ....
...
class Meta:
unique_together = [...]
objects = YourModelManager()
Check the docs about custom managers.

Related

How to exclude deleted objects in a clean() function on a model/form using inline_formsety

I have a model with a custom clean function to make sure no two DateRange overlap:
class MyModel(models.Model):
date_range = DateRangeField()
def clean(self):
error_dict = {}
if MyModel.objects.exclude(id=self.id).filter(
date_range__overlap=self.date_range):
error_dict['date_range'] = ValidationError(
'Range can not overlap with an existing period.',
code='overlap_period')
if error_dict:
raise ValidationError(error_dict)
This works, but if I use inline_formset to submit more than one record at a time and delete a record that would remove the conflict (whilst updating others), the ValidationError still raises.
This is because the filter function is done on the existing records, not the new updated ones.
How can I amend the filter to exclude deleted objects in the inline_formset? Should I be doing a clean on the form instead? If so, how do I reference deleted objects?
You need to create the logic on top of the deleted_objects you get from the formset. For this reason, it is easier to implement this logic in the formset, as in the clean() method of the model you have visibility on the single current instance (and not the full set of new upcoming instances).
In the Django doc it is explained how to deal with deleted_objects
instances = formset.save(commit=False)
for obj in formset.deleted_objects:
# ... your logic here
As you can see in the example, you can intercept the deleted objects in order to create the desired logic of validation.

Concise way of getting or creating an object with given field values

Suppose I have:
from django.db import models
class MyContentClass(models.Model):
content = models.TextField()
another_field = models.TextField()
x = MyContentClass(content="Hello, world!", another_field="More Info")
Is there a more concise way to perform the following logic?
existing = MyContentClass.objects.filter(content=x.content, another_field=x.another_field)
if existing:
x = existing[0]
else:
x.save()
# x now points to an object which is saved to the DB,
# either one we've just saved there or one that already existed
# with the same field values we're interested in.
Specifically:
Is there a way to query for both (all) fields without specifying
each one separately?
Is there a better idiom for either getting the old object or saving the new one? Something like get_or_create, but which accepts an object as a parameter?
Assume the code which does the saving is separate from the code which generates the initial MyContentClass instance which we need to compare to. This is typical of a case where you have a function which returns a model object without also saving it.
You could convert x to a dictionary with
x_data = x.__dict__
Then that could be passed into the object's get_or_create method.
MyContentClass.objects.get_or_create(**x_data)
The problem with this is that there are a few fields that will cause this to error out (eg the unique ID, or the _state Django modelstate field). However, if you pop() those out of the dictionary beforehand, then you'd probably be good to go :)
cleaned_dict = remove_unneeded_fields(x_data)
MyContentClass.objects.get_or_create(**cleaned_dict)
def remove_unneeded_fields(x_data):
unneeded_fields = [
'_state',
'id',
# Whatever other fields you don't want the new obj to have
# eg any field marked as 'unique'
]
for field in unneeded_fields:
del x_data[field]
return x_data
EDIT
To avoid issues associated with having to maintain a whitelist/blacklist of fields you, could do something like this:
def remove_unneeded_fields(x_data, MyObjModel):
cleaned_data = {}
for field in MyObjModel._meta.fields:
if not field.unique:
cleaned_data[field.name] = x_data[field.name]
return cleaned_Data
There would probably have to be more validation than simply checking that the field is not unique, but this might offer some flexibility when it comes to minor model field changes.
I would suggest to create a custom manager for those models and add the functions you want to do with the models (like a custom get_or_create function).
https://docs.djangoproject.com/en/1.10/topics/db/managers/#custom-managers
This would be the cleanest way and involves no hacking. :)
You can create specific managers for specific models or create a superclass with functions you want for all models.
If you just want to add a second manager with a different name, beware that it will become the default manager if you don't set the objects manager first (https://docs.djangoproject.com/en/1.10/topics/db/managers/#default-managers)

Modify read only nested fields in DRF

I realize the title sounds silly but I want to be able to change the references to the Group objects for my User instances. But I do not want them to be able to create new groups or edit existing groups. I think what I want is a read only nested field. However, if I set it to read_only=True I do not get the data in my serializers validated data. If I set it to read_only=False then it tries to create a new Group instead of just changing the references.
class GroupSerializer(serializers.ModelSerializer):
permissions = PermissionSerializer(many=True)
class Meta:
model = Group
fields = (
'pk',
'name',
'permissions',
)
class UserSerializer(serializers.ModelSerializer):
groups = GroupSerializer(many=True)
....
class Meta:
model = User
exclude = (
....
)
def update(self, instance, validated_data):
print(validated_data)
return instance
def validate_groups(self, value):
print("validating groups")
....
return value
With read_only=True nothing happens at all. I get the user back on my PATCH request but the user is exactly the same. With read_only=False I get a validation error returned to me {'groups': [{'name': ['group with this name already exists.']}]}
I have also tried overriding the create and update method on the GroupSerializer but with no change.
At most, I want the GroupSerializer just to validate that the group from the data exists.
Really late answer, but I stumbled upon this in another thread here on StackOverflow (unfortunately I can't find it now), and they referred to the following discussion.
The solution I used was to create two serializers - one for reading and another one for writing - and two respective ViewSets including the correct Mixins. So I could list the nested Foreign Keys when using a GET method and only POST using the identifier for the existing Model of the FK relation. I used the depth attribute. I hope that relates to this problem.
The best solution I have found was to use the PrimaryKeyRelatedField and that provides a read only interface where I can select items that already exists and associate them to the object.
Sadly, there is one downside which is that now when viewing those objects in a GET type view the details of the related object are not shown but rather just the PK. I will figure out a way around this soon, maybe multi-serializer viewsets will do the trick.
Here are the docs

How to track changes when using update() in Django models

I'm trying to keep track of the changes whenever a field is changed.
I can see the changes in Django Admin History whenever I use the .save() method, but whenever I use the .update() method it does not record whatever I changed in my object.
I want to use update() because it can change multiple fields at the same time. It makes the code cleaner and more efficient (one query, one line...)
Right now I'm using this:
u = Userlist.objects.filter(username=user['username']).update(**user)
I can see all the changes when I do
u = Userlist.objects.get(username=user['username'])
u.lastname=lastname
u.save()
I'm also using django-simple-history to see the changes.setup.
From the docs:
Finally, realize that update() does an update at the SQL level and,
thus, does not call any save() methods on your models, nor does it
emit the pre_save or post_save signals (which are a consequence of
calling Model.save())
update() works at the DB level, so Django admin cannot track changes when updates are applied via .update(...).
If you still want to track the changes on updates, you can use:
for user in Userlist.objects.filter(age__gt=40):
user.lastname = 'new name'
user.save()
This is however more expensive and is not advisable if the only benefit is tracking changes via the admin history.
Here's how I've handled this and it's worked well so far:
# get current model instance to update
instance = UserList.objects.get(username=username)
# use model_to_dict to convert object to dict (imported from django.forms.models import model_to_dict)
obj_dict = model_to_dict(instance)
# create instance of the model with this old data but do not save it
old_instance = UserList(**obj_dict)
# update the model instance (there are multiple ways to do this)
UserList.objects.filter(username=username).update(**user)
# get the updated object
updated_object = UserList.objects.get(id=id)
# get list of fields in the model class
my_model_fields = [field.name for field in cls._meta.get_fields()]
# get list of fields if they are different
differences = list(filter(lambda field: getattr(updated_object, field, None)!= getattr(old_instance, field, None), my_model_fields))
The differences variable will give you the list of fields that are different between the two instances. I also found it helpful to add which model fields I don't want to check for differences (e.g. we know the updated_date will always be changed, so we don't need to keep track of it).
skip_diff_fields = ['updated_date']
my_model_fields = []
for field in cls._meta.get_fields():
if field.name not in skip_diff_fields:
my_model_fields.append(field.name)

Django admin model save - Update existing object

This seems really simple.
On my model save() I want to basically do a get_or_create(). So I want to update the model if it exists or create a new one if not.
This seems like a super simple problem, but I am not getting it right!
class StockLevel(models.Model):
stock_item = models.ForeignKey(StockItem)
current_stock_level = models.IntegerField(blank=True, default=0)
def save(self):
try:
# it exists
a = StockLevel.objects.get(stock_item=self.stock_item)
a.current_stock_level = self.current_stock_level
a.save()
except:
# is does not exist yet
# save as normaly would.
super(StockLevel, self).save()
OR
def save(self):
stock_level_item , created = StockLevel.objects.get_or_create(stock_item=self.stock_item)
stock_level_item.current_stock_level = self.current_stock_level
stock_level_item.save()
This would also go into a infinite loop.
This would just put the save() in an infinite loop. But that is the basic idea of how it should work.
Django uses the same save() method for both creating and updating the object.
User code doesn't need to determine whether to create or update the object, since this is done by the method itself.
Furthermore you can force the save() method to create or update the object by using the methods optional arguments.
This is covered in the Django docs.
This really doesn't sound like the best way to do this. The save method is meant for saving the current instance, not magically querying for an existing one. You should take care of this in the form or view code.
So this is how i solved a similar situation just yesterday,
I created a duplicate model to store the updated information.
let's call the new model "StockLevelUpdates".
I then used signals to insert the saved data from the original model.
I will use your model above as the original model to explain further.
class StockLevelUpdates(models.Model):
stock_item = models.ForeignKey(StockItem)
current_stock_level = models.IntegerField(blank=True, default=0)
#receiver(signals.post_save, sender=StockLevel)
def update_info(sender, instance, **kwargs):
try:
# if it exists
a = StockLevelUpdates.objects.get(stock_item=instance.stock_item)
a.current_stock_level = instance.current_stock_level
a.save()
except:
# if it does not exist yet
# save the new instance
obj = StockLevelUpdates(stock_item=instance.stock_item,
current_stock_level = instance.current_stock_level)
obj.save()
This worked well for me, and you can get all your update reports from the duplicate model.
Perhaps there is a better way to do this kind of thing but this was a quick way out of a sticky situation.