I'm receiving the following error when I click the save button on my inline formset using Crispy Forms:
[{'id': ['This field is required.']}, {'id': ['This field is required.']}, {'id': ['This field is required.']}, {}, {}]
The formset is bound but not valid because of the missing id, but I'm not sure how to set the id.
#views.py
class View(LoginRequiredMixin, TemplateView):
template_name = "example.html"
MyFormSet = modelformset_factory(
model=MyModel,
form=MyModelForm,
formset=MyModelFormset,
can_delete=True,
extra=1,
fields=('field_1','field_2', ))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
my_formset = self.MyFormSet()
context['my_formset'] = MyModel.objects.all().order_by('field_1')
return context
def post(self, request, *args, **kwargs):
my_formset = self.MyFormSet(request.POST, request.FILES)
if my_formset.is_valid():
try:
my_formset.save()
except:
messages.add_message(request, messages.ERROR, 'Cannot delete: this parent has a child 1 !')
else:
context = self.get_context_data()
context['my_formset'] = my_formset
return render(request, self.template_name, context)
return HttpResponseRedirect(reverse_lazy("example"))
#forms.py
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ['field_1', 'field_2']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(
Row(
Column('field_1'),
Column('field_2'),
Column('DELETE'),
)
)
#template
<form action="" enctype="multipart/form-data" method="post">{% csrf_token %}
{{ my_formset.management_form|crispy }}
{% for form in my_formset.forms %}
{% crispy form form.helper %}
{% endfor %}
<button class="btn btn-success" type="submit">Save</button>
</form>
When rendering forms with layouts as part of formset, you must set render_hidden_fields = true. See more notes in the documentation.
https://django-crispy-forms.readthedocs.io/en/latest/form_helper.html
Related
So I've been playing with Django formsets for about a week and I'm about ready to throw out Django altogether. Just kidding. Kinda. :). I have the standard inline formset implementation working finally as I documented here. How do I properly implement Django formsets with a CreateView ( Class Based View )? Hope it helps someone. Now I'm trying to leverage this code by trying to pass a specific queryset to it as shown below.
class PlayerFormSet(PlayerFormSet,BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(PlayerFormSet, self).__init__(*args, **kwargs)
self.queryset = Player.objects.filter(team_pk=20)
As shown above, if I hard code a value, no worries it works to populate my formset. Although hardcoding this value is of little value to the implementation. I have researched form_kwargs, played around with numerous implementations as shown throughout SO, about passing a value but nothing I try works other than the code above for one team obviously and I can't for the life of me figure out how to do something like:
class PlayerFormSet(PlayerFormSet,BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(PlayerFormSet, self).__init__(*args, **kwargs)
self.queryset = Player.objects.filter(self.object.team_pk)
Or...
class PlayerFormSet(PlayerFormSet,BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(PlayerFormSet, self).__init__(*args, **kwargs)
self.queryset = Player.objects.filter(self.instance.team_pk)
I know the pk, it can clearly be passed here in a hardcoded fashion...And I have the value accessible in my view. I just can't figure out how to pass it to the formset to get the proper queryset. Is this even possible? This seems to be way more challenging than it should be. Or of course it could be me. Thanks in advance for any ideas.
Update for more thorough analysis.
My Models....
class Team(models.Model):
team_name = models.CharField(null=True)
class Player(models.Model):
player_name = models.CharField(null=True)
team_pk = models.IntegerField(null=True)
team = models.ForeignKey("Team",null=True,on_delete=models.CASCADE)
forms.py
PlayerFormSet = inlineformset_factory(Team, Player, extra=0, fields=['player_name',])
class PlayerFormSet(PlayerFormSet,BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(PlayerFormSet, self).__init__(*args, **kwargs)
self.queryset = Player.objects.filter(team_pk=20)
views.py
class UpdateTeamView(LoginRequiredMixin,CreateView):
model = Team
form_class = TeamForm
template_name = 'update_team.html'
def get_context_data(self, **kwargs):
context = super(UpdateTeamView, self).get_context_data(**kwargs)
dropdown = self.request.GET.get("dropdown", None)
queryset = Player.objects.filter(team_pk=dropdown)
player_form = PlayerFormSet(queryset=queryset)
context['player_form'] = player_form
return context
def form_valid(self, form, player_form):
self.object = form.save()
player_form.instance = self.object
player_form.save()
def form_invalid(self, form, player_form):
return self.render_to_response(
self.get_context_data(form=form,
player_form=player_form,
))
def post(self, request, *args, **kwargs):
if "cancel" in request.POST:
return HttpResponseRedirect(reverse('Team:team_main_menu'))
else:
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
player_form = PlayerFormSet(self.request.POST)
if (form.is_valid() and player_form.is_valid()):
return self.form_valid(form, player_form)
else:
return self.form_invalid(form, player_form)
My Template...
<form method="POST" enctype="multipart/form-data" id="forms">
{% csrf_token %}
{{ player_form.management_form }}
{{ player_form.non_form_errors }}
{% for hidden in player_form.management_form %}
{{ hidden }}
{% endfor %}
{% for form in player_form.forms %}
{{ form.id }}
<div class="inline {{ player_form.prefix }}">
<div class="leftwidth22">
<div class="width52">
<h2 class="floatright23">Player Name - </h2>
</div>
</div>
<div class="rightwidth53">
<h2 class="width70">
{{ form.player_name }}
</h2>
</div>
{% if player_form.non_form_errors %}
<h3 class="spacer12">
{{ player_form.non_form_errors }}
</h3>
{% endif %}
{% if form.player_name.errors %}
<h3 class="spacer12">
{{ form.player_name.errors }}
</h3>
{% endif %}
{% endfor %}
</form>
If this is the only view using the inlineformset then you can ditch the BaseInlineFormset override and just give the queryset to the formset call in the view.
Because this is an inlineformset it can take an instance of the parent (instead of a queryset of the objects themselves).
def get_context_data(self, **kwargs):
context = super(UpdateTeamView, self).get_context_data(**kwargs)
dropdown = self.request.GET.get("dropdown", None)
instance = Team.objects.get(pk=dropdown)
player_form = PlayerFormSet(instance=instance)
context['player_form'] = player_form
return context
Now the get portion should work. The post and form_valid need fixing too. You don't need to check the form, as all forms will be validated by the formset. Try:
def form_valid(self, player_form):
player_form.save()
return HttpResponseRedirect('/')
def form_invalid(self, player_form):
return self.render_to_response(self.get_context_data(player_form=player_form))
def post(self, request, *args, **kwargs):
if "cancel" in request.POST:
return HttpResponseRedirect(reverse('Team:team_main_menu'))
else:
instance = Team.objects.get(team_pk=dropdown)
player_form = PlayerFormSet(self.request.POST,instance=instance)
if player_form.is_valid():
return self.form_valid(player_form)
else:
return self.form_invalid(player_form)
I don't know how to display formset individually.
I understand how to display all at once.
But I tried some model field names, but it didn't work.
#views
class UserEdit(generic.UpdateView):
model = User
form_class = forms.UserUpdateForm
template_name = 'accounts/accounts_edit.html'
success_url = reverse_lazy('accounts:edit')
def get_object(self):
return get_object_or_404(User, pk=self.request.user.user_id)
#forms
class ProfileUpdateForm(forms.ModelForm):
class Meta:
model = profile
fields = ('first_name','last_name','birthday')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs['class'] = 'form-control'
ProfileFormSet = inlineformset_factory(User,profile,form=ProfileUpdateForm,extra=0)
class UserUpdateForm(mixins.ModelFormWithFormSetMixin,forms.ModelForm):
#Userモデルにprofileモデルを入れる
formset_class = ProfileFormSet
class Meta:
model = User
fields = ('username','email',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs['class'] = 'form-control'
#template
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form.username.label_tag }}
{{ form.username }}
{{ form.email.label_tag }}
{{ form.email }}
{{ form.formset }}
<button type="submit">Submit</button>
</form>
I want to specify and display instead of the method of displaying at once with formset.
Postscript
Mixins are created for data verification and data storage.
I want to display the details of formset display in template.
However, I only know how to display it at once, like {{form.formset}}.
Ideally I want to display it individually like {{form.formset.first_name}}
#mixins
class ModelFormWithFormSetMixin:
def __init__(self, *args, **kwargs):
super(ModelFormWithFormSetMixin, self).__init__(*args, **kwargs)
self.formset = self.formset_class(
instance=self.instance,
data=self.data if self.is_bound else None,
)
def is_valid(self):
return super(ModelFormWithFormSetMixin, self).is_valid() and self.formset.is_valid()
def save(self, commit=True):
saved_instance = super(ModelFormWithFormSetMixin, self).save(commit)
self.formset.save(commit)
return saved_instance
A formset is a set of forms so you have to loop through them {% for subform in form.formset%}{{subform.first_name}}{%endfor%}
Dirkgroten told me in the comments.
Thank you.
I am building a form with subcategory using inlineformset. I use inlineform because I want the subcategory to nest under the category. I want to validate a field (weight) of each subcategory form to add up to 100%. I use the clean() but can't get it to work and the error message doesn't show up on the template. I have done some digging form the past posts but don't see any solutions. I also looked into djangoproject but got nowhere. Please help! What am I missing here?!!
In my forms.py I have:
from django import forms
from django.forms.models import inlineformset_factory
from .models import Category, SubCategory
from django.forms import BaseInlineFormSet
class SubInline(BaseInlineFormSet):
def clean(self):
allocation = 0
for form in self.forms:
if not hasattr(form, 'cleaned_data'):
continue
weight = form.cleaned_data
allocation += int(weight.get('weight'))
if allocation != 100:
raise forms.ValidationError("Weights must sum to 100")
SubCategoryFormSet = inlineformset_factory(Category,
SubCategory,
fields = ['title',
'description',
'weight'],
formset = SubInline,
extra = 0,
can_delete =True)
In my views I have:
class CategorySubCategoryUpdateView(TemplateResponseMixin, View):
template_name = 'manage/subcategory/formset.html'
category = None
def get_formset(self, data = None):
return SubCategoryFormSet(instance = self.category, data=data)
def dispatch(self, request, pk):
self.category = get_object_or_404(Category,
id=pk,
owner=request.user)
return super(CategorySubCategoryUpdateView,
self).dispatch(request)
def get(self, request, *args, **kwargs):
formset = self.get_formset()
return self.render_to_response({'category': self.category,
'formset':formset})
def post(self, request, *args, **kwargs):
formset = self.get_formset(data=request.POST)
if formset.is_valid():
formset.save()
return redirect('categoryportfolios:manage_category_list')
else:
print("100 is needed")
return self.render_to_response({'category':self.category,
'formset': formset})
in my template:
<form class="form-horizontal" action = "" method = "post">
{{formset.management_form}}
{% for form in formset %}
{% for fields in form %}
<div class = "form-group">
{{ fields.errors}}
{{ fields.label_tag }}</br>{{ fields }}
{% csrf_token%}
</div>
{% endfor %}
{% endfor %}
<input type = "submit" class="button" value="save subcategories">
</form>
</div>
</div>
</div>
</div>
I'm trying to set up crispy form on a form and a formset (Visit and VisitService).
I'm having trouble attaching the helper to the formset, no matter how I do it.
I added helper as an attribute to the Formset and added the helper to view and context but it keeps giving me the following error:
VariableDoesNotExist - Failed lookup for key [helper]
(also tried [helper_attribute])
here is what I'm using:
forms.py:
class VisitForm(forms.ModelForm):
class Meta:
model = models.Visit
fields = [
[...all visit fields go here...]
]
def __init__(self, *args, **kwargs):
super(VisitForm, self).__init__(*args, **kwargs)
self.helper = FormHelper(self)
self.helper.layout = Layout(
[...fields go here...]
),
ButtonHolder(
Submit('submit', 'Submit', css_class='button white btn-wide')
)
)
class VisitServiceForm(forms.ModelForm):
class Meta:
model = models.VisitService
fields = [
'service',
'unit',
]
def __init__(self, *args, **kwargs):
super(VisitServiceForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
Row(
Div(Field('service'),css_class='col-sm-4'),
Div(Field('unit'),css_class='col-sm-4'),
)
VisitServiceFormSet = forms.modelformset_factory(
models.VisitService,
form=VisitServiceForm,
extra=2,
)
VisitServiceInlineFormSet = forms.inlineformset_factory(
models.Visit,
models.VisitService,
extra=5,
fields=('service', 'unit'),
formset=VisitServiceFormSet,
min_num=1,
)
views.py:
def create_visit(request, patient_pk):
patient = get_object_or_404(models.Patient, pk=patient_pk)
form_class = forms.VisitForm
form = form_class()
visitservice_forms = forms.VisitServiceInlineFormSet(
queryset=models.VisitService.objects.none()
)
helper = forms.VisitServiceForm()
if request.method == 'POST':
form = form_class(request.POST)
visitservice_forms = forms.VisitServiceInlineFormSet(
request.POST,
queryset=models.VisitService.objects.none()
)
if form.is_valid() and visitservice_forms.is_valid():
visit = form.save(commit=False)
visit.patient = patient
visit.save()
visitservices = visitservice_forms.save(commit=False)
for visitservice in visitservices:
visitservice.visit = visit
visitservice.save()
messages.success(request, "Added visit")
return HttpResponseRedirect(visit.get_absolute_url())
return render(request, 'fpform/visit_form.html', {
'patient': patient,
'form': form,
'formset': visitservice_forms,
'helper': helper,
})
template:
<div class="container">
<form method="POST" action="">
{% crispy form %}
{% crispy formset formset.form.helper_attribute %}
</div>
</form>
in my template I've also used each one of these separately with no luck:
{% crispy formset formset.form.helper %}
{% crispy formset helper_attribute %}
{% crispy formset helper %}
{% crispy formset form.helper_attribute %}
{% crispy formset form.helper %}
I looked at the crispy documentation but couldn't find the answer there.
A whole day spent on this but it hasn't gone anywhere. Also feel free to let me know if there is a better way to achieve this.
To define the layout of formsets you need to create an independent FormHelper class and pass it to the form.
http://django-crispy-forms.readthedocs.io/en/latest/crispy_tag_formsets.html
# forms.py
class VisitServiceFormHelper(FormHelper):
def __init__(self, *args, **kwargs):
super(VisitServiceFormHelper, self).__init__(*args, **kwargs)
self.layout = Layout(
Div(Field('service'),css_class='col-sm-4'),
Div(Field('unit'),css_class='col-sm-4'),
)
#views.py
formset = VisitServiceInlineFormSet(queryset=VisitService.objects.none())
helper = VisitServiceFormHelper()
return render(request, 'template.html', {'formset': formset, 'helper': helper})
#template
{% crispy formset helper %}
How to exclude form fields if the user is not staff ? I tried this but didn't work , giving an error :
global name 'user' is not defined
class PostForm(ModelForm):
class Meta:
model = Photo
exclude = ['author','featured','published']
def __init__(self, *args, **kwargs):
published = kwargs.pop('published', None)
super(PostForm, self).__init__(*args, **kwargs)
if not user.is_staff:
del self.fields['published']
view.py
def addpost(request):
if request.method == 'POST':
form = PostForm(request.POST,request.FILES,user=request.user)
if form.is_valid():
post = form.save(False)
post.author = request.user
form.save()
return HttpResponseRedirect(reverse('insight.content.views.index', ))
else:
form = PostForm(user=request.user)
ispost = True
return render_to_response('form_add_place.html', {'form': form,'ispost':ispost},context_instance=RequestContext(request))
This can be achieved in the template when rendering the form. It will need to allow null values or have a default value in the model definition or alternatively have its validation overridden:
<form method="post">{% csrf_token %}
{% if request.user.is_staff %}
<p>{{ form.published }}</p>
{% endif %}
<p>{{ form.author }}</p>
<!-- ... your other fields -->
</form>
Similarly you can check for is_superuser or check permissions, see the docs: https://docs.djangoproject.com/en/dev/topics/auth/default/#permissions
You need to pass it the user instance from your request - the model form doesn't have access to it.
my_form = PostForm(user=request.user)
Then, in your __init__:
def __init__(self, *args, **kwargs):
published = kwargs.pop('published', None)
user = kwargs.pop('user', None)
super(PostForm, self).__init__(*args, **kwargs)
if not user.is_staff:
del self.fields['published']