Django admin model save - Update existing object - django

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.

Related

How to get a model's last access date in Django?

I'm building a Django application, and in it I would like to track whenever a particular model was last accessed.
I'm opting for this in order to build a user activity history.
I know Django provides auto_now and auto_now_add, but these do not do what I want them to do. The latter tracks when a model was created, and the former tracks when it was last modified, which is different from when it was last accessed, mind you.
I've tried adding another datetime field to my model's specification:
accessed_on = models.DateTimeField()
Then I try to update the model's access manually by calling the following after each access:
model.accessed_on = datetime.utcnow()
model.save()
But it still won't work.
I've gone through the django documentation for an answer, but couldn't find one.
Help would be much appreciated.
What about creating a model with a field that contains the last save-date. Plus saving the object every time is translated from the DB representation to the python representation?
class YourModel(models.Model):
date_accessed = models.DateTimeField(auto_now=True)
#classmethod
def from_db(cls, db, field_names, values):
obj = super().from_db(db, field_names, values)
obj.save()
return obj

modify unique_together constraint to save new object and delete old

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.

Override Default Save Method And Create Duplicate

I am looking to create a duplicate instance each time a user tries to update an instance. The existing record is untouched and the full update is saved to the new instance.
Some foreign keys and reverse foreign keys must also be duplicated. The Django documentation
talks about duplicating objects, but does not address reverse foreign keys.
Firstly, is there an accepted way of approaching this problem?
Secondly, I am unsure whether it's best to overwrite the form save method or the model save method? I would want it to apply to everything, regardless of the form, so I assume it should be applied at the model level?
A simplified version of the models are outlined below.
class Invoice(models.Model):
number = models.CharField(max_length=15)
class Line(models.Model):
invoice = models.ForeignKey(Invoice)
price = models.DecimalField(max_digits=15, decimal_places=4)
Here's my shot at it. If you need it to duplicate every time you make any changes, then override the model save method. Note that this will not have any effect when executing .update() on a queryset.
class Invoice(models.Model):
number = models.CharField(max_length=15)
def save(self, *args, **kwargs):
if not self.pk:
# if we dont have a pk set yet, it is the first time we are saving. Nothing to duplicate.
super(Invoice, self).save(*args, **kwargs)
else:
# save the line items before we duplicate
lines = list(self.line_set.all())
self.pk = None
super(Invoice, self).save(*args, **kwargs)
for line in lines:
line.pk = None
line.invoice = self
line.save()
This will create a duplicate Invoice every time you call .save() on an existing record. It will also create duplicates for every Line tied to that Invoice. You may need to do something similar every time you update a Line as well.
This of course is not very generic. This is specific to these 2 models. If you need something more generic, you could loop over every field, determine what kind of field it is, make needed duplicates, etc.

Django modelformset creates new record instead of updating existing one

