Création d'une instance avec formulaire
Hello
Sur le projet marketplace j'ai un souci avec la création de mes produits depuis mon template product_new.html qui comporte un formulaire.
A partir de ce formulaire l'utilisateur rentre les différents champs pour créer un produit. Au début tout fonctionnait bien. Mais j'avais très peu de champs dans mon model produit (title, description, et différents choice)
Puis j'ai ajouté d'autres champs image, is_favorite... et un champ owner qui est lié à mon model utilisateur. Dans ma view 'ProductCreationView' j'ai fait en sorte que le champ owner se remplisse automatique via le nom de l'utilisateur. Et une fois l'instance crée j'ai une redirection vers mon template list.
Et depuis cela, ça ne fonctionne plus. Je n'arrive pas trouvé pourquoi. Je n'ai pas de message d'erreur et même en mettant des print et en faisant docker-compose logs
je n'arrive pas à voir d'où vient le problème.
Si vous avez une idée pour décoincer tout ça 🙏
mon modèle, je ne le mets pas entier avec tous les textchoices :
class Product(models.Model):
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False,
)
title = models.CharField(max_length=30)
description = models.TextField(max_length=500)
category = models.CharField(max_length=20, choices=CategoryChoices.choices)
sex = models.CharField(max_length=8, choices=SexChoices.choices)
size = models.CharField(max_length=3, choices=SizeChoices.choices)
condition = models.CharField(max_length=25, choices=ConditionChoices.choices)
color = models.CharField(max_length=20, choices=ColorChoices.choices)
quality = models.CharField(max_length=20, choices=QualityChoices.choices)
is_favorite = models.BooleanField(default=False)
is_reserved = models.BooleanField(default=False)
image = models.ImageField(upload_to="images_article/")
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
created_date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse("product_detail", args=[str(self.id)])
ma view :
class ProductCreationView(CreateView):
model = Product
form_class = ProductCreationForm
template_name = "swapping/product_new.html"
success_url = reverse_lazy("product_list")
# FIXME Problème de validation du formulaire avec le champ owner ne se rempli pas automatiquement avec le user connecté
def form_valid(self, form):
form.instance.owner = self.request.user
logger.info(form.cleaned_data)
print(form.is_valid())
print(form.cleaned_data)
print(form.errors)
return super().form_valid(form)
Et si besoin d'autre info le lien du code est ici
Salut Yann !
Avant d'aller plus loin, deux choses à vérifier :
1) As-tu bien appliqué les migrations dans ta base de données ?
2) As-tu ajouté les nouveaux champs dans ton formulaire (dans fields
) ?
Oui j'ai fait la migration. D'ailleurs j'arrive à créer des produits depuis l'admin donc j'imagine que les migrations se sont bien appliquées.
Et mon form ressemble à cela :
from django import forms
from .models import Product
class ProductCreationForm(forms.ModelForm):
class Meta:
model = Product
fields = (
"title",
"image",
"category",
"sex",
"size",
"color",
"condition",
"quality",
"description",
"owner",
)
widgets = {
"title": forms.TextInput(
attrs={
"class": "form-control form-control-sm",
"placeholder": "Title",
"aria-label": "Title",
}
),
"image": forms.FileInput(
attrs={
"class": "form-control",
"aria-label": "Image",
}
),
"category": forms.Select(
attrs={
"class": "form-select",
"aria-label": "Category",
},
choices=Product.CategoryChoices.choices,
),
"sex": forms.Select(
attrs={
"class": "form-select",
"placeholder": "Sex",
"aria-label": "Sex",
},
choices=Product.SexChoices.choices,
),
"size": forms.Select(
attrs={
"class": "form-select",
"placeholder": "Size",
"aria-label": "Size",
},
choices=Product.SizeChoices.choices,
),
"color": forms.Select(
attrs={
"class": "form-select",
"placeholder": "Color",
"aria-label": "Color",
},
choices=Product.ColorChoices.choices,
),
"condition": forms.Select(
attrs={
"class": "form-select",
"placeholder": "Condition",
"aria-label": "Condition",
},
choices=Product.ConditionChoices.choices,
),
"quality": forms.Select(
attrs={
"class": "form-select",
"placeholder": "Quality",
"aria-label": "Quality",
},
choices=Product.QualityChoices.choices,
),
"description": forms.Textarea(
attrs={
"class": "form-control form-control-sm",
"placeholder": "Description",
"aria-label": "Description",
}
),
}
J'ai l'impression que la method form_valid
n'est pas appellée. Car je n'ai aucun de mes print qui n'apparait quand je fais docker-compose logs
Mais est ce que c'est possible ça ?!? 🤔
Je n'ai pas encore trouvé d'où exactement venait le problème mais je m'en rapproche.
J'ai ajouté cette mehod dans ma view :
def form_invalid(self, form):
print(form.errors)
return super().form_invalid(form)
et j'avais ce message dans les logs :
<ul class="errorlist"><li>image<ul class="errorlist"><li>This field is required.</li></ul></li></ul>
swappingplace-web-1 | [17/Nov/2023 21:18:01] "POST /swapping/new/ HTTP/1.1" 200 9307
Je comprends pas trop cette erreur puisque j'uploadais bien une image au moment de la creéation de cettte instance. Mais je me suis dit que mon problème venait donc de mon template que j'ai un peu personnalisé. Mon template =
{% extends "_base.html" %}
{% load crispy_forms_tags %}
{% block title %}New Product{% endblock title %}
{% block content %}
<div class="container-sm p-5">
<h1 class="ms-2">New clothe</h1>
<div class="form-group">
<form action="{% url 'product_new' %}" enctype="multipart/form-data" method="post">
{% csrf_token %}
{{ form|crispy }}
<div class="row p-2">
<div class="col-md-6">
{{ form.title }}
</div>
</div>
<div class="row p-2">
<div class="col-md-6">
{{ form.image }}
</div>
</div>
<div class="row p-2">
<div class="col-md-2 mb-3">
<select class="form-select" name="{{ form.category.name }}">
<option disabled="" selected="" value="">{{ form.category.name|capfirst }}</option>
{% for category_value, category_label in form.category.field.choices %}
<option value="{{ category_value }}">{{ category_label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2 mb-3">
<select class="form-select" name="{{ form.sex.name }}">
<option disabled="" selected="" value="">{{ form.sex.name|capfirst }}</option>
{% for sex_value, sex_label in form.sex.field.choices %}
<option value="{{ sex_value }}">{{ sex_label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<select class="form-select" name="{{ form.size.name }}">
<option disabled="" selected="" value="">{{ form.size.name|capfirst }}</option>
{% for size_value, size_label in form.size.field.choices %}
<option value="{{ size_value }}">{{ size_label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="row p-2">
<div class="col-md-2 mb-3">
<select class="form-select" name="{{ form.color.name }}">
<option disabled="" selected="" value="">{{ form.color.name|capfirst }}</option>
{% for color_value, color_label in form.color.field.choices %}
<option value="{{ color_value }}">{{ color_label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2 mb-3">
<select class="form-select" name="{{ form.condition.name }}">
<option disabled="" selected="" value="">{{ form.condition.name|capfirst }}</option>
{% for condition_value, condition_label in form.condition.field.choices %}
<option value="{{ condition_value }}">{{ condition_label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<select class="form-select" name="{{ form.quality.name }}">
<option disabled="" selected="" value="">{{ form.quality.name|capfirst }}</option>
{% for quality_value, quality_label in form.quality.field.choices %}
<option value="{{ quality_value }}">{{ quality_label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="row p-2">
<div class="col-md-6">
{{ form.description }}
</div>
</div>
<button class="btn btn-primary m-2" type="submit">New</button>
<a %}"="" class="btn btn-outline-secondary" home"="" href="{% url ">Back</a>
</form>
</div>
</div>
{% endblock content %}
J'ai donc essayer avec le classique {{ form | crispy}}
et là tout fonctionne j'ai bien mon instance product associé à l'utilisateur connecté qui l'a crée.
Donc maintenant à voir quelles est la différence entre la personnalisation de mon template et le form | crispy
classique.
En cherchant un peu j'ai vu qu'il y avait plusieurs façon de rendre son formulaire avec crispy.
J'ai ajouté as_crispy_field
pour le champ image :
<div class="row p-2">
<div class="col-md-6">
{{ form.image|as_crispy_field }}
</div>
</div>
et ça réglé le problème. Bon je n'ai pas très bien compris pourquoi mais en tout cas ça fonctionne 😅 j'ai trouvé ça sur ce site, je vais regardé de plus près car il montre plusieurs façon de rendre son formulaire.
Salut Ludo !
Alors normalement tu n'as pas besoin de mettre le code de tous tes champs, tu devrais pouvoir laisser crispy
gérer tout ça.
Les filtres (|crispy et |as_crispy_field) sont assez spécifiques. le |crispy par exemple ne mets pas le tag form
, donc tu dois effectivement le mettre (et ne pas oublier de spécifier multipart/form-data
pour avoir la gestion de l'upload de l'image, bien important effectivement).
Personnellement j'utilise le tag {% crispy form %} qui me donne plus de contrôle sur le fait de mettre ou non le formulaire : https://django-crispy-forms.readthedocs.io/en/latest/crispy_tag_forms.html#crispy-tag-forms
Ça te permet de spécifier dans ton formulaire directement avec Python ce que tu veux faire :
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
class ExampleForm(forms.Form):
[...]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_id = 'id-exampleForm'
self.helper.form_class = 'blueForms'
self.helper.form_method = 'post'
self.helper.form_action = 'submit_survey'
self.helper.add_input(Submit('submit', 'Submit'))
Tu peux par exemple dire de ne pas mettre la balise form
pour le gérer de ton côté en HTML (Voir la doc) :
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
class ExampleForm(forms.Form):
[...]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
Salut Thibault, merci pour ta réponse. J'ai suivi tes recommandations et tout focntionne. Je trouve effectivement plus propre et pratique de gérer directement dans le forms.py. Au final mon forms ressemble à cela :
from django import forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit, Row, Column
from .models import Product
class ProductCreationForm(forms.ModelForm):
class Meta:
model = Product
fields = (
"title",
"image",
"category",
"sex",
"size",
"color",
"condition",
"quality",
"description",
)
widgets = {
# ...
"image": forms.FileInput(
attrs={
"class": "form-control",
"enctype": "multipart/form-data",
"aria-label": "Image",
}
),
}
#...
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
Column("title", css_class="form-group col-md-6"),
Column("image", css_class="form-group col-md-6"),
Row(
Column("category", css_class="col-md-2"),
Column("sex", css_class="col-md-2"),
Column("size", css_class="col-md-2"),
),
Row(
Column("color", css_class="col-md-2"),
Column("condition", css_class="col-md-2"),
Column("quality", css_class="col-md-2"),
),
Column("description", css_class="form-group col-md-6"),
Submit("submit", "New"),
)
Et mon template à cela maintenant, bien plus court !! :
{% block title %}New Product{% endblock title %}
{% block content %}
<div class="container-sm p-5">
<h1>New clothe</h1>
{% crispy form %}
</div>
{% endblock content %}
J'ai du coup une autre question. Dans la doc de django-crispy-forms, je n'ai pas vu l'utilisation du csrf_token. Est ce que c'est normal de ne ma pas le mettre ?
Et voilà ! Exactement comme ça que je gère mes formulaires et tu te retrouves avec juste un {% crispy form %}
dans ton HTML 🙌
L'intérêt de passer par du Python c'est que si tu as des formulaires qui sont similaires, tu peux utiliser les fonctionnalités de l'orienté objet pour te faire une classe de base et ensuite juste surcharger ce que tu souhaites modifier dans la __init__
.
Et le helper layout de Crispy Forms est très bien fait, ils ont pensé à tout, tu peux modifier les classes, changer les templates, etc.
Pour ta question concernant le csrf_token, il est rajouté automatiquement par Crispy Form (jette un coup d'oeil au HTML de la page, tu vas voir qu'il y a bien un input hidden). Tu peux choisir de gérer ça de ton côté si tu mets le form_tag
à False
: dans ce cas-là tu auras juste les inputs du formulaire mais tu devras ajouter la balise HTML form
et le csrf_token
toi-même.
Ok je te remercie beaucoup Thibault pour cette précision et de ton aide.
Inscris-toi
(c'est gratuit !)

Tu dois créer un compte pour participer aux discussions.
Créer un compte person