How to get a duplicate instance on save in Django? - python

Supposing I already have a created instance of a Django's model. I want to get another instance that is a duplicate in the database. Is there a universal way to do it without finding out which unique index is responsible for this duplication:
class MyModel(models.Model):
...
instance = MyModel(...)
print(instance.id) # None
...
duplicate = get_duplicate(instance) # what should I type here instead of get_duplicate?
print(duplicate.id) # Some ID in DB
I want the function get_duplicate to not depend on the internal structure of the model. Also I don't want to modify the model.
For example, if I need to find out a duplicate exists I can do instance.save(). In case of IntegrityError there's a duplicate. But how to find out which one?

When you instantiate a model as such MyModel(...), you get an unsaved instance. To propagate it to the database, you have to call .save() on it, at which point instance.id will be set to something. You could also do MyModel.objects.create(...) as a shortcut.
Now, to answer the question, to duplicate a record you already have in the database; set its id to None, and save it again.
instance = MyModel.objects.get(id=1)
instance.id = None
instance.save()
print(instance.id) # 2
If I understand your question correctly, you want .save() to create two database rows instead of one? I don't understand why you'd want that, or how you'd make it useful, but you'd do it by overriding .save() on your model:
class MyModel(models.Model):
...
def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
instance.id = None
instance.save()
return instance
As for finding a duplicate instance, this is much more difficult. You're going to have to decide what makes an instance a "duplicate". If it's a user model, for instance, maybe if only the email address is the same, it's a duplicate, but if it's a transaction instance, then EVERY field has to be the same.
As this is to inextricably linked to the model's type, you will want to put this on the model itself. I'll write a toy example below:
class MyModel(models.Model):
a = models.CharField(unique=True, ...)
b = models.CharField(unique=True, ...)
c = models.CharField(...)
def get_duplicates(self):
return type(self).filter(
a=self.a,
b=self.b,
)
In this example, a and b must match, but c doesn't have to.
You've already defined what makes a model a "duplicate" with your unique and unique together keys, so your .get_duplicates() function should be informed by those.

Related

How do I ensure that a Foreignkey field is always referenced in a Django model?

I was thinking of creating an instance of a foreignkey field and referring it every time an instance of a model is created, but I didn't find any solution for this. Usually we need to create a model of foreignkey type and then refer to it from the model, but I want it to automatically create one foreignkey instance from the beginning of and always refer to it.
To provide an example let's say we've 2 model fields called User and WeeklySchedule. Everytime an instance of a User is created,another corresponding instance of a WeeklySchedule model is also created that will be referred to by the instance of the User.
We can do this inside save() method of the User model.
def save(self, *args, **kwargs):
schedule = create_and_or_get_new_weekly_schedule()
""" where create_and_or_get_new_weekly_schedule either creates a new
instance or gets that of the foreignkey model
"""
self.availability_schedule_tutor = schedule
super().save(*args, **kwargs)
We can also set the on_delete option of the foreignkey field to models.PROTECT or models.RESTRICT to make sure it never loses reference. Also make sure to set null=True or else an instance of a user can never be created.
Something like the following:
weekly_schedule = WeeklySchedule()
# Set your weekly_schedule fields here
weekly_schedule.save()
user = User()
# Set your user fields here
user.weekly_schedule = weekly_schedule
user.save()

Access primary key/id value during creation of a Django record

For my model I use default auto-incremented PrimaryKey/id field. I also have a field called serialNumber which according to design should reference id and generate it's own value based on id value.
For example if id is 1234 then serialNumber field should be a string '00001234'.
class MyModel(models.Model):
...
serialNumber = models.TextField(max_length=8)
...
I have no trouble updating serialNumber after the creation of record in DB, but my specific case requires serialNumber to be set together with object initial creation because right after it I return the record via REST API.
I tried using signals, but pre_save returns None
#receiver(pre_save, sender=MyModel)
def my_pre_save(sender, instance, **kwargs):
print(instance.id)
On the other hand if I do:
#receiver(post_save, sender=MyModel)
def my_post_save(sender, instance, **kwargs):
print(instance.id)
I receive the value for id but it's too late because right after .save() my REST API returns response with created object and I don't want to add code that manages serialNumber to serializer.
Overriding .save() method for MyModel didn't work either:
def save(self, *args, **kwargs):
print(self.id)
super(MyModel, self).save(*args, **kwargs)
Id field still returns None.
Anyone has an idea how to access id value during initial creation of a record?
Well there is no way you can access the ID before saving it.
ID will be generated by database so you have to call save method so django sends the data to database and then you'll have your ID
This has been explained in django documentations too:
Auto-incrementing primary keys
If you really need to access the ID which is a unique identifier, you can create one manually.
You can add a field called my_id and then give it a unique value before saving the data (you just need to check if that value already exist or not and if it does, then just create another one). This way you'll be able to get a unique ID which you'll be able to find that row with in future.

