Django: Help overwriting a model's save() method to auto populate a CharField based on a function on object creation

三世轮回 提交于 2020-02-25 07:02:09

问题


I have a model (Meal) with several many to many fields (proteins, carbohydrates and fats), which I recently added a 'name' CharField to. I wanted to allow the user to enter a name for a meal, but if they do not enter a name, I want the name to be populated automatically based on the function definitions I have in the model which just concatenate the names of the foods in one string. I was trying to follow this guide.

Now, if the Meal already exists, what I have actually works fine. However, if it does not exist, the food_name for each item appears not to have saved yet because they are empty. I put the super(Meal,self).save() statement before my if not self.name: statement in the hopes that this would save the object to the database so that the food_names could then be retrieved, but it does not work and instead when I do save the name is saved as '(0)'. What I am looking for the name to populate via the __str__ function as 'Pork Tenderloin, Spinach (steamed/boiled), Potato (Red, medium) (3)', for example.

Also, if I don't call super(Meal,self).save() before the if statement, I actually get a 'maximum recursion depth exceeded' error.

Can anyone tell me if there is a way to auto populate this name field based on my function definition on object creation as I've described?

I am new to Django, and have limited experience with Python, so thank you very much for any help you can provide.

Here is my model:

class Meal(models.Model):
    class Meta:
        verbose_name_plural = 'Meal Plan Meals'

    name = models.CharField(max_length=255,blank=True,null=True)
    proteins = models.ManyToManyField(to="Food", limit_choices_to={'food_type': 'P'},blank=True,related_name='food_proteins')
    carbohydrates = models.ManyToManyField(to="Food", limit_choices_to={'food_type': 'C'}, blank=True, related_name='food_carbohydrates')
    fats = models.ManyToManyField(to="Food",  limit_choices_to={'food_type': 'F'}, blank=True, related_name='food_fats')

    def all_foods(self):
       return list(self.proteins.all())+list(self.carbohydrates.all())+list(self.fats.all())

    def __str__(self):
        return ', '.join(map(lambda x: x.food_name, self.all_foods()))+f' ({len(self.all_foods())})'

    def save(self):
        super(Meal,self).save()
        if not self.name:
            self.name = self.__str__()
            self.save()

Edit:

The main reason I am trying to do this is because I need to be able to sort on the string returned by the __str__ method in my Meal model, but after posting another question on stack overflow here I found out that I believe this is not possible. It seems you can only sort on fields in your model, and so I chose instead to add a name field (where I additionally decided that I could allow the user to name the meal something if they wanted to instead of letting it be auto populated). Currently when there are many meals it is impossible to find any single one because the ordering is based on pk's which appears totally random in terms of the names of the items and makes it virtually unusable. Here is a picture for reference:

Currently, I create meal objects through the django admin ui only. Here is the code for my MealAdmin in admin.py:

class MealAdmin(admin.ModelAdmin):
    model = Meal
    save_as = True
    #search bar - search by food name
    search_fields = ['name','proteins__food_name','carbohydrates__food_name','fats__food_name',]
    fieldsets = (
        (None, {
            'fields': ('name', 'proteins', 'carbohydrates', 'fats',),
            'description': "Note: If you do not choose a name for your meal the meal will be named according to all of the foods it contains. Ex: 'Chicken Breast,Rice (white) (cooked),Avocado'"
        }),
    )

and a picture for reference:

So, if anyone has any idea how to cause the save function to auto-populate the name field on creation based on my __str__ function - or any other work around, it would be greatly appreciated!


回答1:


If you want something to happen when creating a new instance through admin interface, the way to do is different than just overriding your model save method: you have to override the save_model method in your admin declaration.

You could try something like below:

# admin.py
class MealAdmin(admin.ModelAdmin):

    def save_model(self, request, obj, form, change):        
        obj.save()
        form.save_m2m()
        # your custom stuff goes here
        if not obj.name:
            obj.name = obj.__str__()
            obj.save()



回答2:


You can try using save signal to do your custom stuff when creating your object. For instance:

from django.db.models.signals import post_save

class Meal(models.Model):

    # [...]

    @classmethod
    def update_name(cls, *args, **kwargs):
        if not cls.name:
            cls.name = self.__str__()
            cls.save()

post_save.connect(Meal.update_name, sender=Meal)

However, this will be called whenever save is being called, not only at creation time (since there is not a post_create signal). Not a big issue, but not 100% satisfying. Hope this one will work!


EDIT

Another try with signals, but with m2m_changed this time. We will try to call update_name each time one of the m2m fields has been updated, because the issue seems to be that those fields being saved independantly from Meal model, everything is asynchronous thus updated data of those fields are not available when needed.

from django.db.models.signals import m2m_changed

class Meal(models.Model):

    # [...]

    @classmethod
    def update_name(cls, *args, **kwargs):
        if not cls.name:
            cls.name = self.__str__()
            cls.save()

m2m_changed.connect(Meal.update_name, sender=Meal.proteins.through)
m2m_changed.connect(Meal.update_name, sender=Meal.carbohydrates.through)
m2m_changed.connect(Meal.update_name, sender=Meal.fats.through)

With this solution, each time one of those m2m field is updated, name will be updated too thanks to update_name method.

Source : https://docs.djangoproject.com/en/3.0/ref/signals/#m2m-changed




回答3:


Actually you can not assign m2m fields if the object does not exist already (because before there is no primary key associated to your instance yet). You first need to save the object, then set a value for m2m fields. Then you can save your object and customize your name field. That's why you have got empty values.

Just a question, why don't you customize your __str__ function instead? It would be (in my opinion of course!) simpler to maintain and use than override save base function. Something like:

def __str__(self):
    if self.name: return self.name
    return ', '.join(map(lambda x: x.food_name, self.all_foods()))+f' ({len(self.all_foods())})'

So if there is a custom name value set by user, fine you will use that; if not, you will return the whole thing. I am never comfortable in overriding default save functions actually when you can do otherwise!



来源:https://stackoverflow.com/questions/59442018/django-help-overwriting-a-models-save-method-to-auto-populate-a-charfield-ba

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!