Django 使用内联对象复制对象

问题描述 投票:0回答:1

我有包含成分和成分数量的食谱模型。


MEAL_TYPES = (("midi", "Midi"), ("soir", "Soir"), ('all', 'Non spécifié'))

class Ingredient(models.Model):
    name = models.CharField(max_length=100)
    # Vous pouvez ajouter d'autres champs pour stocker des informations supplémentaires sur les ingrédients

    def __str__(self):
        return self.name


class Recipe(models.Model):
    """
    A model to create and manage recipes
    """

    user = models.ForeignKey(
        User, related_name="recipe_owner", on_delete=models.CASCADE
    )
    title = models.CharField(max_length=300, null=False, blank=False)
    description = models.CharField(max_length=500, null=False, blank=False)
    instructions = RichTextField(max_length=10000, null=False, blank=False)
    ingredients = RichTextField(max_length=10000, null=False, blank=False)
    image = ResizedImageField(
        size=[400, None],
        quality=75,
        upload_to="recipes/",
        force_format="WEBP",
        blank=False,
        null=False,
    )
    image_alt = models.CharField(max_length=100, default="Recipe image")
    meal_type = models.CharField(max_length=50, choices=MEAL_TYPES, default="all")

    calories = models.IntegerField(default=0)
    posted_date = models.DateTimeField(auto_now=True)
    newingredient = models.ManyToManyField(Ingredient, through='IngredientQuantite')

    class Meta:
        ordering = ["-posted_date"]

    def __str__(self):
        return str(self.title)



    
class IngredientQuantite(models.Model):
    recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
    ingredient = models.ForeignKey(Ingredient, on_delete=models.CASCADE)
    quantity = models.FloatField(default=0)
    unite = models.CharField(default="g", max_length=20, choices=[("g", "g"),('mg', 'mg'), ("ml", "ml"),('kg', "kg"), ('cl', "cl"), ('l', "l"), ('caf', "cuillère à café"), ('cas', "cuillère à soupe"), ('verre', "verre"), ('bol', "bol"), ('pincee', "pincée"), ('unite', "unité")])
    # Le champ "quantity" stocke la quantité de cet ingrédient dans la recette.
    # 'g', 'kg', 'ml', 'cl', 'l', 'cuillère à café', 'cuillère à soupe', 'verre', 'bol', 'pincée', 'unité'
    def __str__(self):
        return f"{self.quantity} {self.ingredient} in {self.recipe}"

为了添加新食谱,我有一个 AddRecipe 视图和一个 DuplicateRecipe 视图,在我介绍新成分之前它们可以完美地工作。

内联表单如下所示: end of my form with inlines form

它适用于除内联字段之外的所有字段。

这是我的views.py代码


class AddRecipe(LoginRequiredMixin, CreateView):
    template_name = "recipes/add_recipe.html"
    model = Recipe
    form_class = RecipeForm
    success_url = "/recipes/"

    def get_context_data(self, **kwargs):
        ctx=super().get_context_data(**kwargs)
        if self.request.POST:
            # ctx['form']=RecipeForm(self.request.POST)
            ctx['inlines']=IngredientQuantiteFormSet(self.request.POST)
        else:
            # ctx['form']=RecipeForm()
            ctx['inlines']=IngredientQuantiteFormSet()
        return ctx    
    
    def form_valid(self, form):
        form.instance.user = self.request.user
        ctx = self.get_context_data()
        inlines = ctx['inlines']
        
        if inlines.is_valid() and form.is_valid():
            req = form.save()
            inlines.instance = req
            print(inlines.instance)
            inlines.save()
        return super(AddRecipe, self).form_valid(form)


class DuplicateRecipe(LoginRequiredMixin, CreateView):
    template_name = "recipes/duplicate_recipe.html"
    model = Recipe
    form_class = RecipeForm
    success_url = "/recipes/"  # Redirigez l'utilisateur vers la liste des recettes après la duplication

    def get_initial(self):
        # Récupérez la recette d'origine par clé primaire
        original_recipe = get_object_or_404(Recipe, pk=self.kwargs["pk"])

        # Créez un dictionnaire d'initialisation pour le formulaire
        initial = {
            "title": f"Copy of {original_recipe.title}",
            "description": original_recipe.description,
            "ingredients": original_recipe.ingredients,
            "instructions": original_recipe.instructions,
            # Ajoutez d'autres champs liés à votre modèle Recipe ici
            "image": original_recipe.image,
            "image alt": original_recipe.image_alt,
            "meal_type": original_recipe.meal_type,
            "calories": original_recipe.calories,
        }

        return initial

    # def form_valid(self, form, ):
    #     form.instance.user = self.request.user
    #     return super(DuplicateRecipe, self).form_valid(form)
    
    
    def get_context_data(self, **kwargs):
        ctx=super().get_context_data(**kwargs)
        original_recipe = get_object_or_404(Recipe, pk=self.kwargs["pk"])
        if self.request.POST:
            # ctx['form']=RecipeForm(self.request.POST)
            ctx['inlines']=IngredientQuantiteFormSet(self.request.POST)
        else:
            # ctx['form']=RecipeForm()
            ctx['inlines']=IngredientQuantiteFormSet(instance=original_recipe)
        return ctx    
    
    def form_valid(self, form):
        form.instance.user = self.request.user
        ctx = self.get_context_data()
        inlines = ctx['inlines']
        
        if inlines.is_valid() and form.is_valid():
            req = form.save()
            inlines.instance = req
            inlines.save()
        return super(DuplicateRecipe, self).form_valid(form)

    # def form_invalid(self, form, formset):
    #     return self.render_to_response(self.get_context_data(form=form, formset=formset))