Accessing "self" in save method of class-based model

I have two models that look like this:
class ModelOne(models.Model):
foo = models.CharField(max_length=25)
def save(self,*args,**kwargs):
a = ModelTwo.objects.get(pk=arbitrary_pk)
a.somefield.add(self) # I am worried about this line here
super(ModelOne,self).save(*args,**kwargs)
class ModelTwo(models.Model):
somefield = models.ManyToManyField(ModelOne)
The line where I am adding self to a.somefield is the line I am worried about. How can I do this without error? Currently, I am getting:
ValueError: Cannot add "<ModelOne>": the value for field "modelone" is None
Thanks in advance
You can't do that because when you call .add() you have yet to save your model. That means that the model may not have been created (so it doesn't have an ID yet).
Basically you're telling Django to update the Foreign Key with something that doesn't exist yet (NULL), which will error out. You need to make sure the model has been created before you can set the foreign key.
try moving the a.somefield.add(self) to AFTER the super() call.
You cannot save many to may field before calling actual save method, you modify code like,
def save(self,*args,**kwargs):
super(ModelOne,self).save(*args,**kwargs) # Here your self has been saved
a = ModelTwo.objects.get(pk=arbitrary_pk)
a.somefield.add(self) # Now your self can be add as ManyToMany as it is already saved in db
I hope this help.
Add the instance to the many to many field after calling the save method.
class ModelOne(models.Model):
foo = models.CharField(max_length=25)
def save(self,*args,**kwargs):
super(ModelOne,self).save(*args,**kwargs)
a = ModelTwo.objects.get(pk=arbitrary_pk)
a.somefield.add(self) #add self to the object manytomany.
a.save() #save the object.
class ModelTwo(models.Model):
somefield = models.ManyToManyField(ModelOne)
You need to save the self object first. The many to many relation needs to have the related object saved in the database first, inorder to define the relationship. Then, define the relationship using a.somefield.add(self). Then, save the a object. Otherwise, the relation won't be committed in the database.
I ended up utilizing post_save to get this to work.

Django foreign key new model instance used as default

I'm wondering whether it's possible to do something like the following
class ModelA(models.Model):
my_field = models.ForeignKey('UniqueKey', to_field='key' default=UniqueKey.create(key=KeyGen()))
# KeyGen() returns a new random key
In essence, I have a number of models that have a unique 32 digit key as one of their fields. I have created the UniqueKey model to manage all of those keys and make sure no duplicates ever arise. What I'm trying to do is create a new UniqueKey instance and save it to the database whenever I make a new ModelA instance.
Is this possible or am I just going about this the wrong way?
I think your best approach will be to use the post_save signal when an instance of your model is created. https://docs.djangoproject.com/en/1.9/ref/signals/#post-save
i.e.
class ModelA(models.Model):
my_field = models.ForeignKey('UniqueKey', to_field='key')
def set_default_uniquekey_for_modela(sender, instance, created, raw, using, update_fields):
if created is True:
new_unique_key = UniqueKey(key=KeyGen())
new_unique_key.save()
instance.my_field = new_unique_key
instance.save()
post_save.connect(set_default_uniquekey_for_modela, sender=ModelA)
So when an instance of ModelA is created, it will exectute set_default_uniquekey_for_modela(...) to create a new UniqueKey instance and assign that to the instance.

Ordered ManyToManyField that can be used in fieldsets

