Override Default Save Method And Create Duplicate - django

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.

Related

Django formset save() writes new object on database?

So I have MyModel:
class MyModel(models.Model):
name = models.CharField(_('Name'), max_length=80, unique=True)
...
def save(self, *args, **kwargs):
if MyModel.objects.count() >= 5:
raise ValidationError("Can not have more than 5 MyModels!")
super().save(*args, **kwargs) # Call the "real" save() method.
There are already 5 objects from MyModel on the database.
I have a page where I can edit them all at the same time with a formset.
When I change one or more of them, I will get the Validation Error "Can not have more than 5 MyModels!".
Why is this happenning? I tought the formset was supposed to edit the existing objects, but it appears to be writing a new one and deleting the old one.
What is really happening when I do a formset.save() on the database?
Do I have to remove the save() method?
The save method inside the Model is called regardless you are creating or editing. Although you can distinguish between them by checking if the object has a primary key, like this:
if not self.pk and MyModel.objects.count() >= 5:
If you want more sophisticated control over validation, I suggest putting them in the forms. Specially if you want to limit the number of formset, you can check this documentation.

Django slugs not unique with unique=True

I created two articles with the title "test" and this is what the second one generates as an error:
duplicate key value violates unique constraint "xxx_content_slug_xxxx_uniq"
DETAIL: Key (slug)=(test) already exists.
Knowing that this is my model:
class Content()
slug = models.SlugField(unique=True)
def save(self, *args, **kwargs):
self.slug = slugify(self.title)
super(Content, self).save(*args, **kwargs)
and knowing that I made the migration in the DB.
I don't know how to solve that.
Note: The problem is generated from the class post that inherits the content class and I don't think this information would help in this context.
slugify does not check for uniqueness, so if you create two articles with the same title, slugify(self.title) is going to generate the same slug twice, which will of course cause the database to complain about a violation of the unique constraint.
Setting unique = True on a field, will not automatically create unique values for every new instance, it'll only create a constraint on the database that the same value can not be saved twice.
You can use django-autoslug, which is a library that does exactly what you need.
Simply install the package, and implement your fields like this:
from django.db.models import CharField, Model
from autoslug import AutoSlugField
class Content(models.Model)
title = CharField(max_length=200)
slug = AutoSlugField(populate_from='title')
AutoSlugField does the following:
populate itself from another field,
preserve uniqueness of the value and
use custom slugify() functions for better i18n.
(Also, I assume that Content is a subclass of Model, otherwise your code wouldn't work at all.)

Keeping a computed model field up-to-date via QuerySets

Preface:
Let's assume we are working on a DB that stores issues of magazines.
Those issues usually do not have a 'name' per se; instead a whole bunch of attributes (release year, serial number, release months, etc.) will contribute to the name the user may later on identify the issue with.
Depending on the attributes available per issue, this name will be calculated based on a pattern.
For example: an issue from the year 2017 with number 01 will get the name: 2017-01. An issue from the years 2000 and 2001, and the months Jan and Feb will get the name 2000/01-Jan/Feb.
The attributes can be changed at any time.
It is expected that the user can also do queries based on this name - so simply displaying the computed value (through __str__) is not enough.
What I have done so far:
For a long time, I actually calculated the name every time __str__ was called on the issue's instance. It was the quick and dirty (and slow) way.
Querying for the name was very slow and rather complicated and unreliable as it required 'reverse-engineering' the __str__ method and guessing what the user was trying to search for.
Then I tried a hybrid approach, by using a _name model field that is updated if a _changed_flag (f.ex. through signals) is set and the instance is instantiated or saved. This still didn't leave me with an up-to-date name on the database table unless I instatiated every instance that needed updating first. Again, slow. And care had to be taken to not end up in infinite recursions upon calling refresh_from_db (which recreates the current instance in the background).
TL:DR
Right now, I am using a custom QuerySet as a manager for a model with a computed field:
class ComputedNameModel(BaseModel):
_name = models.CharField(max_length=200, editable=False, default=gettext_lazy("No data."))
_changed_flag = models.BooleanField(editable=False, default=False)
name_composing_fields = []
objects = CNQuerySet.as_manager()
# ... some more methods ...
def __str__(self):
return self._name
class Meta(BaseModel.Meta):
abstract = True
QuerySet:
class CNQuerySet(MIZQuerySet):
def bulk_create(self, objs, batch_size=None):
# Set the _changed_flag on the objects to be created
for obj in objs:
obj._changed_flag = True
return super().bulk_create(objs, batch_size)
def filter(self, *args, **kwargs):
if any(k.startswith('_name') for k in kwargs):
self._update_names()
return super().filter(*args, **kwargs)
def update(self, **kwargs):
# it is save to assume that a name update will be required after this update
# if _changed_flag is not already part of the update, add it with the value True
if '_changed_flag' not in kwargs:
kwargs['_changed_flag'] = True
return super().update(**kwargs)
update.alters_data = True
def values(self, *fields, **expressions):
if '_name' in fields:
self._update_names()
return super().values(*fields, **expressions)
def values_list(self, *fields, **kwargs):
if '_name' in fields:
self._update_names()
return super().values_list(*fields, **kwargs)
def _update_names(self):
if self.filter(_changed_flag=True).exists():
with transaction.atomic():
for pk, val_dict in self.filter(_changed_flag=True).values_dict(*self.model.name_composing_fields).items():
new_name = self.model._get_name(**val_dict)
self.filter(pk=pk).update(_name=new_name, _changed_flag=False)
_update_names.alters_data = True
As you can see the, the boilerplate is real. And I have only cherry picked the QuerySet methods that I know I use for now.
Through signals (for relations) or QuerySet methods, a record's _changed_flag is set when anything about it changes. The records are then updated the next time the _name field is requested in any way.
It's blazingly fast, as it does not require a model instance (only the model's classmethod _get_name()) and works entirely off querysets and in-memory data.
Question:
Where to put the call to _update_names() such that the names are updated when required without overriding every single queryset method?
I have tried putting it in:
_clone: bad things happened. To not end up in recursion hell, you would have to keep track of which clone is trying to update and which are there to simply fetch data for the update. Clones are also created upon initializing your app which has a good (tables are always up-to-date) and a bad side (updating without yet having a need for it, costing time). Not all queryset methods create clones and generally, putting your update check in _clone feels too deep.
__repr__: Keeps the shell output up-to-date, but not much more. The default implementation takes a slice of the queryset, disabling the ability to filter, so the updating has to be done before __repr__.
_fetch_all: Like _clone: it runs an update when you may not need it and requires keeping an internal 'you-are-allowed-to-try-an-update' check.

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.

