Provide different kwargs for each form in formset - python

I am trying to use a formset to create forms for a set of timeframes related to dates:
class Event(models.Model):
date = models.DateField()
class TimeFrame(models.Model):
start = models.DateTimeField()
end = models.DateTimeField()
event = models.ForeignKey('Event')
I have code that gets me a queryset of timeframes for each event and added a kwarg to pass this into my form:
class SelectDatesForm(forms.Form):
timeframes = forms.ModelChoiceField(queryset=HostTimeFrame.objects.none())
def __init__(self, *args, **kwargs):
qs = kwargs.pop('timeframes')
super(SelectDatesForm, self).__init__(*args, **kwargs)
self.fields['timeframes'].queryset = qs
Now I'm trying to construct a formset that lets me show timeframes for multiple events on one page. I already found this question, explaining how to pass initial data, for serveral forms, but its not the same as passing it to a queryset.
Also there is this new function from django 1.9 but it doesnt allow me to get different querysets for each form.
UPDATE:
I got the solution from the answer working, however, whenever im running formset.is_valid() i get the error:
Select a valid choice. That choice is not one of the available
choices.
Here is what I do in my view:
timeframes = [HostTimeFrame.objects.all()]
SelectDatesFormset = formset_factory(form=SelectDatesForm, extra=len(timeframes), formset=BaseSelectDatesFormSet)
if request.method == 'POST':
formset = SelectDatesFormset(request.POST, form_kwargs={'timeframes_list': timeframes})
if formset.is_valid():
# do something with the formset.cleaned_data
print(formset)
pass
else:
formset = SelectDatesFormset(form_kwargs={'timeframes_list': timeframes})
Ive been trying for hours to find where this actual validation is done, but i cant find it for the live of me.
Edit: I tested this with the singular form, and i have the same issue, I created a new question for this here.

UPDATE:
Only partial solution, see question.
Solved it myself:
First I created a BaseFormSet:
class BaseSelectDatesFormSet(BaseFormSet):
def get_form_kwargs(self, index):
kwargs = super(BaseSelectDatesFormSet, self).get_form_kwargs(index)
kwargs['timeframes'] = kwargs['timeframes_list'][index]
return kwargs
Then I could pass the list of timeframes in the view:
SelectDatesFormset = formset_factory(form=SelectDatesForm, extra=4, formset=BaseSelectDatesFormSet)
formset = SelectDatesFormset(form_kwargs={'timeframes_list': timeframes})
Finally I had to update my form init to pop the list as well so the super constructor doesnt complain about unwanted kwargs:
def __init__(self, *args, **kwargs):
qs = kwargs.pop('timeframes')
qs_list = kwargs.pop('timeframes_list')
super(SelectDatesForm, self).__init__(*args, **kwargs)
self.fields['timeframes'].queryset = qs.order_by('start')

