I have recipe model with ingredients and ingredients quantities.
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}"
To add new recipes, I have an AddRecipe view and a DuplicateRecipe view which work perfectly before I introduce the newIngredients.
The inlinesforms looks like this: end of my form with inlines form
It works for all the fields except the inline ones.
Here my views.py code
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))
If I go the addRecipe form, the form is empty at the start and the validation form create the recipe as expected. If i go the DuplicateRecipe form, all the fields are already complete when I valid the duplicateRecipe form, all fields are saved in a new recipe except the inlines. If I remove the "instance=original_recipe" it works as well as addRecipe but without the autocompletion that I expect for a duplication.
Anyone have an idea that why it doesn't work with the instance=original_recipe ? or how to fix the form validation problem?
In case anyone has the same problem later, I will detail the solution I found.
Thanks this link, I managed to refactor my code more cleanly. I then redid the duplicate function using get_initial which I already had as well as using a custom inlineformset_factory with the "initial" parameter.
#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'),
}
The template code 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 %}