I have a System that can have one or more Models. I have modeled this relationship in the database with a manytomany field. The code below is for editing the system and its associated methods in a single form.
Adding a new method by filling out its form and pressing submit works only the first time. If I then make a small change and submit again, I get the following message (generated by the code below):
METHODFORMSET.ERRORS: [{}, {'name': [u'Method with this Name already exists.']}]
This is caused by the fact that the name field is unique, but it should have updated, not created a new record, even though I am using the POST data to generate the methodformset instance...
Note that this behaviour only applies to the last appended method instance, not to ones that were already present in the table.
Here is the relevant code, can anyone let me know what I am doing wrong?
def sysedit(request, sys_id):
system = System.objects.get(id=sys_id)
MethodFormSet = modelformset_factory(Method, form=MethodForm)
post = None
if request.POST:
post = request.POST.copy()
if 'add_method' in request.POST:
post['method-TOTAL_FORMS'] = repr(int(
post['method-TOTAL_FORMS'])+ 1)
systemform = SystemForm(data=post, instance=system)
methodformset = MethodFormSet(data=post, prefix='method',
queryset=Method.objects.filter(id__in=system.method.all()))
if methodformset.is_valid():
mfs = methodformset.save()
print 'SAVED-method', mfs
for mf in mfs:
if systemform.is_valid():
sp = systemform.save(mf)
print 'SYSTEM', sp
else:
print 'SYSFORMSET.ERRORS:', systemform.errors
else:
print 'METHODFORMSET.ERRORS:', methodformset.errors
return render_to_response('sysedit.html',
{'systemform': systemform,
'methodformset': methodformset,
'system': system},
context_instance=RequestContext(request))
class System(models.Model):
method = models.ManyToManyField(Method)
...
class Method(models.Model):
name = models.CharField(unique=True)
...
class MethodForm(ModelForm):
class Meta:
model = Method
class SystemForm(ModelForm):
def save(self, new_method=None, commit=True, *args, **kwargs):
m = super(SystemForm, self).save(commit=False, *args, **kwargs)
if new_method:
m.method.add(new_method)
if commit:
m.save()
return m
class Meta:
model = System
exclude = ('method')
[EDIT after Sergzach's answer]:
The problem is not how to deal with the Method with this name already exists error, but to prevent that from occurring in the first place. I think the actual problem may have something to do with the way modelformsets deal with new forms. Somehow it looks like it always tries to create a new instance for the last formset, regardless of whether it already exits.
So if I do not add a new formset after the last one was appended, the modelformset will try to re-create the last one (even though it was just created on the previous submit).
The initial situation is that I have 1 valid Method instance and 1 new unbound instance in the methodformset. I then fill out the form and hit save, which validates both Methods and binds the 2nd one, which is then saved to the table.
So far all is well, but if I then hit save the 2nd time the error occurs. Maybe this has to do with the fact that method-TOTAL_FORMS=2 and method-INITIAL_FORMS=1. Could it be that this causes modelformset to force a create on the 2nd Method?
Can anyone confirm/deny this?
[Edit after a weekend of not looking at the code]:
The problem is caused by the fact that I am saving the forms in the view and after saving, I am sending the original methodformset instance (from before the save) to the template. The problem can be solved by re-instantiating modelformset after the save, using the queryset and NOT the POST data.
So the general rule to prevent errors like this, is either to go to a different page after a save (avoid it altogether), or use the above solution.
Before I post this as THE solution, I need to do more testing.
You can validate each form when saving a formset. I have created a simple example (similar to your code) and it works well for me. It creates new objects if there is no object with a such name otherwise it edits an existing object.
You need a form to edit your model objects:
class EditMethodForm( forms.ModelForm ):
class Meta:
model = Method
exclude = ( 'name', )
Then instead of methodformset.is_valid() you do the next:
for methodform in methodformset:
try:
instance = Method.objects.get( name = request.POST[ 'name' ] )
except Method.DoesNotExist:
methodform.save()
else:
editmethodform = EditMethodForm( request.POST, instance = instance )
if editmethodform.is_valid():
editmethodform.save()
There are some additional features in your code. I show the working principle. Is it enough to understand the solution?
I have solved the problem by re-instantiating modelformset after the save (see edit at the bottom of the question)

Getting ID of newly created object in save()

I want to save an object, so that the M2M get saved. Then I want to read out the M2M fields to do some calculations and set a field on the saved object.
class Item(models.Model):
name = models.CharField(max_length=20, unique=True)
product = models.ManyToManyField(SomeOtherModel, through='SomeTable')
def save(self, *args, **kwargs):
super(Item, self).save(*args, **kwargs)
m2m_items = SomeTable.objects.filter(item = self)
# DO SOME STUFF WITH THE M2M ITEMS
The m2m_items won't turn up,. Is there any way to get these up ?
Some confusion here.
Once you've called super, self.id will have a value.
However, I don't understand the point of your filter call. For a start, you probably mean get rather than filter anyway, as filter gets a queryset, rather than a single instance. But even so, the call is pointless: you've just saved it, so whatever you get back from the database will be exactly the same. What's the point?
Edit after question update OK, thanks for the clarification. However, the model's save() method is not responsible for doing anything with M2M items. They need to be saved separately, which is the job of the form or the view.