For peeps using Class Based View FormView along with form_class as formset or formset_factory, they can add an extra attribute as follows:
Pass form_kwargs in the get_form method by overriding it.
timeframes = [HostTimeFrame.objects.all()]
class SelectDatesView(FormView):
form_class = formset_factory(form=SelectDatesForm, extra=len(timeframes)
def get_form(self, form_class=None):
"""Override the method to add form kwargs. Returns an instance of the form to be used in this view."""
if form_class is None:
form_class = self.get_form_class()
return form_class(**self.get_form_kwargs(), form_kwargs={"timeframes": timeframes})
One can access it directly in the __init__ method's kwargs.
def __init__(self, *args, **kwargs):
super(SelectDatesForm, self).__init__(*args, **kwargs)
qs = kwargs.get('timeframes')
self.fields['timeframes'].queryset = qs.order_by('start')

Related

Render dropdown in django view using values from postgres arrayfield

Here is an example from the model:
class Shipment(models.Model):
shipment_id = models.BigAutoField(null=False, primary_key=True)
potential_shipping_dates = ArrayField(models.DateField(), verbose_name='Ship Dates', null=True)
Here is what I'm sort of attempting in my form:
class ShippingForm(forms.Form):
potential_shipping_dates = forms.ModelChoiceField(queryset=Shipment.objects.all())
def __init__(self, *args, **kwargs):
super(ShippingForm, self).__init__(*args, **kwargs)
And here is where my form is added to context:
context['shippingForm'] = ShippingForm(initial=??what_goes_here_maybe??)
My form renders fine but I want to show a dropdown with a date for each option.
Okay this is a bit complex, but I think I understand what you're trying to do, and where you're going wrong.
So you have a Shipment model, and each Shipment instance has a field with a few different potential_shipping_dates.
Say you have 2 shipments:
IN : ship1 = Shipment.objects.first()
OUT:
IN : ship1.potential_shipping_dates
OUT: ['01/01/2021', '02/02/2021']
IN : ship2 = Shipment.objects.last()
OUT:
IN : ship2.potential_shipping_dates
OUT: ['03/03/2021', '04/04/2021']
Now, do you want the dropdown to have all 4 dates as possibilities, and that will select the Shipment?
Or do you want to select a date after selecting the shipment in the form?
^^ Answered in comments
Okay so you will need to pass the instance through to the form:
views.py
# Inherit from Django's UpdateView to have `instance` passed through to the form
class ShippingFormView(UpdateView):
model = Shipment
form_class = ShippingForm
# Or if you don't want to inherit from inherit from UpdateView
class ShippingFormView(Blah):
model = Shipment
form_class = ShippingForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['instance'] = self.get_object()
return kwargs
# Or if you're using function based views
def shipping_form_view(request, pk):
shipment = get_object_or_404(Shipment, pk=pk)
form = ShippingForm(request, instance=shipment)
...
forms.py
class ShippingForm(forms.Form):
potential_shipping_dates = forms.ChoiceField(choices=[])
def __init__(self, *args, instance, **kwargs):
super(ShippingForm, self).__init__(*args, **kwargs)
self.fields['potential_shipping_dates'].choices = ((dt, dt) for dt in instance.potential_shipping_dates)
ModelChoiceFields are used when selecting an object, not an attribute on one.

Django Formset: Is there any chance to add missing field content in the post() method of a CreateView?

I've created a CreateView with a modelformset, added some js to add or remove additional forms. It is a view to create a booking from a bank booking entry. And as the 'from_account' is obvious (the booking is launched from a certain bank booking entry and handing over the pk from the bank booking in the url), I do not want to show this (selection-) field in the form. So I put the field as a hidden field in the formset with the objective to fill it in the post.
All this works as expected until submit.
Now, in the post() method, I see in formset.error correctly the errors for the missing fields 'bank_account_ta' and 'from_account'. Since I don't know the total_forms number (it might have been changed using js), I cannot prefill it while instantiating the formset class.
Working around in the template with js is possible, but not the proper way...
I would rather using the formset.clean() method but whatever I try, the formset.error remains as before which leads to not formset.is_valid().
My questions: What is the easiest way to complete data (cleaned_data) of a formset in post()? Another point which is still hurting me: Is it necessary or helpful to define self.object in a formset and if yes, how? I put it to None as I receive a missing object error if I leave it undefined.
Thank you in advance.
views.py
class TransactionMulitCreateView(LoginRequiredMixin, CreateView):
model = Transaction
template_name = 'accounting/transaction_formset.html'
extra_context = {'title': model._meta.verbose_name, }
success_url = reverse_lazy('finance:ta_groups')
TaFormset = modelformset_factory(Transaction,
formset=TransactionBaseFormset,
exclude=('validity_period_start', 'validity_period_end', 'ta_group'),
extra=1)
def get(self, request, *args, **kwargs):
self.object = None
ba = BankTransaction.objects.get(pk=self.kwargs['bank_ta_pk'])
form = self.TaFormset(queryset=Transaction.objects.none(), initial=[
{
# commented out, as it was the js approach.
# 'bank_account_ta': ba,
# 'from_account': ba.account.ca_account,
'posting_text': ba.booking_string(),
'valuta': ba.valuta,
'entry_date': ba.entry_date,
'value': ba.amount,
},
], ba=ba)
helper = TransactionMultiFormsetHelper()
return self.render_to_response(self.get_context_data(form=form, helper=helper, bank_ta=ba))
def post(self, request, *args, **kwargs):
self.object = None
ba = BankTransaction.objects.get(pk=self.kwargs['bank_ta_pk'])
# pass in initial to be able to detect changes, see https://docs.djangoproject.com/en/3.1/topics/forms/formsets/
formset = self.TaFormset(request.POST, request.FILES, initial=[
{
# commented out, as it was the js approach.
# 'bank_account_ta': ba,
# 'from_account': ba.account.ca_account,
'posting_text': ba.booking_string(),
'valuta': ba.valuta,
'entry_date': ba.entry_date,
'value': ba.amount,
},
], ba=ba)
formset.clean()
# formset = self.TaFormset()
if formset.is_valid():
instances = formset.save(commit=False)
split = TransactionGroup.objects.create(create_by=self.request.user, update_by=self.request.user)
for instance in instances:
instance.ta_group = split
instance.save()
# BankTransaction.objects.filter(pk=self.kwargs['bank_ta_pk']).update(is_booked=True)
ba.is_booked = True
ba.save()
return HttpResponseRedirect(reverse_lazy('finance:bank_tas'))
# return self.form_valid(formset)
else:
helper = TransactionMultiFormsetHelper()
return self.render_to_response(self.get_context_data(form=formset, helper=helper, bank_ta=ba))
# return self.form_invalid(formset)
forms.py
class TransactionBaseFormset(forms.BaseModelFormSet):
def __init__(self, *args, **kwargs):
# self.ba_pk = kwargs.pop('ba_pk')
try:
self.ba = kwargs.pop('ba')
except KeyError:
self.ba = None
super().__init__(*args, **kwargs)
def clean(self):
data = None
for form in self.forms:
data = form.cleaned_data
data['bank_account_ta'] = self.ba
data['from_account'] = self.ba.account.ca_account
return data
Got it!
The simple solution is to exclude all enforced fields and fill them either in the clean() or in the is_valid() function.
TaFormset = modelformset_factory(Transaction,
formset=TransactionBaseFormset,
exclude=(
'validity_period_start',
'validity_period_end',
'ta_group',
'bank_account_ta',
'from_account',
),
extra=1)
However, I would still like to know which role plays (or might play) self.object in a formset (or modelformset)?

Validation error for one to one relationship in django modelfrom

I have 2 models which are related to each other using One to One Relation. How can i display the validation error if object of one model is already related to another.
Suppose i have 2 models
Class Place(models.Model):
field1 = CharField()
field2 = CharField()
Class Restaurant(models.Model):
related_field = OneToOneField(Place)
field3 = CharField()
In my case For one place one Restaurant. If user submits new Restaurant for the same place, then a validation should raise
If Restaurant object already attached to Place object, so for second Restaurant for same Place object i want to display validation
In admin we usually see this message "Restaurent with this Place already exists." But how can i achieve the same this using modelform with generic views
class RestaurantCreateView(CreateView):
form_class = RestaurantForm
template_name = 'restaurant_create.html'
def get_success_url(self):
return reverse_lazy('restuarants', args=[self.object.id])
def get_place(self, **kwargs):
id = self.kwargs['pk']
place = Place.objects.get(id=id)
return place
def get_context_data(self, **kwargs):
context = super(RestaurantCreateView, self).get_context_data(**kwargs)
place = self.get_place()
context['place'] = place
return context
def form_valid(self, form):
self.object = form.save(commit=False)
place = self.get_place()
self.object.place = place
self.object.save()
return HttpResponseRedirect(self.get_success_url())
How can perform One To One relation validation in class based generic views?
Then you need to pass the Place object into your Restaurant form. The form validation has no access to these things defined in the view unless you explicitly pass it that data.
class MyForm(forms.ModelForm):
class Meta:
model = Restaurant
def __init__(self, *args, **kwargs):
self.place = kwargs.pop('place', None)
super(MyForm, self).__init__(*args, **kwargs
def clean_related_field(self):
place = self.place
try:
place.restaurant
except Restaurant.DoesNotExist:
return place
raise forms.ValidationError("Restaurant already exists for that book")
class MyView(...):
form_class = MyForm
def get_form_kwargs(self):
kwargs = super(MyView, self).get_form_kwargs()
kwargs['place'] = Place.objects.get(id=self.kwargs['pk'])
return kwargs
{% if form.related_field.errors %}.........{% endif %}
Another approach is to fake validation in form_valid() and set the errors dict, and return the response. This method may be preferable since it's much more contained. Also especially if you are not using the form field anyways, you might as well add an error flag to the context.
class MyView():
def form_valid(self, form):
if Restaurant.objects.filter(related_field=self.get_place()):
ctx = self.get_context_data(form=form)
ctx['massive_error'] = True
return self.render_to_response(ctx)

How do I initiate values for fields in a form for editing in a template

I understand that you can use the initiate parameter for a Form class from this question.
I am creating an edit form and I'm trying to figure out how to initiate values from a pre-existing object.
Do I do it in the template level or in the view level (I don't even know how to do it in the template level)? Or maybe I need to pass the actual object to the form and initiate in the form level?
What is the best practice?
EDIT:
For #Bento: In my original Form, I'm doing something like this
class OrderDetailForm(forms.Form):
work_type = forms.ChoiceField(choices=Order.WORK_TYPE_CHOICES)
note = forms.CharField(widget=forms.Textarea)
def __init__(self, creator_list=None, place_list=None, *args, **kwargs):
super(OrderCreateForm, self).__init__(*args, **kwargs)
if creator_list:
self.fields['creator'] = UserModelChoiceField(
queryset=creator_list,
empty_label="Select a user",
)
def clean(self):
super(OrderCreateForm, self).clean()
if 'note' in self.cleaned_data:
if len(self.cleaned_data['note']) < 50:
self._errors['note'] = self.error_class([u"Please enter a longer note."])
del self.cleaned_data['note']
return self.cleaned_data
How would I do that with ModelForm?
Assuming you are using a ModelForm, it's actually fairly simple. The task is something like this: retrieve the object of the model that you want to populate your 'edit' for with, create a new form based on your ModelForm, and populate it with the object using 'instance'.
Here's the skeleton of your view:
def view(request):
obj = Model.objects.get(pk = objectpk)
form = MyModelForm(instance = obj)
return render (request, "template", {'form' = form})
You can access the 'initial' values by using something like:
form.fields['fieldname'].initial = somevalue
And then you'd return the form like above.

Accessing parent model instance from modelform of admin inline

I'm using a TabularInline in Django's admin, configured to show one extra blank form.
class MyChildInline(admin.TabularInline):
model = MyChildModel
form = MyChildInlineForm
extra = 1
The model looks like MyParentModel->MyChildModel->MyInlineForm.
I'm using a custom form so I can dynamically lookup values and populate choices in a field. e.g.
class MyChildInlineForm(ModelForm):
my_choice_field = forms.ChoiceField()
def __init__(self, *args, **kwargs):
super(MyChildInlineForm, self).__init__(*args, **kwargs)
# Lookup ID of parent model.
parent_id = None
if "parent_id" in kwargs:
parent_id = kwargs.pop("parent_id")
elif self.instance.parent_id:
parent_id = self.instance.parent_id
elif self.is_bound:
parent_id = self.data['%s-parent'% self.prefix]
if parent_id:
parent = MyParentModel.objects.get(id=parent_id)
if rev:
qs = parent.get_choices()
self.fields['my_choice_field'].choices = [(r.name,r.value) for r in qs]
This works fine for the inline records bound to an actual record, but for the extra blank form, it doesn't display any values in my choice field, since it doesn't have any record id and there can't lookup the associated MyParentModel record.
I've inspected all the values I could find (args, kwargs, self.data, self.instance, etc) but I can't find any way to access the parent object the tabular inline is bound to. Is there any way to do this?
Update: As of Django 1.9, there is a def get_form_kwargs(self, index) method in the BaseFormSet class. Hence, overriding that passes the data to the form.
This would be the Python 3 / Django 1.9+ version:
class MyFormSet(BaseInlineFormSet):
def get_form_kwargs(self, index):
kwargs = super().get_form_kwargs(index)
kwargs['parent_object'] = self.instance
return kwargs
class MyForm(forms.ModelForm):
def __init__(self, *args, parent_object, **kwargs):
self.parent_object = parent_object
super(MyForm, self).__init__(*args, **kwargs)
class MyChildInline(admin.TabularInline):
formset = MyFormSet
form = MyForm
For Django 1.8 and below:
To pass a value of a formset to the individual forms, you'd have to see how they are constructed. An editor/IDE with "jump to definition" really helps here to dive into the ModelAdmin code, and learn about the inlineformset_factory and it's BaseInlineFormSet class.
From there you'll find that the form is constructed in _construct_form() and you can override that to pass extra parameters. It will likely look something like this:
class MyFormSet(BaseInlineFormSet):
def _construct_form(self, i, **kwargs):
kwargs['parent_object'] = self.instance
return super(MyFormSet, self)._construct_form(i, **kwargs)
#property
def empty_form(self):
form = self.form(
auto_id=self.auto_id,
prefix=self.add_prefix('__prefix__'),
empty_permitted=True,
parent_object=self.instance,
)
self.add_fields(form, None)
return form
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.parent_object = kwargs.pop('parent_object', None)
super(MyForm, self).__init__(*args, **kwargs)
class MyChildInline(admin.TabularInline):
formset = MyFormSet
form = MyForm
Yes, this involves a private _construct_form function.
update Note: This doesn't cover the empty_form, hence your form code needs to accept the parameters optionally.
I'm using Django 1.10 and it works for me:
Create a FormSet and put the parent object into kwargs:
class MyFormSet(BaseInlineFormSet):
def get_form_kwargs(self, index):
kwargs = super(MyFormSet, self).get_form_kwargs(index)
kwargs.update({'parent': self.instance})
return kwargs
Create a Form and pop an atribute before super called
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
parent = kwargs.pop('parent')
super(MyForm, self).__init__(*args, **kwargs)
# do whatever you need to with parent
Put that in the inline admin:
class MyModelInline(admin.StackedInline):
model = MyModel
fields = ('my_fields', )
form = MyFrom
formset = MyFormSet
AdminModel has some methods like get_formsets. It receives an object and returns a bunch of formsets. I think you can add some info about parent object to that formset classes and use it later in formset's __init__
Expanding on ilvar's answer a bit, If the form field of interest is constructed from a model field, you can use the following construction to apply custom behavior to it:
class MyChildInline(admin.TabularInline):
model = MyChildModel
extra = 1
def get_formset(self, request, parent=None, **kwargs):
def formfield_callback(db_field):
"""
Constructor of the formfield given the model field.
"""
formfield = self.formfield_for_dbfield(db_field, request=request)
if db_field.name == 'my_choice_field' and parent is not None:
formfield.choices = parent.get_choices()
return formfield
return super(MyChildInline, self).get_formset(
request, obj=obj, formfield_callback=formfield_callback, **kwargs)
return result

Categories