Edit Model data using ModelForm: ModelForm validation error - django

I am working on my first django app. I am building an app that allows the user to rate beer. I want my user to be able to edit an entry they've already created. I take them to a ModelForm, and ask for their entry. When the POST method is called, my data is invalid. Here is my model.py:
from django.db import models
class Rating(models.Model):
beer_name = models.TextField()
score = models.DecimalField(max_digits=2, decimal_places=1)
notes = models.TextField(blank=True)
brewer = models.TextField(blank=True)
and forms.py:
from django import forms
from ratings.models import Rating
class RatingForm(forms.ModelForm):
class Meta:
model = Rating
fields = ['beer_name', 'score', 'notes', 'brewer']
Here is the views.py of my edit function:
def edit(request, row_id):
rating = get_object_or_404(Rating, pk=row_id)
if request.method == "POST":
form = RatingForm(request.POST, instance=rating)
if form.is_valid():
form.save()
return redirect(home)
else:
return HttpResponse("Invalid entry.")
else:
context = {'form': rating}
form = RatingForm(instance=rating)
return render(
request,
'ratings/entry_def.html',
context
)
However, every time the POST is called I get an "Invalid entry." HttpResponse, meaning my form.is_valid() is being returned False. Here is my template:
{% extends "base.html" %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-sm-10 col-sm-offset-1">
<h2>Edit Rating</h2>
<form role="form" method="post">
{% csrf_token %}
<p>Beer Name: <textarea>{{ form.beer_name }}</textarea></p>
<p>Score: <input type="text" name="BeerScore" value="{{ form.score }}"></p>
<p>Notes: <textarea>{{ form.notes }}</textarea></p>
<p>Brewer: <textarea>{{ form.brewer }}</textarea></p>
<p><button type="submit" class="save btn btn-primary">Save</button></p>
<p><button type="reset" class="btn btn-primary">Cancel</button></p>
</form>
</div>
</div>
</div>
{% endblock %}
So when I press my Save button, I am getting the response. Here is my edit url in urls.py:
urlpatterns = [
...
url(r'rating/edit/(?P<row_id>[0-9]+)/$', edit , name='rating-edit'),
]

You're wrapping fields in other fields which don't have name attributes. This is most likely causing the values to be excluded from the request.POST data.
Additionally, Django form fields all have a corresponding HTML widget. So there's really no need to render the HTML by hand, unless you need to.
Change your template code to:
<p>
{{ form.beer_name.label }}: {{ form.beer_name }}
{% if form.beer_name.errors %}
<br />{{ form.beer_name.errors }}
{% endif %}{# repeat for other fields as needed #}
</p>
<p>{{ form.score.label }}: {{ form.score }}</p>
<p>{{ form.notes.label }}: {{ form.notes }}</p>
<p>{{ form.brewer.label }}: {{ form.brewer }}</p>
<p><button type="submit" class="save btn btn-primary">Save</button></p>
<p><button type="reset" class="btn btn-primary">Cancel</button></p>
If you need to change the widget, do so at the form class level:
class RatingForm(forms.ModelForm):
class Meta:
model = Rating
def __init__(self, *args, **kwargs):
super(RatingForm, self).__init__(*args, **kwargs)
self.fields['notes'].widget = forms.Textarea()
This way, Django manages the attributes and binding for you.
Your view can also use some cleanup:
def edit(request, row_id):
rating = get_object_or_404(Rating, pk=row_id)
form = RatingForm(request.POST or None, instance=rating)
if request.method == "POST" and form.is_valid():
form.save()
return redirect(home)
context = {'form': rating}
return render(request, 'ratings/entry_def.html', context)

Related

Show django form in a designed page

How are you?
I m totally new in Django.I designed a page and I wanted to show a django form(edit or create) in a well designed HTML page. but i do not know how.
This is my owner method:
class OwnerUpdateView(LoginRequiredMixin, UpdateView):
"""
queryset to the requesting user.
"""
def get_queryset(self):
print('update get_queryset called')
""" Limit a User to only modifying their own data. """
qs = super(OwnerUpdateView, self).get_queryset()
return qs.filter(user=self.request.user)
class OwnerCreateView(LoginRequiredMixin, CreateView):
"""
Sub-class of the CreateView to automatically pass the Request to the Form
and add the owner to the saved object.
"""
# Saves the form instance, sets the current object for the view, and redirects to get_success_url().
def form_valid(self, form):
print('form_valid called')
object = form.save(commit=False)
object.user = self.request.user
object.save()
return super(OwnerCreateView, self).form_valid(form)
This is my views.py
class TaskUpdateView(OwnerUpdateView):
model = Task
fields = ["title", "text", "endDate"]
class TaskCreateView(OwnerCreateView):
model = Task
fields = ["title","text","status","endDate"]
This is my urls.py:
app_name='task'
urlpatterns = [
path('', views.TaskListView.as_view(), name='all'),
path('task/<int:pk>/', views.TaskDetailView.as_view(), name='detail'),
path('task/create', views.TaskCreateView.as_view(success_url=reverse_lazy('task:all')), name='task_create'),
path('task/update/<int:pk>', views.TaskUpdateView.as_view(success_url=reverse_lazy('task:all')),
name='task_update'),
path('task/delete/<int:pk>', views.TaskDeleteView.as_view(success_url=reverse_lazy('task:all')),
name='task_delete'),
path("accounts/login/", views.login, name='login'),
path("accounts/logout/", views.logout, name='logout'),
]
And this is the models.py:
class Task(models.Model):
title=models.CharField(max_length=250)
text=models.TextField()
user=models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=False)
status=models.ForeignKey('Status',on_delete=models.SET_NULL,null=True)
startDate=models.DateTimeField(auto_now_add=True)
endDate=models.DateField(null=True)
def __str__(self):
return self.title
class Status(models.Model):
name=models.CharField(max_length=250)
def __str__(self):
return self.name
And this is where these both function work:
{%extends 'base.html'%}
{% block content %}
<form action="" method="post">
{% csrf_token %}
<table>{{ form.as_table }}</table>
<input type="submit" value="Submit">
{# <input type="submit" onclick="window.location='{% url 'project:all' %}' ; return false;" value="Cancel">#}
</form>
{% endblock %}
How can i separate each element of this form and put it in a better designed page?
Thanks
There are two ways:
Option 1:
Loop over the form fields and render them individually:
{% for field in form %}
<div class="form-group">
{{ field.errors }}
{{ field.label_tag }} {{ field }}
{% if field.help_text %}
<span class="form-text">{{ field.help_text|safe }}</span>
{% endif %}
</div>
{% endfor %}
See docs for more.
Option 2:
You can manually create form inputs and give them the correct field name attribute. This gives you more control but also requires more work:
<div class="form-group"
<input
type="text"
name="title"
value="{{ form.title.value }}"
class="form-control {% if form.title.errors %}is-invalid{% endif %}"
>
{% if form.title.help_text%}
<span class="form-text">{{ form.title.help_text|safe }}</span>
{% endif %}
<div class="invalid-feedback">{{ form.title.errors }}</div>
</div>
<!-- now do the same for other fields -->

I can't post in Django

I can't post in Django, because when I import an image it doesn't work for me. it tells me that there's no file selected but I selected one.
This is the post model that I created, models.py file:
class Post(models.Model):
publisher = models.ForeignKey(User,on_delete=models.CASCADE)
caption = models.CharField(max_length=100)
date_created = models.DateTimeField(default=timezone.now())
image = models.ImageField(upload_to="post_images")
def __str__(self):
return self.caption
here's the forms.py file for the Post model:
from django import forms
from .models import Post
class CreatePostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['caption','image']
here's the Publish function in views.py file which implements the logic for my publish feature:
#login_required
def Publish(request):
if request.method == "POST":
form = CreatePostForm(request.POST,request.FILES)
if form.is_valid():
form.publisher = request.user
form.save()
return redirect("home")
else:
form = CreatePostForm()
return render(request,"posts/publish.html",{
"form":form,
})
int the urls.py file:
from django.urls import path
from . import views
urlpatterns = [
path('publish/',views.Publish,name="publish"),
path('',views.home,name="home"),
]
and here's in html template:
{% extends "users/base.html" %}
{% load crispy_forms_tags %}
{% block title %}create{% endblock title%}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-sm-6 col-md-5 authentification">
<div class="form-header">
<h1>
publish
</h1>
</div>
<div class="form-body">
<form method="POST">
<fieldset class="form-group" enctype="multipart/form-data">
{% csrf_token %}
{{ form|crispy }}
</fieldset>
<div class="form-group">
<button type="submit" class="btn btn-primary form-control">publish</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}
the Django version used is 2.2 and the Python 3.8. and Windows 10 Pro
You should alter the .publisher attribute of the .instance wrapped in the form, not the form itself, so:
#login_required
def Publish(request):
if request.method == 'POST':
form = CreatePostForm(request.POST,request.FILES)
if form.is_valid():
form.instance.publisher = request.user
form.save()
return redirect('home')
else:
form = CreatePostForm()
return render(request,'posts/publish.html',{
'form': form,
})
Since you are submitting both files and data, you should specify the enctype=… attribute [mdn] in the <form>:
<form enctype="multipart/form-data" method="POST">
…
</form>
Note: Django's DateTimeField [Django-doc]
has a auto_now_add=… parameter [Django-doc]
to work with timestamps. This will automatically assign the current datetime
when creating the object, and mark it as non-editable (editable=False), such
that it does not appear in ModelForms by default.
Note: It is normally better to make use of the settings.AUTH_USER_MODEL [Django-doc] to refer to the user model, than to use the User model [Django-doc] directly. For more information you can see the referencing the User model section of the documentation.

Django: Create an editable form for each instance within a queryset all in one page?

Sorry if this is too much code, but I believe it is all relevant to the question at hand.
Long story short, on my series_detail page, all episodes belonging to each series is shown, as well as forms to add a new episode or edit an existing one.
The edit episode form, however, requires an instance, which always returns the very first object of the episodes queryset. This is presumably because of the .first(), but I used this since you can only have one object passed as an instance.
What I am trying to achieve is:
after showing the edit modal next to each episode, show the instance of each episode instead of only the first episode.
save only that episode's instance after the form is filled
achieve this without redirecting to an edit page
models.py
class Series(models.Model):
name = models.CharField(max_length=100)
class Episode(models.Model):
series = models.ForeignKey(Series, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
episode_no = models.IntegerField(null=True)
description = models.TextField(max_length=500)
image = models.ImageField(upload_to='pics/episodes',)
forms.py
class EpisodeForm(forms.ModelForm):
name = forms.CharField()
description = forms.CharField(widget=forms.Textarea, required=False)
episode_no = forms.IntegerField()
class Meta:
model = Episode
fields = ['name', 'description', 'episode_no' ,'image']
views.py
def series_detail(request, pk):
try:
series = Series.objects.get(pk=pk)
except:
return render(request, 'error_404.html')
episodes = Episode.objects.filter(series=series).first()
if request.method == 'POST':
if 'addepisodeform' in request.POST:
e_form = EpisodeForm(request.POST, request.FILES, prefix='addepisode')
e_form.instance.series = Series.objects.get(pk=pk)
if e_form.is_valid():
e_form.save()
return redirect('series_detail', pk=pk)
messages.success(request, 'Episode was created')
else:
return redirect('series_detail', pk=pk)
messages.error(request, 'Episode was not created')
elif 'editepisodeform' in request.POST:
edit_e_form = EpisodeForm(request.POST, request.FILES, instance=episodes, prefix='editepisode')
edit_e_form.instance.series = Series.objects.get(pk=pk)
if edit_e_form.is_valid():
edit_e_form.save()
return redirect('series_detail', pk=pk)
messages.success(request, 'Episode was updated')
else:
return redirect('series_detail', pk=pk)
messages.error(request, 'Episode was not updated')
else:
e_form = EpisodeForm(prefix='addepisode')
edit_e_form = EpisodeForm(instance=episodes, prefix='editepisode')
context = {
'episodes': episodes,
'e_form': e_form,
'edit_e_form': edit_e_form
}
return render(request, 'series/series_detail.html', context)
def delete_episode(request, pk1, pk2):
try:
series = Series.objects.get(pk=pk1)
except:
return render(request, 'error_404.html')
try:
episode = Episode.objects.get(series=series, episode_no=pk2)
except:
return render(request, 'error_404.html')
episode.delete()
return redirect('series_detail', pk=pk1)
urls.py
path('series/<int:pk>/', views.series_detail, name='series_detail'),
path('series/<int:pk1>/episode/<int:pk2>/delete/', views.delete_episode, name='delete_episode'),
series_detail.html
<button type="submit" name="addepisodeform">
Add Episode
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ e_form }}
</form>
</button>
{% for episode in episodes %}
{{ episode.name }}
{{ episode.description}}
<img src="{{ episode.image.url }}" height="125px" width="300px" style="object-fit: cover;">
<button type="submit" name="editepisodeform">
Edit Episode
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ edit_e_form }}
</form>
</button>
{% endfor %}
Okay, so it turns out that formsets were the way to go after all. Thanks to Willem Van Onsem's answer, I decided to go that route after all and it worked like a charm.
A form can only edit one instance, but with formsets, I was able to not only edit each episode rather than just the first instance, but even create a new object and delete multiple objects at the same time!
views.py
#import the formset
from django.forms import modelformset_factory, inlineformset_factory
#formset
EpisodeFormset = inlineformset_factory(Series, Episode, fields=('name', 'episode_no', 'description', 'image'), can_delete=True, extra=1)
#post call
if request.method == 'POST':
if 'editepisodeform' in request.POST:
formset = EpisodeFormset(request.POST, request.FILES, instance=series, prefix='editepisode')
if formset.is_valid():
formset.save()
return redirect('series_detail', pk=pk)
else:
return redirect('series_detail', pk=pk)
else:
formset = EpisodeFormset(instance=series)
series_detail.html
<div>
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
<button type="submit" class="btn btn-primary" name="editepisodeform">Edit</button>
{{ form.as_p }}
{% for episode in episodes %}
{% if episode.episode_no == form.episode_no.value %}
Episode: {{ episode.episode_no }} <br>
Name: {{ episode.name }} <br>
<img src="{{ episode.image.url }}" height="125px" width="300px" style="object-fit: cover;"> <br> {% endif %}
{% endif %}
{% endfor %}
{% endfor %}
</form>
</div>