I've been working through an ordered ManyToManyField widget, and have the front-end aspect of it working nicely:
Unfortunately, I'm having a great deal of trouble getting the backend working. The obvious way to hook up the backend is to use a through table keyed off a model with ForeignKeys to both sides of the relationship and overwrite the save method. This would work great, except that due to idiosyncrasies of the content, it is an absolute requirement that this widget be placed in a fieldset (using the ModelAdmin fieldsets property), which is apparently not possible.
I'm out of ideas. Any suggestions?
Thanks!
In regard to how to set up the models, you're right in that a through table with an "order" column is the ideal way to represent it. You're also right in that Django will not let you refer to that relationship in a fieldset. The trick to cracking this problem is to remember that the field names you specify in the "fieldsets" or "fields" of a ModelAdmin do not actually refer to the fields of the Model, but to the fields of the ModelForm, which we are free to override to our heart's delight. With many2many fields, this gets tricky, but bear with me:
Let's say you're trying to represent contests and competitors that compete in them, with an ordered many2many between contests and competitors where the order represents the competitors' ranking in that contest. Your models.py would then look like this:
from django.db import models
class Contest(models.Model):
name = models.CharField(max_length=50)
# More fields here, if you like.
contestants = models.ManyToManyField('Contestant', through='ContestResults')
class Contestant(models.Model):
name = models.CharField(max_length=50)
class ContestResults(models.Model):
contest = models.ForeignKey(Contest)
contestant = models.ForeignKey(Contestant)
rank = models.IntegerField()
Hopefully, this is similar to what you're dealing with. Now, for the admin. I've written an example admin.py with plenty of comments to explain what's happening, but here's a summary to help you along:
Since I don't have the code to the ordered m2m widget you've written, I've used a placeholder dummy widget that simply inherits from TextInput. The input holds a comma-separated list (without spaces) of contestant IDs, and the order of their appearance in the string determines the value of their "rank" column in the ContestResults model.
What happens is that we override the default ModelForm for Contest with our own, and then define a "results" field inside it (we can't call the field "contestants", since there would be a name conflict with the m2m field in the model). We then override __init__(), which is called when the form is displayed in the admin, so we can fetch any ContestResults that may have already been defined for the Contest, and use them to populate the widget. We also override save(), so that we can in turn get the data from the widget and create the needed ContestResults.
Note that for the sake of simplicity this example omits things like validation of the data from the widget, so things will break if you try to type in anything unexpected in the text input. Also, the code for creating the ContestResults is quite simplistic, and could be greatly improved upon.
I should also add that I've actually ran this code and verified that it works.
from django import forms
from django.contrib import admin
from models import Contest, Contestant, ContestResults
# Generates a function that sequentially calls the two functions that were
# passed to it
def func_concat(old_func, new_func):
def function():
old_func()
new_func()
return function
# A dummy widget to be replaced with your own.
class OrderedManyToManyWidget(forms.widgets.TextInput):
pass
# A simple CharField that shows a comma-separated list of contestant IDs.
class ResultsField(forms.CharField):
widget = OrderedManyToManyWidget()
class ContestAdminForm(forms.models.ModelForm):
# Any fields declared here can be referred to in the "fieldsets" or
# "fields" of the ModelAdmin. It is crucial that our custom field does not
# use the same name as the m2m field field in the model ("contestants" in
# our example).
results = ResultsField()
# Be sure to specify your model here.
class Meta:
model = Contest
# Override init so we can populate the form field with the existing data.
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance', None)
# See if we are editing an existing Contest. If not, there is nothing
# to be done.
if instance and instance.pk:
# Get a list of all the IDs of the contestants already specified
# for this contest.
contestants = ContestResults.objects.filter(contest=instance).order_by('rank').values_list('contestant_id', flat=True)
# Make them into a comma-separated string, and put them in our
# custom field.
self.base_fields['results'].initial = ','.join(map(str, contestants))
# Depending on how you've written your widget, you can pass things
# like a list of available contestants to it here, if necessary.
super(ContestAdminForm, self).__init__(*args, **kwargs)
def save(self, *args, **kwargs):
# This "commit" business complicates things somewhat. When true, it
# means that the model instance will actually be saved and all is
# good. When false, save() returns an unsaved instance of the model.
# When save() calls are made by the Django admin, commit is pretty
# much invariably false, though I'm not sure why. This is a problem
# because when creating a new Contest instance, it needs to have been
# saved in the DB and have a PK, before we can create ContestResults.
# Fortunately, all models have a built-in method called save_m2m()
# which will always be executed after save(), and we can append our
# ContestResults-creating code to the existing same_m2m() method.
commit = kwargs.get('commit', True)
# Save the Contest and get an instance of the saved model
instance = super(ContestAdminForm, self).save(*args, **kwargs)
# This is known as a lexical closure, which means that if we store
# this function and execute it later on, it will execute in the same
# context (i.e. it will have access to the current instance and self).
def save_m2m():
# This is really naive code and should be improved upon,
# especially in terms of validation, but the basic gist is to make
# the needed ContestResults. For now, we'll just delete any
# existing ContestResults for this Contest and create them anew.
ContestResults.objects.filter(contest=instance).delete()
# Make a list of (rank, contestant ID) tuples from the comma-
# -separated list of contestant IDs we get from the results field.
formdata = enumerate(map(int, self.cleaned_data['results'].split(',')), 1)
for rank, contestant in formdata:
ContestResults.objects.create(contest=instance, contestant_id=contestant, rank=rank)
if commit:
# If we're committing (fat chance), simply run the closure.
save_m2m()
else:
# Using a function concatenator, ensure our save_m2m closure is
# called after the existing save_m2m function (which will be
# called later on if commit is False).
self.save_m2m = func_concat(self.save_m2m, save_m2m)
# Return the instance like a good save() method.
return instance
class ContestAdmin(admin.ModelAdmin):
# The precious fieldsets.
fieldsets = (
('Basic Info', {
'fields': ('name', 'results',)
}),)
# Here's where we override our form
form = ContestAdminForm
admin.site.register(Contest, ContestAdmin)
In case you're wondering, I had ran into this problem myself on a project I've been working on, so most of this code comes from that project. I hope you find it useful.

Categories

Resources