I'm trying to do Django class-based CreateView inline formsets.
I have a product model and a productImage model and productImage is inline
everything looks fine but after i submit my product the images that are selected in formset will not save.
Here is my code
models.py:
class Product(models.Model):
name=models.CharField(max_length=200, db_index=True),
slug=models.SlugField(max_length=200, db_index=True, allow_unicode=True),
description=models.TextField(blank=True),
vector_column=SearchVectorField(null=True, editable=False),
meta={"indexes": (GinIndex(fields=["vector_column"]),)}
category = models.ForeignKey(Category, related_name='products',
on_delete=models.CASCADE)
price = models.DecimalField(max_digits=10, decimal_places=2)
available = models.BooleanField(default=True)
def get_absolute_url(self):
return reverse('shop:product_detail', args=[self.id, self.slug])
class ProductImage(models.Model):
product = models.ForeignKey(Product, default=None, on_delete=models.CASCADE)
image = models.ImageField(upload_to='products/%y/%m/%d')
def __str__(self):
return self.product.name
views.py
class ProductCreate(StaffAccessMixin, ProductFormValidMixin, ProductFieldsMixin,
CreateView):
model = Product
def get_context_data(self, **kwargs):
context = super(ProductCreate, self).get_context_data(**kwargs)
if self.request.POST:
context['product_image_formset'] = ProductImageFormset(self.request.POST)
else:
context['product_image_formset'] = ProductImageFormset()
return context
template_name = 'products/product-create-update.html'
ProductFormValidMixin:
class ProductFormValidMixin():
def form_valid(self, form):
context = self.get_context_data()
product_image_formset = context['product_image_formset']
if self.request.user.is_superuser:
self.obj = form.save()
if product_image_formset.is_valid():
product_image_formset.instance = self.obj
product_image_formset.save()
return redirect('administration:product-update', pk=self.obj.pk)
else:
self.obj = form.save(commit=False)
self.obj.available = False
return super().form_valid(form)
ProductFieldsMixin:
class ProductFieldsMixin():
def dispatch(self, request, *args, **kwargs):
if request.user.is_superuser:
self.fields = ["name",
"slug",
"description",
"category",
"price",
"available",
"image",
]
else:
raise Http404
return super().dispatch(request, *args, **kwargs)
forms.py:
from django.forms.models import inlineformset_factory
from .models import Product, ProductImage
ProductImageFormset = inlineformset_factory(Product, ProductImage, fields=('image',),
extra=3)
Formset template:
<h5 class="text-info">Add Product Metas</h5>
{{ product_image_formset.management_form|crispy }}
{% for form in product_image_formset.forms %}
<tr class="{% cycle 'row1' 'row2' %} formset_row-{{product_image_formset.prefix }}">
{% for field in form.visible_fields %}
<td>
{# Include the hidden fields in the form #}
{% if forloop.first %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% endif %}
{{ field.errors.as_ul }}
{{ field|as_crispy_field }}
</td>
{% endfor %}
</tr>
{% endfor %}
Everything is ok and my product form is saved after submit but the images i select in my form set won't save
Problem solved!
This code will work for other types of data but not for images and files
to solve this problem get_context_data function should be like this:
def get_context_data(self, **kwargs):
context = super(ProductCreate, self).get_context_data(**kwargs)
if self.request.POST:
context['product_image_formset']=ProductImageFormset(self.request.POST,
**self.request.FILES**)
else:
context['product_image_formset'] = ProductImageFormset()
return context
In my first code self.request.FILES was missing :) and because of that images couldn't saved
Related
I'm trying to make a simple django app which is like a todo app, I want to add the percentage of task completed.
Here's my model.py
from django.db import models
from django.urls import reverse
class Task(models.Model):
title = models.CharField(max_length=200)
create_time = models.DateTimeField(auto_now_add=True)
complete_time = models.DateTimeField(blank=True, null=True)
status = models.BooleanField(default=False)
def __str__(self):
return self.title
and here's the template file
<form method="POST" action="/">
{% csrf_token %}
{{form}}
<input class="btn submit" type="submit" name="save">
</form>
{% for task in tasks %}
{% if task.status == True %}
<strike>{{task}}, {{task.complete_time}}</strike>
{% else %}
{% endif %}
{% endfor %}
and this is views.py file
def list(request):
queryset = Task.objects.order_by('complete_time','complete_time')
if request.method =='POST':
form = TaskForm(request.POST)
if form.is_valid():
form.save()
return redirect('/')
context = {
'tasks':queryset,
'form':form,
}
return render(request, 'tasklist.html', context)
use a class-based view
in views.py
from django.views.generic.list import ListView
class list(ListView):
model = Task
template_name = 'tasl_list.html'
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(**kwargs)
context['get_percentage_done'] = Task.objects.filter(status = True).count() * 100 / Task.objects.all().count()
return context
in your template
{{get_percentage_done}}
I'm trying to create a form for edit table Person, but in this form field Car must be filtered by selected Company. I'm very new to python/django so this may be a bad approach, high level suggestions welcome. Here is what I have:
I have 3 models in models.py:
from django.db import models
class Company(models.Model):
company = models.CharField(max_length=25)
def __str__(self):
return self.company
class Car(models.Model):
car = models.CharField(max_length=25)
company = models.ForeignKey(Company, on_delete = models.CASCADE)
def __str__(self):
return self.car
class Person(models.Model):
name = models.CharField(max_length=20)
car = models.ForeignKey(Car, on_delete = models.CASCADE)
def __str__(self):
return self.name
views.py:
def index(request):
people = Person.objects.all().select_related('car__company')
table = PersonTable(people)
return render(request, 'index.html', {'table': table})
def edit(request, id):
person = Person.objects.get(id = id)
car = Car.objects.get(id = person.car_id)
company = Company.objects.get(id = car.company_id)
if request.method == 'POST':
form = PersonForm(car.company_id, request.POST, instance=person)
if form.is_valid():
form.save()
return redirect('/person/')
else:
companys = CompanyForm(instance=company)
form = PersonForm(car.company_id, instance=person)
return render(request, 'person/edit.html', {'companys':companys, 'form':form})
def Company_select(request):
if request.method == 'POST':
form = CompanySelectForm(request.POST)
if form.is_valid:
return redirect('/person/edit/1/') # Return to form PersonForm with id=1.
# How to return selected Company?
# How to return to page with other id?
else:
form = CompanySelectForm()
return render(request, 'person/company_select.html', {'form':form})
urls.py:
app_name = 'person'
urlpatterns = [
path('edit/<int:id>/', views.edit),
path('company/', views.Company_select, name='company'),
path('', views.index),
]
forms.py
class CompanyForm(forms.ModelForm):
class Meta:
model = Company
fields = ('company',)
class CompanySelectForm(forms.Form):
company = forms.ModelChoiceField(queryset=Company.objects.all().order_by('company'))
class Meta:
fields = ('company',)
class PersonForm(forms.ModelForm):
class Meta:
model = Person
fields = ('name', 'car',)
def __init__(self, company, *args, **kwargs):
super(PersonForm, self).__init__(*args, **kwargs)
self.fields['car'].queryset = Car.objects.filter(company=company) #Filter by Company
edit.html
{% extends "base.html" %}
{% block header %}Edit{% endblock header %}
{% block content%}
<form action="" method="post">
{% csrf_token %}
{{ form.as_table }}
{{ companys.as_table }}
<button type="button" class="btn btn-default btn-xs">Select Company</button>
<button class="btn btn-primary" type="submit" name="submit">submit</button>
</form>
{% endblock content %}
company_select.html
{% extends "base.html" %}
{% block header %}Select Company{% endblock header %}
{% block content%}
<form action="" method="post">
{% csrf_token %}
{{ form.as_table }}
<button class="btn btn-primary" type="submit" name="submit">submit</button>
</form>
{% endblock content %}
This is my proposition : tested
form.py => add attribut that will call a JS function defined in your .html
class PersonForm(ModelForm):
class Meta:
model = Person
fields = ('name', 'company', 'car')
def __init__(self, *args, **kwargs,):
super(PersonForm, self).__init__(*args, **kwargs)
class SelectForm(Form):
name = CharField(label='name')
company = ChoiceField(label='company')
car = ChoiceField(label='car')
class Meta:
fields = ('name', 'company', 'car', )
def __init__(self, cars=None, *args, **kwargs, ):
super(SelectForm, self).__init__(*args, **kwargs)
self.fields['car'].choices = cars
self.fields['company'].choices = tuple(Company.objects.all().values_list(flat=False))
self.fields['company'].widget.attrs['onchange'] = "load_car()"
edit.html: => define the JS function
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form id='myform' method="POST">
{% csrf_token %}
{{ form }}
</form>
<button class="btn btn-primary" type="submit" name="submit">submit</button>
</body>
<script>
function load_car()
{
document.getElementById('myform').action = "/stack/myview";
document.getElementById("myform").submit();
}
</script>
</html>
Views.py:=> new view that will look for the company name in the database and return to the template
def index(request): # RF configuration FORM
form = PersonForm()
content = {'form': form}
return render(request, 'stack/index.html', content)
def edit(request, my_id):
if request.method == 'POST':
form = PersonForm(request.POST)
if form.is_valid():
form.save()
# Do ....
return HttpResponseRedirect(reverse('stack:index'))
else:
person = Person.objects.values_list().get(id__exact=my_id)
cars = []
[cars.append((x['car'], x['car'])) for x in
list(Car.objects.filter(company__exact=person[2]).values('car'))]
form = SelectForm(cars=cars, initial={'name': person[1], 'company': person[2], 'car': person[3]})
content = {'form': form}
return render(request, 'stack/edit.html', content)
else:
person = Person.objects.values_list().get(id__exact=my_id)
cars = []
[cars.append((x['car'], x['car'])) for x in list(Car.objects.filter(company__exact=person[2]).values('car'))]
form = SelectForm(cars=cars, initial={'name': person[1], 'company': person[2], 'car': person[3]})
content = {'form': form}
return render(request, 'stack/edit.html', content)
def myview(request):
cars = []
[cars.append((x['car'], x['car'])) for x in list(Car.objects.filter(company__exact=request.POST['company']).
values('car'))]
form = SelectForm(cars=cars, initial={'name': request.POST['name'], 'company': request.POST['company'], 'car': None})
content = {'form': form}
return render(request, 'stack/edit.html', content)
I install django-smart-selects
In models.py:
from smart_selects.db_fields import ChainedForeignKey
...
class Person(models.Model):
name = models.CharField(max_length=20)
company = models.ForeignKey(Company, on_delete = models.CASCADE)
car = ChainedForeignKey(
Car,
chained_field="company",
chained_model_field="company",
show_all=False,
auto_choose=True,
sort=True,
on_delete=models.CASCADE,
blank=True,
null=True)
def __str__(self):
return self.name
I have a problem when saving/creating new post for blog.
I already have Post model and each post has it's own category. so I have Category model too. In CreateView template I already got all categories from DB and displayed in select/option tag. The issue is I want to save category_id when I create new post. But I don't know how? How can I say if form POSTED get category_id and save it for Post model that has category_id field?
View:
class PostCreateForm(LoginRequiredMixin, CreateView):
model = Post
fields = ['title', 'short_text', 'long_text', 'post_pic']
def get_context_data(self, **kwargs):
context = super(PostCreateForm, self).get_context_data(**kwargs)
context['categories'] = Category.objects.all().order_by('id')
return context
def form_valid(self, form, **kwargs):
form.instance.author = self.request.user
return super().form_valid(form)
Model:
class Category(models.Model):
title = models.CharField(max_length=255)
def __str__(self):
return self.title
class Post(models.Model):
title = models.CharField(max_length=255)
short_text = models.TextField()
long_text = models.TextField()
post_pic = models.ImageField(default="post_pic.jpg",
blank=False, upload_to='post_pics')
date_published = models.DateTimeField(default=timezone.now())
date_upadated = models.DateTimeField(auto_now=True)
author = models.ForeignKey(User, on_delete=models.CASCADE)
category = models.ForeignKey(Category, default=None,
on_delete=models.CASCADE)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post-detail', kwargs={'pk': self.pk})`
post_form.html
{% extends 'blog/base.html' %}
{% load crispy_forms_tags %}
{% block title_head %}
New Post
{% endblock title_head %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<fieldset class="form-group">
<legend class="border-bottom mb-4 pb-2">New Post</legend>
{{ form|crispy }}
<select class="form-control" name="category">
{% for category in categories %}
<option value="{{ category.id }}">{{ category }}</option>
{% endfor %}
</select>
</fieldset>
<div class="form-group">
<button type="submit" class="btn btn-outline-info">Post</button>
</div>
</form>
{% endblock content %}
There are a few ways to achieve this. Here is the simplest:
If you expect the Catagories to remain the same then you can hard code them and use a CharField with the choices= keyword on your Post model.
class Post(models.Model):
CATEGORY_CHOICES = (
('Horror', 'Horror'),
('Romance', 'Romance'),
('Comedy', 'Comedy'),
)
category = models.CharField(choices=CATEGORY_CHOICES)
Then all you need to do is add 'category' to your fields list in the CreateView.
Side Note You should probs rename your CreateView it's a view not a form. This may cause confusion.
Solved!
we have access category in list of fields so it's useless to fetch all categories then displaying them in select-option tag in html page. just add category field in list of fields
Change PostCreateForm
change it from this
class PostCreateForm(LoginRequiredMixin, CreateView):
model = Post
fields = ['title', 'short_text', 'long_text', 'post_pic']
def get_context_data(self, **kwargs):
context = super(PostCreateForm, self).get_context_data(**kwargs)
context['categories'] = Category.objects.all().order_by('id')
return context
def form_valid(self, form, **kwargs):
form.instance.author = self.request.user
return super().form_valid(form)
to this
class PostCreateForm(LoginRequiredMixin, CreateView):
model = Post
fields = ['title', 'short_text', 'long_text', 'post_pic', 'category']
def get_context_data(self, **kwargs):
context = super(PostCreateForm, self).get_context_data(**kwargs)
return context
def form_valid(self, form, **kwargs):
form.instance.author = self.request.user
return super().form_valid(form)
This form was showing the radio-buttons when in the Function based views but changed to the checkbox when I introduced the Class based views, what could be the solution to this. I want them to be showing the radio-buttons again
forms.py
class ProductImagesForm(forms.ModelForm):
media = forms.ImageField(label='Image')
featured_image = forms.BooleanField(initial=True)
def __init__ (self, *args, **kwargs):
super(ProductImagesForm, self).__init__(*args, **kwargs)
self.fields['featured_image'] = forms.BooleanField( widget = forms.RadioSelect(attrs={'checked': 'true'}, choices=((self.prefix, 'featured'),)))
models.py
def product_download(instance, filename):
return '%s/%s' %(instance.product.slug, filename)
class ProductImages(models.Model):
product = models.ForeignKey(Product)
title = models.CharField(max_length=120)
media = models.ImageField(upload_to=product_download,
width_field='max_width',
height_field='max_height',
null=True, blank=True)
max_width = models.CharField(max_length=100, null=True, blank=True)
max_height = models.CharField(max_length=100, null=True, blank=True)
featured_image = models.BooleanField(default=True)
timestamp = models.DateTimeField(auto_now_add=True, auto_now=False)
updated = models.DateTimeField(auto_now_add=False, auto_now=True)
def __unicode__(self):
return unicode(self.media)
class Meta:
verbose_name = "product image"
verbose_name_plural = "product images"
template
<form enctype="multipart/form-data" action="" method="post"> {% csrf_token %}
{{ form.as_p }}
{{ formset.management_form }}
<div class="link-formset">
{% for obj in formset %}
{{ obj.as_p }}
{% endfor %}
</div>
views.py
def get(self, *args, **kwargs):
self.object = self.get_object()
form_class = self.get_form_class()
form = self.get_form(form_class)
formset = ImagesFormset(queryset=ProductImages.objects.none())
return self.render_to_response(self.get_context_data(form=form, formset=formset))
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form_class = self.get_form_class()
form = self.get_form(form_class)
formset = ImagesFormset(self.request.POST, self.request.FILES)
form_valid = form.is_valid()
formset_valid = formset.is_valid()
if form_valid and formset_valid:
seller = self.get_account()
form.instance.seller = seller
self.object = form.save()
media = formset.save(commit=False)
for img in media:
img.product = self.object
img.save()
formset.save()
return self.form_valid(form, formset)
else:
return self.form_invalid(form, formset)`
After some good researching and keen documentation reading, I got to the point here, I hope it will help some other folks too
forms.py
class ProductImagesForm(forms.ModelForm):
media = forms.ImageField(label='Image')
featured_image = forms.BooleanField(initial=True)
def __init__ (self, *args, **kwargs):
super(ProductImagesForm, self).__init__(*args, **kwargs)
self.fields['featured_image'] = forms.BooleanField(queryset=ProductImages.objects.all(), widget = forms.RadioSelect( attrs={'checked': True}, choices=((self.prefix, 'featured_image')),))
def add_prefix(self, field):
if field == 'featured_image': return field
else: return self.prefix and ('%s-%s' % (self.prefix, field)) or field
class Meta:
model = ProductImages
fields = ['media', 'featured_image']
ImagesFormset = modelformset_factory(ProductImages, fields=('media', 'featured_image'), extra=5)
template
{{ formset.management_form }}
<div class="link-formset">
{% for choice in formset %}
<div>
{{ choice.media }}
<input type="radio" name="{{choice.featured_image.label}}">{{ choice.featured_image.label }}</
</div>
{% endfor %}
</div>
My aim is to use Two models in One template. I have tried various ways around this and have had no success. Originally I had 2 views, 2 models and Two forms. After searching I found people using inlineformsets. So I dropped One of the Views and set-up the inlineformset.
This is currently where I am up to and seem to be hitting a wall.
The template renders to the browser and the 'object_list' part displays the database content as desired and the 'form' part renders the form and validates/saves the data correctly. The issue is with the 'formset'. No fields are rendered (I would expect to see a dropdown as the field is a foreignkey) and when the 'submit' button is pressed I get:
AttributeError at /settings/
'NoneType' object has no attribute 'save'
Any help in finding the error or pointers on alternative solutions would be greatly appreciated.
The Code:
models.py
from django.db import models
class RevisionSettings(models.Model):
global_revision_type = models.CharField(max_length = 5, unique=True, blank = True)
global_revision_description = models.CharField(max_length = 300, unique=True, blank = True)
class Meta:
ordering = ["global_revision_type"]
def __unicode__(self):
return u'%s %s' % (self.global_revision_type, self.global_revision_description)
class RevisionDefaultType(models.Model):
defaultrevisiontype = models.ForeignKey(RevisionSettings)
class Meta:
ordering = ["defaultrevisiontype"]
def __unicode__(self):
return unicode(self.defaultrevisiontype)
views.py
class RevisionSettingsView(CreateView):
template_name = 'settings/revisionsettings_view.html'
model = RevisionSettings
form_class = SettingsForm
success_url = reverse_lazy('globalsettings')
success_message = 'Successfully added your new revision type'
def get(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
formset = SettingsFormSet(instance = RevisionSettings)
return self.render_to_response(
self.get_context_data(form=form,
formset=formset))
def post(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
formset = SettingsFormSet(self.request.POST)
if 'rev_settings_form_1' in self.request.POST:
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
elif 'rev_settings_form_2' in self.request.POST:
if formset.is_valid():
return self.formset_valid(formset)
else:
return self.form_invalid(formset)
def form_valid(self, form):
self.object = form.save()
self.object.save()
return HttpResponseRedirect(self.get_success_url())
def formset_valid(self, formset):
self.object.save()
formset.instance = self.object
formset.save()
return HttpResponseRedirect(self.get_success_url())
def form_invalid(self, form, formset):
return self.render_to_response(self.get_context_data(form=form,formset=formset))
def get_context_data(self, **kwargs):
kwargs['object_list'] = RevisionSettings.objects.order_by('global_revision_type')
return super(RevisionSettingsView, self).get_context_data(**kwargs)
forms.py
from django import forms
from django.forms.models import inlineformset_factory
from .models import RevisionSettings, RevisionDefaultType
class SettingsForm(forms.ModelForm):
class Meta:
model = RevisionSettings
class DefaultSettingsForm(forms.ModelForm):
class Meta:
model = RevisionDefaultType
SettingsFormSet = inlineformset_factory(RevisionSettings, RevisionDefaultType)
revisionsettings_view.html
(I have removed most of the HTML styling to keep the information to the point)
{% extends 'base_private.html' %}
{% block content %}
{% for object in object_list %}
<tr>
<td align="center">{{ object.global_revision_type }}</td>
<td align="center">{{ object.global_revision_description }}</td>
<td align="center"><span class="glyphicon glyphicon-remove-circle"></span></td>
</tr>
{% endfor %}
<form action = '{{ action }}' method = 'POST' class="form-horizontal" role="form">
{% csrf_token %}
<tr>
<td align="center">
{{ formset.management_form }}
{% for form in formset %}
{{ form.id }}
{{ form.defaultrevisiontype.label_tag }}
{{ form.defaultrevisiontype }}
{% endfor %}
</td>
</tr>
<span class="input-group-addon">
<input type = 'submit' name = 'rev_settings_form_2' value = 'Update Default Revision Type' class = 'btn btn-success'>
</span>
<td align="center">{{ form.global_revision_type }}{{ form.global_revision_type.errors }}</td>
<td align="center">{{ form.global_revision_description }}{{ form.global_revision_description.errors }}</td>
</tr>
<span class="input-group-addon">
<input type = 'submit' name = 'rev_settings_form_1' value = 'Add Revision Type' class = 'btn btn-success'>
</span>
</form>
{% endblock %}
Formsets are overkill for two forms. This is actually not too hard but poorly documented. You can make both forms the same form type, just give a prefix.
def parent_apply(request):
if request.method == 'POST':
parent_form = SignupForm(request.POST, prefix="parent")
student_form = StudentApplyForm(request.POST, prefix="student")
if parent_form.is_valid() and student_form.is_valid():
parent = parent_form.save()
student = student_form.save(parent)
else: messages.error(request, "Please correct the errors marked in red.")
else:
parent_form = SignupForm(prefix="parent")
student_form = StudentApplyForm(prefix="student")
return render_to_response('template_path/forms.html', { 'parent_form':parent_form, 'student_form':student_form }, context_instance=RequestContext(request))
The forms are just regular Django forms, no special settings required. You can change the order on which they validate and save one even if the other did not validate if you choose.
In your HTML Template, wrap both forms in the same tag and they will submit at the same time. If you want your forms to go to different view functions, specify two different elements.
Thanks for all the help. The pointers really helped me come to this solution. The main change was to 'def get' as shown below. I dropped the formset and passed the forms this way.
def get(self, request, *args, **kwargs):
form = self.settings_form_class
formset = self.default_form_class
return self.render_to_response(self.get_context_data(form = form, formset = formset))
I was unaware this was possible! Thanks again.