ManyToManyField is empty in post_save() function - python

When a new record is added to a table, I have to execute a SQL statement on an external database. This query includes the use of a ManyToManyField. So I just connected the function like this:
post_save.connect(post_save_mymodel, sender=MyModel)
And in my post_save_mymodel() function, here is what I do:
def post_save_mymodel(sender, instance, created, *args, **kwargs):
if created:
for e in instance.my_m2mfield.all():
# Query including "e".
But, too bad, instance.my_m2mfield.all() is always empty! Even though they should contain some elements! I tried to get the new element by doing
new_element = MyModel.objects.get(pk=instance.pk)
but it doesn't change anything, I've still got the same issue...
Wny help/advice?

This is because you first save your instance and after that you add m2m relations to it. This is how ManyToMany fields work in Django data models. Django needs to know the ID of the items that should be connected with m2m relations.
I think that your code looks like this:
instance = MyModel.objects.create(some_field=some_value) # post save signal triggered here
instance.my_m2mfield = my_set_of_m2m_models
You need to connect your handler to the django.db.models.signals.m2m_changed signal. See the docs. For example:
def post_save_mymodel(sender, instance, action, reverse, *args, **kwargs):
if action == 'post_add' and not reverse:
for e in instance.my_m2mfield.all():
# Query including "e"
m2m_changed.connect(post_save_mymodel, sender=MyModel.my_m2mfield.through)

Related

How to insert an item into a Queryset which is sorted by a numeric field and increment the value of the field of all subsequent items

Let´s say, there is a Django model called TaskModel which has a field priority and we want to insert a new element and increment the existing element which has already the priority and increment also the priority of the following elements.
priority is just a numeric field without any special flags like unique or primary/foreign key
queryset = models.TaskModel.objects.filter().order_by('priority')
Can this be done in a smart way with some methods on the Queryset itself?
I believe you can do this by using Django's F expressions and overriding the model's save method. I guess you could instead override the model's __init__ method as in this answer, but I think using the save method is best.
class TaskModel(models.Model):
task = models.CharField(max_length=20)
priority = models.IntegerField()
# Override the save method so whenever a new TaskModel object is
# added, this will be run.
def save(self, *args, **kwargs):
# First get all TaskModels with priority greater than, or
# equal to the priority of the new task you are adding
queryset = TaskModel.objects.filter(priority__gte=self.priority)
# Use update with the F expression to increase the priorities
# of all the tasks above the one you're adding
queryset.update(priority=F('priority') + 1)
# Finally, call the super method to call the model's
# actual save() method
super(TaskModel, self).save(*args, **kwargs)
def __str__(self):
return self.task
Keep in mind that this can create gaps in the priorities. For example, what if you create a task with priority 5, then delete it, then add another task with priority 5? I think the only way to handle that would be to loop through the queryset, perhaps with a function like the one below, in your view, and call it whenever a new task is created, or it's priority modified:
# tasks would be the queryset of all tasks, i.e, TaskModels.objects.all()
def reorder_tasks(tasks):
for i, task in enumerate(tasks):
task.priority = i + 1
task.save()
This method is not nearly as efficient, but it will not create the gaps. For this method, you would not change the TaskModel at all.
Or perhaps you can also override the delete method of the TaskModel as well, as shown in this answer, but I haven't had a chance to test this yet.
EDIT
Short Version
I don't know how to delete objects using a similar method to saving while keeping preventing priorities from having gaps. I would just use a loop as I have shown above.
Long version
I knew there was something different about deleting objects like this:
def delete(self, *args, **kwargs):
queryset = TaskModel.objects.filter(priority__gt=self.priority)
queryset.update(priority=F('priority') - 1)
super(TaskModel, self).delete(*args, **kwargs)
This will work, in some situations.
According to the docs on delete():
Keep in mind that this [calling delete()] will, whenever possible, be executed purely in
SQL, and so the delete() methods of individual object instances will
not necessarily be called during the process. If you’ve provided a
custom delete() method on a model class and want to ensure that it is
called, you will need to “manually” delete instances of that model
(e.g., by iterating over a QuerySet and calling delete() on each
object individually) rather than using the bulk delete() method of a
QuerySet.
So if you delete() a TaskModel object using the admin panel, the custom delete written above will never even get called, and while it should work if deleting an instance, for example in your view, since it will try acting directly on the database, it will not show up in the python until you refresh the query:
tasks = TaskModel.objects.order_by('priority')
for t in tasks:
print(t.task, t.priority)
tr = TaskModel.objects.get(task='three')
tr.delete()
# Here I need to call this AGAIN
tasks = TaskModel.objects.order_by('priority')
# BEFORE calling this
for t in tasks:
print(t.task, t.priority)
# to see the effect
If you still want to do it, I again refer to this answer to see how to handle it.

How to query with the ID of the parent instance using **kwargs in Django?