Django: making a custom PK auto-increment?

I've been using custom primary keys for a model in Django. (This was because I was importing values into the database and they already had ID's attached, and it made sense to preserve the existing values.)
class Transaction(models.Model):
id = models.IntegerField(primary_key=True)
transaction_type = models.IntegerField(choices=TRANSACTION_TYPES)
date_added = models.DateTimeField(auto_now_add=True)
However, now I want to add new instances of the model to the database, and I'd like to autogenerate a unique primary key. But if I don't specify the ID at the time of creating the instance, I get an error:
t = Transaction(transaction_type=0)
t.save()
gives:
IntegrityError at /page
(1048, "Column 'id' cannot be null")
How can I autogenerate a unique ID to specify for new values, without having to alter the way I import the existing values?
UPDATE
I've written this custom method, which seems to work...
class Transaction(models.Model):
def save(self, *args, **kwargs):
if not self.id:
i = Transaction.objects.all().order_by('-id')[0]
self.id = i.id+1
super(Transaction, self).save(*args, **kwargs)
You can use AutoField for the column id instead of IntegerField. The following should work for you:
id = models.AutoField(primary_key=True)
id will now increase automatically and won't have concurrency problems as it may encounter in save method.
I've ended up using very similar piece of code, but have made it slightly more generic:
def save(self, *args, **kwargs):
if self.id is None:
self.id = self.__class__.objects.all().order_by("-id")[0].id + 1
super(self.__class__, self).save(*args, **kwargs)
it uses self.__class__ so you can just copy paste this code to any model class without changing anything.
How are you importing the existing values? It would be trivial to write something into your Transactions __init__ to generate a new ID for you, but without knowing how you're importing the other values I can't say for sure whether it will alter the way you work with them.
If you remove your declared id field, django will automatically assume this:
id = models.AutoField(primary_key=True)
In Django 1.8, inspectdb will automatically detect auto_increment and use an AutoField when generating models.
Django migrations will do most of the hard work for you here.
Firstly, stop any access to your app so users can't change the database whilst you are working on it.
It would then be very wise to backup your database, before performing any work, as a precaution.
Remove your manually declared id field from your models.py (i.e. delete it).
Run makemigrations and then migrate. Django will modify the id field to the correct implementation for your database version.
Run this (example) command in psql adapting, if need be, to your table names:
select setval(pg_get_serial_sequence('transactions_transaction', 'id'), max(id)) from transactions_transaction;
This will set your id field to the correct serial sequence value in postgres for your table (i.e. the largest value of the id field of your existing records). This is crucial, as otherwise the value will be 1!
And that's it: from now on everything will be automatic again.