Accessing context variables in modelform - django

I have a modelformset factory like so:
Form:
class AwesomeModelForm(ModelForm):
class Meta:
model = AwesomeModel
fields = ('thing', 'field_2', 'field_3')
widgets = {
'thing': HiddenInput(),
}
View:
class AwesomeView(TemplateView):
formset = modelformset_factory(AwesomeModel, form=AwesomeModelForm,
can_delete=True)
def get_context_data(self, **kwargs):
self.id = self.kwargs['id']
self.thing = Thing.objects.get(id=self.id)
formset = self.allocate_formset(queryset=AwesomeModel.objects\
.filter(thing=self.thing))
context = super(AllocateLeave, self).get_context_data(**kwargs)
context['formset'] = formset
return context
I'd like to prepopulate the modelformset_factory using initial e.g.
formset = self.allocate_formset(queryset=AwesomeModel.objects.filter(thing=self.thing), initial=[{'thing': self.thing,}])
Django seems to think that the application of "initial" means the form has changed, so if I want to change a form value but not the 'extra' form, the validation fails because the 'extra' form is incomplete.
e.g.
for form in new_formset.forms:
print form.has_changed()
returns True for the 'extra' field with the inital set and False if initial is not set in the formset declaration.
While iterating through the forms in the formset works to save each existing form, if I want to delete a form, or apply any formset wide validation, the form fails validation.
I think the solution might be to use a clean method for the 'thing' field, but I can't see how to access the context variables of the view in the modelform class. Is this the right approach or should I be pursuing another option?
Thanks

Related

Django UpdateView creates duplicated formsets

I'm having problems when updating the entries of my products, discounts and inventory in the database, the inventories and discounts have the product id as a foreign key and I have defined two inline formsets to enter all the data in one single form
ProductMeta=forms.inlineformset_factory(Product,Inventory,InventoryForm,extra=0,can_delete=False)
DiscountMeta=forms.inlineformset_factory(Product,ProductDiscount,DiscountForm,extra=0,can_delete=False)
Something strange happens, both formsets work normally when creating using extra=1 in the formsets, but in the UpdateView I must use extra=0, otherwise the inventory and discount forms get duplicated, and both forms disappear from the creation view
views.py
class UpdateProduct(UpdateView):
model=Product
form_class =ProductForm
template_name = 'new_product.html'
title = "UPDATE PRODUCT"
def get_context_data(self, **kwargs):
context = super(UpdateProduct, self).get_context_data(**kwargs)
if self.request.method == 'POST':
context['product_meta_formset'] = ProductMeta(self.request.POST,instance=self.object)
context['discount_meta_formset'] = DiscountMeta(self.request.POST,instance=self.object)
else:
context['product_meta_formset'] = ProductMeta(instance=self.object)
context['discount_meta_formset'] = DiscountMeta(instance=self.object)
return context
def form_valid(self, form):
context=self.get_context_data()
productMeta=context['product_meta_formset']
discountMeta=context['discount_meta_formset']
if productMeta.is_valid() and discountMeta.is_valid():
self.object=form.save()
productMeta.instance=self.object
productMeta.save()
discountMeta.instance=self.object
discountMeta.save()
if not productMeta.is_valid():
print("productMeta invalid")
if not discountMeta.is_valid() :
print("discountMeta invalid")
return redirect(reverse("products:listProducts"))
For some reason Django considers that my productMeta and discountMeta are invalid since both prints are executed and obviously the changes are not saved in the database, for now I fix the inlineformset extra=0 problem using other inlineformsets, but I would like to know how to solve the invalid formset issue and correctly save the information.
NOTE: The formsets read the data from database and places them in the respective html input

Django forms dynamic generation and validation