如果我进入 addRecipe 表单,该表单一开始是空的,验证表单会按预期创建菜谱。 如果我进入 DuplicateRecipe 表单,则当我验证重复菜谱表单时,所有字段都已完成,所有字段都保存在新菜谱中(除了内联菜谱)。 如果我删除“instance=original_recipe”,它的效果与 addRecipe 一样好,但没有我期望的重复自动完成功能。

有人知道为什么它不能与 instance=original_recipe 一起使用吗?或者如何解决表单验证问题?

python django many-to-many inline-formset
1个回答
0
投票

如果以后有人遇到同样的问题,我会详细说明我找到的解决方案。

谢谢这个链接,我设法更干净地重构我的代码。 然后,我使用我已经拥有的 get_initial 以及使用带有“initial”参数的自定义 inlineformset_factory 重新编写了重复函数。

#views.py
from typing import Any
from django.db.models.query import QuerySet
from django.shortcuts import render, get_object_or_404, redirect
from django.views.generic import (
    CreateView,
    ListView,
    DetailView,
    DeleteView,
    UpdateView,
)

from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin

# Create your views here.
from .models import Recipe, IngredientQuantite
from .forms import RecipeForm, IngredientQuantiteFormSet, IngredientQuantiteEditFormSet
from django.template.loader import render_to_string


from django.db.models import Q

class RecipeInline():
    form_class = RecipeForm
    model = Recipe
    template_name = "recipes/add_recipe_bis.html"

    def form_valid(self, form):
        form.instance.user = self.request.user
        named_formsets = self.get_named_formsets()
        if not all((x.is_valid() for x in named_formsets.values())):
            return self.render_to_response(self.get_context_data(form=form))

        self.object = form.save()

        for name, formset in named_formsets.items():
            formset_save_func = self.formset_ingredients_valid
            if formset_save_func is not None:
                formset_save_func(formset)
            else:
                formset.save()
        return redirect('recipes')

    def formset_ingredients_valid(self, formset):
        """
        Hook for custom formset saving.Useful if you have multiple formsets
        """
        ingredientQuantity = formset.save(commit=False)  # self.save_formset(formset, contact)
        for obj in formset.deleted_objects:
            obj.delete()
        for iq in ingredientQuantity:
            iq.recipe = self.object
            iq.save()



class RecipeCreate(RecipeInline, CreateView):
    
    def get_context_data(self, **kwargs):
        ctx = super(RecipeCreate, self).get_context_data(**kwargs)
        ctx['inlines'] = self.get_named_formsets()
        ctx['title'] = "Create Recipe"
        return ctx

    def get_named_formsets(self):
        if self.request.method == "GET":
            return {
                'ingredients': IngredientQuantiteFormSet(prefix='ingredients'),
            }
        else:
            return {
                'ingredients': IngredientQuantiteFormSet(self.request.POST or None, self.request.FILES or None, prefix='ingredients'),
            }

class RecipeUpdate(RecipeInline, UpdateView):
    def get_context_data(self, **kwargs):
        ctx = super(RecipeUpdate, self).get_context_data(**kwargs)
        ctx['inlines'] = self.get_named_formsets()
        ctx['title'] = "Edit Recipe"
        return ctx

    def get_named_formsets(self):
        return {
            'ingredients': IngredientQuantiteEditFormSet(self.request.POST or None, self.request.FILES or None, instance=self.object, prefix='ingredients'),
        }


from django.shortcuts import render, get_object_or_404, redirect
from django.forms import inlineformset_factory