Error in as_crispy_field got passed an invalid or inexistent field

The page returns the error as_crispy_field got passed an invalid or inexistent field after SUBMIT Button is clicked. I was trying to submit the form when the error is raised. The record is SUCCESSFULLY saved into the database but an error is raised. A form for the template was created to set up the fields. The Form was instantiated in the views so that it could easily map the elements from the FORM to the TEMPLATE.
What caused the error and how can I resolve it?
FORM: Here is my form code
class ModelCreateDescriptionForm(forms.ModelForm):
name = forms.CharField(max_length=100)
description = forms.Textarea()
model_type = forms.ChoiceField(
widget = forms.Select,
choices = MODEL_TYPE_CHOICES,
)
def __init__(self, project_id=1, *args, **kwargs):
super(ModelCreateDescriptionForm, self).__init__(*args, **kwargs)
# project = Project.objects.get(id=project_id)
self.fields['model_type'].choices = MODEL_TYPE_CHOICES
self.fields['model_type'].required = True
class Meta:
model = Model
fields = ['name', 'description', 'model_type']
VIEW: Here is my view
def modelCreationDescription(request, **kwargs):
pk = kwargs.get('pk')
project = Project.objects.get(id=pk)
context = {}
context['project'] = project
if request.method == 'POST':
form = ModelCreateDescriptionForm(pk, request.POST)
name = request.POST.get(u'name')
description = request.POST.get(u'description')
model_type = request.POST.get(u'model_type')
if not(name and model_type):
messages.warning(request, f'Please fill model name and model type!')
if form.is_valid():
formDescription = form.save(commit=False)
try:
formDescription.name = name
formDescription.description = description
formDescription.model_type = model_type
formDescription.project = project
except:
messages.warning(request, f'Something wrong!')
return redirect('all-model-listview')
# save the form value
formDescription.save()
messages.success(request, f'Model description successfully created')
return render(request, 'models/pred_steps/modelSetTargetFeatures.html', {'model_form': formDescription })
else:
form = ModelCreateDescriptionForm(project_id=pk)
context = {
'form': form,
'project': project,
'create_model_description': True,
}
return render(request, 'models/pred_steps/modelCreateDescriptions.html', context)
HTML: This is the template that returning an error
<div class="card border-left-info mb-1 shadow">
<div class="col-xl-10 col-md-8 mb-1">
{% if project %}
<p><h5 class="text-info">Project: {{ project }}</h5></p>
{% endif %}
<form method="POST">
{% csrf_token %}
<fieldset class='form-group'>
{{ form.name|as_crispy_field }}
</fieldset>
<fieldset class='form-group'>
{{ form.description|as_crispy_field }}
</fieldset>
<fieldset class='form-group'>
{{ form.model_type|as_crispy_field }}
</fieldset>
<div class="form-group">
{% if project.id %}
<a class="btn btn-outline-secondary float-right" href="{% url 'all-model-listview' %}">Cancel</a>
{% endif %}
<button class="btn btn-outline-success" type="submit">Submit and Continue</button>
</div>
</form>
</div>
</div>
In case the next template is causing the error here are the codes
FORM:
class ModelSetTargetFeaturesForm(forms.ModelForm):
target_column_classification = forms.ChoiceField(
widget = forms.Select,
)
target_column_regression = forms.ChoiceField(
widget = forms.Select,
)
def __init__(self, project_id=1, *args, **kwargs):
super(ModelSetTargetFeaturesForm, self).__init__(*args, **kwargs)
project = Project.objects.get(id=project_id)
df = pd.read_csv(project.base_file, encoding='ISO-8859-1')
cols = df.columns
a_cols = np.column_stack(([cols, cols]))
self.fields['target_column_classification'].choices = a_cols
self.fields['target_column_classification'].required=True
self.fields['target_column_regression'].choices = a_cols
self.fields['target_column_regression'].required=True
# project = Project.objects.get(id=project_id)
class Meta:
model = Model
fields = ['target_column_classification', 'target_column_regression', ]
VIEW:
def modelSetTargetFeatures(request, **kwargs):
pk = kwargs.get('pk')
model = Model.objects.get(id=pk)
project = Model.objects.get(project=model.project.pk)
context = {}
context['model'] = model
context['project'] = project
if request.method == 'POST':
form = ModelSetTargetFeaturesForm(pk,request.POST)
target_column_classification = request.POST.get(u'target_column_classification')
target_column_regression = request.POST.get(u'target_column_regression')
if not(target_column_regression and target_column_classification):
messages.warning(request, f'Please fill model name and model type!')
if form.is_valid():
formTargetFeatures = form.save(commit=False)
formTargetFeatures.target_column_classification = target_column_classification
formTargetFeatures.target_column_regression = target_column_regression
# save the form value
formTargetFeatures.save()
messages.success(request, f'Model description successfully created')
return render(request, 'models/pred_steps/modelFeaturesSelection.html', {'model_form': formTargetFeatures })
else:
form = ModelSetTargetFeaturesForm(model=pk)
context = {
'form': form,
'project': project,
'model': model,
'create_model_description': True,
}
return render(request, 'models/pred_steps/modelSetTargetFeatures.html', context)
TEMPLATE:
<div class="card border-left-info mb-1 shadow">
<div class="col-xl-10 col-md-8 mb-1">
{% if project %}
<p><h5 class="text-info">Project: {{ project }}</h5></p>
<p><h5 class="text-info">Model: {{ name }}</h5></p>
{% endif %}
<form method="POST">
{% csrf_token %}
<fieldset class='form-group'>
{{ form.target_column_classification|as_crispy_field }}
</fieldset>
<fieldset class='form-group'>
{{ form.target_column_regression|as_crispy_field }}
</fieldset>
<div class="form-group">
{% if project.id %}
<a class="btn btn-outline-secondary float-right" href="{% url 'all-model-listview' %}">Cancel</a>
{% endif %}
<button class="btn btn-outline-success" type="submit">Next: Feature Selection</button>
</div>
</form>
</div>
</div>
Your view is making use of two different html templates:
'models/pred_steps/modelCreateDescriptions.html'
and
'models/pred_steps/modelSetTargetFeatures.html'
The first one is used for presenting the form, upon a GET request, and allowing the participant to input their data: return render(request, 'models/pred_steps/modelCreateDescriptions.html', context)
However, once the participant's data are POST'ed, this happens:
form = ModelCreateDescriptionForm(pk, request.POST)
# ... inside of if_valid
formDescription = form.save(commit=False)
# ...
formDescription.save()
# ...
return render(request, 'models/pred_steps/modelSetTargetFeatures.html', {'form': formDescription })
It's this second rendition that is causing problems, so I assume you are using crispy fields in the modelSetTargetFeatures.html template also. When rendering this other template, you seem to try to include formDescription as a form. However, formDescription is not a form, because form.save(commit=False) returns a Model object, not a form. Hence, django/crispy forms doesn't understand what's going on and rightly says that what you are trying to use as a form is not a valid form (since it's actually a Model instance). If you really do want to pass the form itself on to the other template, you can simply use {'form': form}.
You might also want to use a better name for your model than simply Model. It's very confusing and might cause bugs, since the name is identical to that of django.db.models.Model, which we subclass from for creating specific Models. Additionally, you might want to use the pythonic naming convention of using snakecase (e. g. my_function_based_view) for functions, and camelcase with the first letter capitalized for classes (e. g. MyFormClass).
(note that the above response refers to the code as you originally posted it - you've done some edits since I started responding, but the basic issue seems to be the same even after the edits)

Using modelformset_factory in a Django Class Based View

I am building a view that will let me update multiple fields on multiple objects at the same time. I'm doing this using ModelFormSet & modelformset_factory.
The template will be a table of forms with the object name to the left of the fields (see image below).
I found this example, but I am stuck on how to implement the class based view & template.
My Formset
class BaseFormSet(BaseModelFormSet):
def __init__(self, *args, **kwargs):
super(BaseFormSet, self).__init__(*args, **kwargs)
self.queryset = Reference.objects.filter(
start__isnull=True)
ReferenceFormSet = modelformset_factory(
Reference,
fields=('start', 'end'),
formset=BaseFormSet,
extra=0)
My View
class ReferenceFormSetView(LoginRequiredMixin, SuperuserRequiredMixin, FormView):
model = Reference
form_class = ReferenceFormSet
template_name = "references/references_form.html"
def form_valid(self, form):
for sub_form in form:
if sub_form.has_changed():
sub_form.save()
return super(ReferenceFormSetView, self).form_valid(form)
My Template
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="container">
<h1>{{ headline }}</h1>
<div class="row">
<form action="" method="post">
{% crispy form %}
<div class="">
<input type="submit" value="Submit" />
</div>
</form>
</div>
</div>
{% endblock content %}
Questions
The view seems odd with the Formset in the form_class. Is there a better way to handle this?
How can I access the instance name to display in the form?
I found a solution using a package called django-extra-views.
There is a class called ModelFormSetView which does exactly what I wanted. Here is my implementation (simplified) for others to use -
My View
class ReferenceFormSetView(ModelFormSetView):
model = Reference
template_name = "references/references_form.html"
fields = ['start', 'end']
extra = 0
def get_queryset(self):
return self.model.objects.all()
def get_success_url(self):
return reverse('references:formset')
def formset_valid(self, formset):
"""
If the formset is valid redirect to the supplied URL
"""
messages.success(self.request, "Updated")
return HttpResponseRedirect(self.get_success_url())
def formset_invalid(self, formset):
"""
If the formset is invalid, re-render the context data with the
data-filled formset and errors.
"""
messages.error(self.request, "Error dummy")
return self.render_to_response(self.get_context_data(formset=formset))
My Template
<form class="" method="post">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
<div class="">
{% for field in form %}
{{ field }}
{% endfor %}
</div>
{% endfor %}
<button type="submit" class="btn btn-primary">Save</button>
</form>