Let's say I have the following model
class Foo(models.Model):
title = models.CharField(default='untitled')
# Bar is a MPTT class, so I'm building a tree here
# Should not matter for my question...
root = models.ForeignKey(Bar)
leaf = models.ForeignKey(Bar)
To create new Foo objects I want to make use of a ModelForm like this:
class FooForm(ModelForm):
# possibly custom validation functions needed here...
class Meta:
model = Foo
fields = '__all__'
My view looks like this:
def create(request, leaf_id=None):
form = FooForm(data=request.POST or None)
if form.is_valid():
new = form.save()
return redirect('show.html', root_id=new.root.id)
return render('create_foo.html',
{ 'form': form })
As you can see, the view function should be used to handle two different use-cases:
/foo/create/
/foo/create/4where 4 is a leaf-ID.
If the leaf-ID is given, the form obviously isn't required to show a form field for this. Furthermore, root can be determined from leaf, so it isn't required aswell.
I know that I can dynamically change the used widgets, so I can switch them to HiddenInput, but I would like to not even show them as hidden to the user. But if I dynamically exclude them, they are not available for form validation and the whole process will fail during the validation process.
What I would like to achieve is: Show only form fields to the user, that are not yet pre-filled. Is there any best-practice available for this case?
You can do that by overriding the __init__() method of FooForm.
We override the __init__() method and check if instance argument was passed to the form. If instance was passed, we disable the root and leaf form fields so that it is not displayed in the template.
We will pass instance argument to the form when the request is of type foo/create/4 i.e. leaf_id is not None.
forms.py
class FooForm(ModelForm):
def __init__(self, *args, **kwargs):
super(FooForm, self).__init__(*args, **kwargs) # call the 'super()' init method
instance = getattr(self, 'instance', None) # get the `instance` form attribute
if instance and instance.id: # check if form has 'instance' attribute set and 'instance' has an id
self.fields['root'].widget.attrs['disabled'] = 'disabled' # disable the 'root' form field
self.fields['leaf'].widget.attrs['disabled'] = 'disabled' # disable the 'leaf' form field
# custom validation functions here
....
class Meta:
model = Foo
fields = '__all__'
In our view, we first check if leaf_id argument was passed to this view. If leaf_id was passed,we retrieve the Foo object having leaf id as the leaf_id. This instance is then passed when initializing a form and is updated when form.save() is called. We will use the instance to populate the form with values as the attributes set on the instance.
If leaf_id is not passed, then we initialize FooForm with data argument.
views.py
def create(request, leaf_id=None):
# Get the instance if any
instance = None
if leaf_id:
instance = Foo.objects.get(leaf_id=leaf_id) # get the 'Foo' instance from leaf_id
# POST request handling
if request.method=='POST':
if instance:
form = FooForm(data=request.POST, instance=instance) # Populate the form with initial data and supply the 'instance' to be used in 'form.save()'
else:
form = FooForm(data=request.POST)
if form.is_valid():
new = form.save()
return redirect('show.html', root_id=new.root.id)
return render('create_foo.html',
{ 'form': form })
# GET request handling
if instance:
form = FooForm(initial=instance._data, instance=instance) # form will be populated with instance data
else:
form = FooForm() # blank form is initialized
return render('create_foo.html',
{ 'form': form })

Django: How to add a form to bounded formset in backend?

Basically, what I'd like to do is take a formset that has been bounded to request.POST, add a blank form to it, and return the new formset.
class MyView(View):
def post(self, request):
MyFormSet = formset_factory(MyForm)
posted_formset = MyFormSet(request.POST)
# Append a blank form to posted_formset here
return render(request, 'mytemplate.html', {'formset': posted_formset})
Is there any clean way to do this in Django 1.6? Right now I've got a workaround that involves creating a new formset with initial set to the data bound in posted_formset, but it's kind of clunky, and I feel like there should be an easier way to do this.
This might be a possible duplicate of this question: Django: How to add an extra form to a formset after it has been constructed?
Otherwise have a look at the extra parameter for the factory function. You get more information about formsets in the Django Documentation
So it seems like there's no clean way to do this, as after a formset is bound to a QueryDict, the extra parameter passed to formset_factory becomes irrelevant. Here's how I'm doing it in my view right now:
class MyView(View):
def post(self, request):
MyFormSet = formset_factory(MyForm)
posted_formset = MyFormSet(request.POST)
initial = []
for form in posted_formset:
data = {}
for field in form.fields:
data[field] = form[field].value()
initial.append(data)
formset = AuthorFormSet(initial=initial)
return render(request, 'mytemplate.html', {'formset': formset})
Basically, I bind a formset to request.POST, then iterate through it and grab the field values and put them into an array of dicts, initial, which I pass in when I make a new formset (see docs) that will have an extra blank form at the end because it's not bound.
This should work with any form and formset. Hope it helps someone.