I am currently implementing soft deletion for all models in my database. The idea is that when an instance gets deleted, it actually gets archived with all of its children. If the user tries to create an instance that is identical to the archived one, the archived one gets undeleted along with all of its children instead of creating a new instance.
To do this, I am using django-safedelete where I am making a BaseModel with an overwritten save() method that looks something like this:
def save(self, *args, **kwargs):
# get the foreign key id
foreign_key_id = self.foreign_field.id
# execute a query by that id and some other params
'''I don't know how to do this'''
As to how to do it, I thought I could construct a kwargs dictionary that consists of pairs of <field>:value where <field> = self._meta.get_field(some_field.name) and value = getattr(self, some_field.name).
So how do I add the foreign_key_id to kwargs? I know there is this syntax: Model.objects.filter(foreign_field__id=value)
...but I don't know how to replicate that to put into kwargs the way I'm doing it.
Likewise, is there a better way to do this in general? I don't want to hard-code too many things, which is why I didn't just do this individually for each of the models that I have.
Thank you so much in advance.

Django select existing related record, or create new inline

I have a django model 'User' with a foreignkey to a related model 'Group'.
I am using a modelForm to render the form for creating a user, which allows the user to select a group from a dropdown of existing groups.
However, I'd like the option for the user to create a 'new' Group within that form if they don't find one they want in the list.
I know I could do an inline form, but I'm not sure how to accomplish that while retaining the ability to optionally select an existing related record.
Any advice?
After many hours of rearch, I have found a solution.
I tried many things, including overriding the clean() function on my form, however that required removing immutability and was messy to get the validation right.
Ultimately my solution was so sublcasss ModelChoiceField. In the model choice field, override the to_python() method with your logic to create related object if it does not exist. In addition, I passed this field a queryset paramater so that in my form I was able to pass the newly created object only to this form instance, but not show on every users form.
class FlexibleModelChoiceField(ModelChoiceField):
def __init__(self, queryset, *args, **kwargs):
super(FlexibleModelChoiceField, self).__init__(queryset, *args, **kwargs)
self.queryset = queryset
def to_python(self, value):
try:
# Logic to get or create the model instance object
return model_instance_object
except (ValueError, self.queryset.model.DoesNotExist):
raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')

Django - NEVER update a column when saving

I am trying to use citus data (https://www.citusdata.com/) with Django.
Most everything is working so far, except trying to save a model that has already been saved:
NotSupportedError: modifying the partition value of rows is not allowed
This is because django always includes every single field in the update SQL, even if that field has not changed.
In Citus, you must pick a field to be your partitioning field, and then you cannot change it. So, when I'm saving an object, it doesn't like that the partition key is in the update statement, even if it didn't change.
I know that you can pass the update_fields keyword arg to the save method, but I'm wondering if I can somehow tell django to NEVER include a field when updating?
Django does not provide this functionality "out of the box". You could override the save method of your class to set all fields other than your partition field as the value for update_fields
def save(self, **kwargs):
kwargs.setdefault('update_fields', ['field1', 'field2'])
return super(Class, self).save(**kwargs)
A more dynamic option, if you do not want to update this method everytime you change the fields of your class, would be to use the Meta API to get all fields of the class and exclude your partition field
def save(self, **kwargs):
kwargs.setdefault(
'update_fields',
[f.name for f in self.__class__._meta.get_fields() if f.name != 'partition_field']
)
return super(Class, self).save(**kwargs)
There are several other methods by which Django will attempt to update your models. Maybe a base class that all your models inherit from that implement these methods would work

Django ManyToMany access in save() function

I want to create additional objects based on user input during the .save() method of my model. the info is stored in a ManyToMany-relation .available_specs.
My code works, if one presses save twice, but not at the first time. The problem is described in the comments here Django foreign key access in save() function, too.
Here is what I do:
def save(self, *args, **kwargs):
my_save_result = super().save(*args, **kwargs) #save first to create an id
self.refresh_from_db() #just as a test, did not help at all
#the following line is the problem:
# in the first save, available_specs.all() returns []
# when I re-open the model and save again, I get the correct list
for spec in self.available_specs.all():
VehicleSpec.objects.get_or_create(vehicle=self, spec=spec)
return my_save_result
I'm using Python 3.5 and Django 1.9
Edit:
I tried to use the post_save() signal using the following code (and removed the overridden save()-method):
#receiver(post_save, sender=VehicleModel)
def create_dependent_vehicle_specs(sender, **kwargs):
vehicle = kwargs['instance']
#in the first save-process I get an empty list here,
#after I hit save again, the code works.
print(vehicle, vehicle.available_specs.all())
for spec in vehicle.available_specs.all():
VehicleSpec.objects.get_or_create(vehicle=vehicle, spec=spec)
Edit2: m2m_changed did the trick:
#receiver(m2m_changed, sender=VehicleModel)
def create_dependent_vehicle_specs(sender, **kwargs):
vehicle = kwargs['instance']
for spec in vehicle.available_specs.all():
VehicleSpec.objects.get_or_create(vehicle=vehicle, spec=spec)
m2m_changed.connect(create_dependent_vehicle_specs, sender=VehicleModel.available_specs.through) # #UndefinedVariable
BTW: A related question is here: Django accessing ManyToMany fields from post_save signal
You need to use a m2m_changed signal. You should not be calling another models' get_or_create in another models' save method

Categories