I'm using Factory Boy for testing a Django project and I've run into an issue while testing a model for which I've overridden the save method.
The model:
class Profile(models.Model):
active = models.BooleanField()
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE,
related_name='profiles')
department = models.ForeignKey(Department, null=True, blank=True)
category_at_start = models.ForeignKey(Category)
role = models.ForeignKey(Role)
series = models.ForeignKey(Series, null=True, blank=True)
status = models.ForeignKey('Status', Status)
def save(self, *args, **kwargs):
super(Profile, self).save(*args, **kwargs)
active_roles = []
active_status = []
for profile in Profile.objects.filter(user=self.user):
if profile.active:
active_roles.append(profile.role.code)
active_status.append(profile.status.name)
self.user.current_role = '/'.join(set(active_roles))
if 'Training' in active_status:
self.user.current_status = 'Training'
elif 'Certified' in active_status:
self.user.current_status = 'Certified'
else:
self.user.current_status = '/'.join(set(active_status))
self.user.save()
super(Profile, self).save(*args, **kwargs) ### <-- seems to be the issue.
The factory:
class ProfileFactory(f.django.DjangoModelFactory):
class Meta:
model = models.Profile
active = f.Faker('boolean')
user = f.SubFactory(UserFactory)
department = f.SubFactory(DepartmentFactory)
category_at_start = f.SubFactory(CategoryFactory)
role = f.SubFactory(RoleFactory)
series = f.SubFactory(SeriesFactory)
status = f.SubFactory(StatusFactory)
The test:
class ProfileTest(TestCase):
def test_profile_creation(self):
o = factories.ProfileFactory()
self.assertTrue(isinstance(o, models.Profile))
When I run the tests, I get the following error:
django.db.utils.IntegrityError: UNIQUE constraint failed: simtrack_profile.id
If I comment out the last last/second 'super' statement in the Profile save method the tests pass. I wonder if this statement is trying to create the profile again with the same ID? I've tried various things such as specifying in the Meta class django_get_or_create and various hacked versions of overriding the _generation method for the Factory with disconnecting and connecting the post generation save, but I can't get it to work.
In the meantime, I've set the strategy to build but obviously that won't test my save method.
Any help greatly appreciated.
J.
factory_boy uses the MyModel.objects.create() function from Django's ORM.
That function calls obj.save(force_insert=True): https://github.com/django/django/blob/master/django/db/models/query.py#L384
With your overloaded save() function, this means that you get:
Call super(Profile, self).save(force_insert=True)
[SQL: INSERT INTO simtrack_profile SET ...; ]
=> self.pk is set to the pk of the newly inserted line
Execute your custom code
Call super(Profile, self).save(force_insert=True)
This generates this SQL: INSERT INTO simtrack_profile SET id=N, ..., with N being the pk of the object
Obviously, a crash occurs: there is already a line with id=N.
You should fix your save() function, so that the second time you call super(Profile, self).save() without repeating *args, **kwargs again.
Notes:
Your code will break when you add an object through Django's admin, or anytime you'd use Profile.objects.create().
Since you don't modify self in your overloaded save() function, you should be able to remove the second call to super(Profile, self).save() altogether; although keeping it around might be useful to avoid weird bugs if you need to add more custom behavior later.
Related
I have two Django models:
from django.db import models
class Policy(models.Model):
status = models.IntegerField()
def save(self, *args, **kwargs):
quote = self.documents.get(document_type=DocumentType.quote)
if self.status == 0:
quote.delete()
elif self.status == 1:
new_quote_content = create_new_quote()
quote.s3_file.save(quote.name, File(new_quote_content))
super().save(*args, *kwargs)
class Document(models.Model):
policy = models.ForeignKey(
to=Policy,
null=True,
blank=True,
on_delete=models.CASCADE,
related_name="documents",
)
s3_file = models.FileField(
storage=S3Storage(aws_s3_bucket_name="policy-documents"),
upload_to=get_document_s3_key,
max_length=255,
)
I want to delete/update the document when the policy status is updated and I've overriden the save() method in Policy to do it. However, neither the doc deletion nor the doc's FieldFile update works in the save() method. If I move them to outside the save() method, everything works.
Does someone understand what's the issue here?
It is not calling the super method to save the model. To override a model it has to be something like this as given in the documentation of Django.
from django.db import models
class Blog(models.Model):
name = models.CharField(max_length=100)
tagline = models.TextField()
def save(self, *args, **kwargs):
do_something()
super().save(*args, **kwargs) # Call the "real" save() method.
do_something_else()
The save() was being called from a Policy ModelAdmin with a inlined Document form set. After it was run, Django executed the ModelAdmin's save_related() method, which saved the Document form set data, overwriting the Document changes I had just saved in save(). I solved it by overriding save_related() and deleting/updating the document after form.save_m2m() and form.save_formset().
I have created a (kind of) singleton to put all the app parameters in my database:
class SingletonModel(models.Model):
def save(self, *args, **kwargs):
self.pk = 1
super(SingletonModel, self).save(*args, **kwargs)
#classmethod
def load(cls):
return cls.objects.all().get()
class Meta:
abstract = True
class AppParameters(SingletonModel, models.Model):
DEFAULT_BALANCE_ALERT_THRESHOLD = models.PositiveIntegerField(default=5)
# other parameters...
It worked pretty well, until I tried to use one of these parameters in a default attribute of a model field:
class Convive(models.Model):
balance_alert_threshold = models.IntegerField(
default=AppParameters.load().DEFAULT_BALANCE_ALERT_THRESHOLD,
blank=True,
null=True)
This seemed to work too, but when I use a script to reinitialise local data, the first manage.py migrate produce a DoesNotExist since my Singleton does not exist yet.
It happens because of a file importing Convive model.
How would you solve this?
Is there a way to "delay" the evaluation of the default field?
Thanks.
EDIT
After posting this, I think that if my code processes db queries at import time, something may be wrong with it...
Create a method that returns the default value,
def get_default_balance_alert_threshold():
return AppParameters.load().DEFAULT_BALANCE_ALERT_THRESHOLD
then use that method as your default.
class Convive(models.Model):
balance_alert_threshold = models.IntegerField(
default=get_default_balance_alert_threshold,
blank=True,
null=True,
)
I want to get value from foreign key from another model and subscribe it.
It works when field is declared as ForeignKey but when field is declared as ManyToManyField it not works.
How can I do it?
Please help.
class Document(models.Model):
project = models.ForeignKey(Project)
text = models.ForeignKey(Text, null=True, blank=True)
language = models.ManyToManyField(Language, null=True, blank=True)
def save(self, *args, **kwargs):
self.text = self.project.text #WORKS
self.language = self.project.language.all() #NOT WORKS
super(Document, self).save(*args, **kwargs)
Does something like this works?
class Document(models.Model):
project = models.ForeignKey(Project)
text = models.ForeignKey(Text, null=True, blank=True)
languages = models.ManyToManyField(Language) # no need to null/blank + put the name on plural if using a many to many relation.
def save(self, *args, **kwargs):
self.text = self.project.text
super(Document, self).save(*args, **kwargs)
# super() must be called BEFORE feeding your many to many.
project_languages = self.project.languages.all()
self.languages.add(*project_languages)
When using a many to many field attribute, you need to wait for your instance is created before adding values in your many to many attribute.
You do this with add, contrary to foreign key fields.
You need to use .add() for initializing ManyToManyField
Change this line
# use plural naming convention when its ManyToMany relation `language` should be `languages` in your Document model
self.language = self.project.language.all()
to
# call save method of super class before adding values to many-to-many
super(Document, self).save(*args, **kwargs)
p_language = self.project.language.all()
self.language.add(*p_language)
So you must call save before adding the many-to-many relationship. Since add immediately affects the database, you do not need to save afterwards.
I added a new model to my app named SocialProfile, which is responsible for keeping social-related properties of a user which has a one-to-one relationship with UserProfile model. This is the SocialProfile model in models.py:
class SocialProfile(models.Model):
profile = models.OneToOneField('UserProfile', on_delete=models.CASCADE)
facebook_profiles = models.ManyToManyField('FacebookContact', related_name='synced_profiles', blank=True)
google_profiles = models.ManyToManyField('GoogleContact', related_name='synced_profiles', blank=True)
hash = models.CharField(max_length=30, unique=True, blank=True)
def save(self, *args, **kwargs):
if not self.pk:
hash = gen_hash(self.id, 30)
while SocialProfile.objects.filter(hash=hash).exists():
hash = gen_hash(self.id, 30)
self.hash = hash
def __str__(self):
return str(self.profile)
Right now, I keep a record for synced facebook & google profiles. Now, the problem is that creating new objects does not actually add any record in the database. I cannot create instances with scripts or admin. In case of scripts, the following runs without errors but no record is created:
for profile in UserProfile.objects.all():
sp = SocialProfile.objects.create(profile=profile)
print(profile, sp)
SocialProfile.objects.count()
The prints are done, and look correct and the count() returns 0. I try creating objects in admin, but I get the following error:
"{{socialprofile object}}" needs to have a value for field "socialprofile" before
this many-to-many relationship can be used.
I think that is another problem, because if I comment the Many-to-Many relationships, it is done, without error (still no new records). I mentioned it just if it might help.
I have checked the database, the tables are there, no new migrations are detected either.
Any help and idea about what could be the problem would be appreciated!
You've overwritten the save method so that it never actually saves anything. You need to call the superclass method at the end:
def save(self, *args, **kwargs):
if not self.pk:
...
return super(SocialProfile, self).save(*args, **kwargs)
I'm trying to override a save method so that on creation of one model, an instance of the second model is created. However, it looks like the secondary model that I'm trying to create (Restaurant in this example) is being created twice. Why is that?
models.py
class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
def __str__(self):
return "%s the place" % self.name
def save(self, *args, **kwargs):
super(Place, self).save(*args, **kwargs)
if Restaurant.objects.filter(place=self).count() == 0:
restaurant = Restaurant.objects.create(place=self)
class Restaurant(models.Model):
place = models.OneToOneField(
Place,
on_delete=models.CASCADE,
primary_key=True,
)
Your save method does not have proper indentation. I assume this was an error in cut and paste. With in that method.
if Restaurant.objects.filter(place=self).count() == 0:
restaurant = Restaurant.objects.create(restaurant=self)
This is essentially what get_or_create does but does atomically.
This method is atomic assuming correct usage, correct database
configuration, and correct behavior of the underlying database.
However, if uniqueness is not enforced at the database level for the
kwargs used in a get_or_create call (see unique or unique_together),
this method is prone to a race-condition which can result in multiple
rows with the same parameters being inserted simultaneously.
You can do the same in your own code of course with an atomic block but why bother. Just do
Restaurent.objects.get_or_create(place=self)
and isn't that place=self instead of restaurent=self as in your save method?
You can try:
obj.save(commit=False)
#change fields
obj.save()
First you will create save 'instance', do what you have to do, and then call the right save() method.