Override Django formset delete behavior: Delete form instance if empty field in Django model formset

I run into this often:
I want to hide the default delete box in formsets and delete the instance of an object if a certain field is cleared in the each form of the formset.
The typical problem is that, either validation gets in the way or this breaks the blank form detection and starts adding all forms ( even blank extra ) when the formset is saved.
Here is the solution I found:
This code creates a model formset, ensures it validates by permitting a blank field, and then on save determines which objects to delete and which forms to save.
TaskFormset = inlineformset_factory(User, FAQ, extra=3, can_delete=False, exclude=('user', 'answer',))
formset = TaskFormset(request.POST, request.FILES, instance=user)
for form in formset:
form.fields['question'].required = False
// later when performing the formset save
for form in formset:
if form.instance.pk and form.cleaned_data['question'].strip() == '':
form.instance.delete()
elif form.cleaned_data:
form.save()
There's a method on BaseFormSet called _should_delete_form which makes it easy to automatically delete an instance based on your own criteria, however it's a "private" method so not sure about future support. You also need to save the formsets by calling formset.save().
In the example below it will delete a row if all the fields values evaluate to false values.
class MyFormSet(forms.BaseInlineFormSet):
def _should_delete_form(self, form):
"""Return whether or not the form should be deleted."""
if form.cleaned_data.get(forms.formsets.DELETION_FIELD_NAME):
return True # marked for delete
fields = ('name', 'question', 'amount', 'measure', 'comment')
if not any(form.cleaned_data[i] for i in fields):
return True
return False

Newbie Django Question : Can't find data I thought I preset in a Form

I'm still getting to grips with Django and, in particular, Forms.
I created MyForm which subclasses forms.Form in which I define a field like this :
owner = forms.CharField(widget=forms.HiddenInput)
When I create a new, blank instance of the Form I want to prefill this with the creator's profile, which I'm doing like this :
form = MyForm( {'owner' : request.user.get_profile()} )
Which I imagine sets the owner field of the form to the request.user's id. (The type of the corresponding "owner" field in the models.Model class is ForeignKey of Profile.)
Before rendering the form, I need to check one piece of information about the owner. So I try to access form.owner, but there seems to be no "owner" attribute of the form object. I also tried form.instance.owner, but similarly, no luck.
What am I doing wrong? What have I misunderstood?
You can access this value via the form's data dictionary:
form.data.get('owner')
Initial data in a form should be passed in with the initial kwarg.
Once you've turned the form into a bound form (usually by passing request.POST in as the first argument to instantiate the form, the place you are currently incorrectly providing the initial dictionary), and performed validation with form.is_valid(), the data the user submitted will be in form.cleaned_data, a dictionary. If they changed the initial value, their changed value will be in that dictionary. If not, your initial value will be.
If you don't want to let the user modify the value, then don't have it be a field, instead pass it in as a kwarg, and store it as an instance attribute in form.__init__():
class MyForm(Form):
def __init__(self, *args, profile, **kwargs):
super().__init__(*args, **kwargs)
self.profile = profile
...
form = MyForm(
request.POST if request.POST else None,
profile=request.user.get_profile(),
)
if request.method == "POST" and form.is_valid():
do_stuff_with(form.profile)
Also as with most things, this all gets easier if you drink the Django kool-aid and use generic views.
class MyView(FormView):
form_class = MyForm
...
def get_form_kwargs(self):
return {
**super().get_form_kwargs(),
"profile": self.request.user.get_profile()
}
def form_valid(self, form):
do_stuff_with(form.profile)
return super().form_valid(form)
Or for the initial case whereby you want it to be editable:
class MyView(FormView):
form_class = MyForm
...
def get_initial(self):
return {
**super().get_initial(),
"profile": self.request.user.get_profile()
}
def form_valid(self, form):
do_stuff_with(form.cleaned_data.get("profile"))
return super().form_valid(form)
If MyForm happens to be a form about one single instance of a specific model then this gets even easier with UpdateView instead of FormView. The more you buy into the way Django wants to do things, the less you have to work.