class RecipeCopy(RecipeInline, CreateView):
    
    def get_initial(self):
        original_recipe = get_object_or_404(Recipe, pk=self.kwargs["pk"])

        # Créez un dictionnaire d'initialisation pour le formulaire
        initial = {
            "title": f"Copy of {original_recipe.title}",
            "description": original_recipe.description,
            "ingredients": original_recipe.ingredients,
            "instructions": original_recipe.instructions,
            "image": original_recipe.image,
            "image alt": original_recipe.image_alt,
            "meal_type": original_recipe.meal_type,
            "calories": original_recipe.calories,
        }

        return initial
    
    def get_context_data(self, **kwargs):
        ctx = super(RecipeCopy, self).get_context_data(**kwargs)
        ctx['inlines'] = self.get_named_formsets()
        ctx['title'] = "Copy Recipe"
        return ctx

    def get_named_formsets(self):
        original_recipe = get_object_or_404(Recipe, pk=self.kwargs["pk"])
        
        ingredients = IngredientQuantite.objects.all().filter(recipe=original_recipe)
        
        ingredientInits=[]
        for ingre in ingredients:
            ingredientToDict = {
                'ingredient': ingre.ingredient,
                'quantity': ingre.quantity,
                'unite': ingre.unite,
            }
            ingredientInits.append(ingredientToDict)
        
        print("ingredientInits", ingredientInits)
        
        
        IngredientQuantiteFormSet = inlineformset_factory(
            Recipe, IngredientQuantite, form=IngredientQuantiteForm,
            extra=len(ingredientInits)+1, can_delete=False,
            can_delete_extra=True
        )
        
        if self.request.method == "GET":
            return {
                'ingredients': IngredientQuantiteFormSet(prefix='ingredients', initial=ingredientInits),
            }
        else:
            return {
                'ingredients': IngredientQuantiteFormSet(self.request.POST or None, self.request.FILES or None, prefix='ingredients'),
            }

模板代码

add_recipe_bis.html
:

{% extends "base.html" %}


{% block title %}{{title}}{% endblock title %} 

{% block content %}
<div class="">
    <form method="post" enctype="multipart/form-data" class="p-2 form">
        <h1 class="text-center">{{title}}</h1>
        {% csrf_token %}
        {{ form.media }}
        {{ form|crispy }}
        {{ inlines.ingredients.management_form }}
        <div class="flex mb-3">
            <h2 class="">Ingredients</h2>
        </div>
        <div id="form_set" class="mb-3">
            {% for form in inlines.ingredients.forms %}
            <div class="form-row">
                    <div class='ingredients-container'>
                        {{ form|crispy }}
                        <input class="delete btn btn-danger" value="Delete" type="button" formnovalidate></input>
                    </div>
            </div>
            {% endfor %}
        </div>
        <div class="centered-button-container">
            <input class="btn btn-secondary addmore-button" type="button" value="Add an Item +" id="add_more">
        </div>
        <div id="empty_form" style="display:none">
            <div class="form-row">
                <div class='ingredients-container'>
                    {{ inlines.ingredients.empty_form|crispy }}
                    <input class="delete btn btn-danger" value="Delete" type="button" formnovalidate></input>
                </div>
            </div>
        </div>
        <div class="text-center">
            <button type="submit" class="btn btn-primary mt-2">{{title}}</button>
        </div>
    </form>
</div>


<script>

    $('#add_more').click(function(ev) {
        // var form_idx = $('#id_form-TOTAL_FORMS').val();
        // $('#form_set').append($('#empty_form').html().replace(/__prefix__/g, form_idx));
        // $('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1);
      ev.preventDefault();
      var count = $('#form_set').children().length;
      var tmplMarkup = $('#empty_form').html();
      var compiledTmpl = tmplMarkup.replace(/__prefix__/g, count);
      $('div#form_set').append(compiledTmpl);
      $('#id_ingredients-TOTAL_FORMS').attr('value', count+1);
    });

    if (`{{title}}`=="Edit Recipe") 
    {
        $('div > div[id*="DELETE"]').closest('.mb-3').parent().css('display', 'none');
    }

    $(document).on("click", ".delete", function() {
        var delcount = $('#form_set').children().length;
        $('#id_ingredients-TOTAL_FORMS').attr('value', delcount-1);

        if (`{{title}}`=="Edit Recipe") 
        {
            var $parent = $(this).closest('.ingredients-container'); // Parent element to hide
            var $checkbox = $parent.find('input[type="checkbox"]'); // Checkbox to check
            $checkbox.prop('checked', true); // Check the sibling checkbox
            $parent.css('display', 'none'); // Hide the parent element
        }
        else
        {
            $(this).parent().parent().remove();
        }
        
    });
</script>

{% endblock content %}
© www.soinside.com 2019 - 2024. All rights reserved.