Django: Different form field widget per form in formset - django

I'm trying to find best approach for what i want. And i could use some help for that.
I have Model A and Model B. Model B has
modela = forms.ForeignKey(Model a)
I want to create a view where you can edit both single Model A and several Model B's on same page. Django has formsets for this and they work great.
I have one detail though, that messes things up tiny bit. Namely - i want the widgets or model B fields to be different based on what choices they have done in previous fields of same object. Because - based on type, the widget has to be datetime picker input or plain textinput.
Model B looks like this:
class ModelB(models.Model):
m0odela = models.ForeignKey(ModelA)
target_value = models.CharField()
target_type = models.CharField( choices = ( there are choices))
target_threshold = models.CharField()
I know i can provide my own form for formset and i could do this widget assignment in that form.
But the problem is, that when formset has no instances/queryset then i cant check if 'target_type' has been set for forms instance. So i would have to do it based on self.data or self.initial in form. But self.initial is also not present in form.__init__(). What i can work with is self.data - but that is raw request.POST or request.GET data - which contains all keys like 'mymodelb_set-0-target_type'.
So i'm bit lost here. Do i have to do some key parsing and figure out which -target_type belongs to current form and get chosen value there and assign widgets based on this value?
Or do i have to create my own subclass of BaseInlineFormSet and override _construc_form there somehow? So that form would have initial key with related data in **kwargs.
Has someone ran into this kind of problem before?
Alan

Well i had to solve it so i solved it as good/bad i could.
I created my own subclass of inline formset:
class MyInlineFormSet(BaseInlineFormSet):
def _construct_form(self, i, **kwargs):
initial = {}
fname = '%s-%s-%s' % (self.prefix, i, 'important_field_name')
initial['target_type'] = self.data[fname] if fname in self.data.keys() else 'km'
kwargs.update({'initial':initial})
form = super(MyInlineFormSet, self)._construct_form(i, **kwargs)
return form
And then in the form class:
class MyNiftyForm(forms.ModelForm):
class Meta:
model = MyAwesomeObject
fields=('field_one', 'field_two', 'field_three')
def __init__(self, *args, **kwargs):
super(ServiceTargetForm, self).__init__(*args, **kwargs)
if self.instance:
if self.instance.field_one == 'date':
self.fields['field_one'].widget.attrs['class'] = 'datepicker'
if self.initial:
if self.initial['field_one'] == 'date':
self.fields['field_one'].widget.attrs['class'] = 'datepicker'
and then in view:
MySuperCoolFormSet = inlineformset_factory(ImportantObject, MyAwesomeObject, extra = 1, form = MyNiftyForm, formset = MyInlineFormSet)
And it works.
Alan

Related

add extra field to ModelForm

I am adding an extra field to a Django ModelForm like that:
class form(forms.ModelForm):
extra_field = forms.CharField(label='Name of Institution')
class Meta:
model = db_institutionInstitution
fields = ['conn_kind','time','inst_name2']
The form is actually working fine, but I cant prepopulate it. I use it in a modelformset_factory:
formset = modelformset_factory(db_institutionInstitution,form=form)
I manually run through a queryset and add the entry in the dictionary needed for the additional form in the formset. However, when I call:
formset1 = formset(prefix='brch',queryset=qs1)
the extra_field is not prepopulated as intended (the rest is working fine).
Can anyone help?
If you want to set a default.
extra_field = forms.CharField(label='Name of Institution', initial="harvard")
If you want to dynamically set a value put it on form initialization:
def __init__(self, *args, **kwargs):
super(form, self).__init__(*args, **kwargs)
self.fields['extra_field'].initial = "harvard"

Django custom widget, populate from another table (similar to inlines)

I am writing a custom widget for multiple image uploads. My models are:
models.py
class Room(models.Model):
....
....
class Picture (models.Model):
room = models.ForeignKey(Room)
url=models.ImageField(upload_to='slider', height_field=None, width_field=None, max_length=100)
def __unicode__(self):
return str(self.url)
I want to create custom widget which allow multiple image upload to be shown on rooms form
This is what I tried so far:
forms.py
class MultyImageWidget(forms.Widget):
....
def render(self, name, value, attrs=None):
context = {
'images': Picture.objects.filter(room = *room_id_of_currently_edited_room*)
# OR
# Any another way to get set of images from pictures table
}
return mark_safe(render_to_string(self.template_name, context))
class RoomsForm(forms.ModelForm):
gallery = forms.ImageField(widget=MultyImageWidget, required=False)
class Meta:
model = Room
fields = '__all__'
So problem is, I don't have gallery field in room model but I want to use widget to manage pictures which is stored in picture table similar to how one can manage data through inlines.
How to get id of room which is currently edited from my widget?
Or is there any other way to get related pictures?
Thanks
I believe you are using a wrong approach to your problem. Widget should only be responsible of displaying data and should not care about where that data come from. your render method accepts a value parameter, which is used to pass data to widget, so your render should look similar to this:
class MultyImageWidget(forms.Widget):
....
def render(self, name, value, attrs=None):
context = {
'images': value
}
return mark_safe(render_to_string(self.template_name, context))
Now we need to pass the needed Picture queryset. We can do this via form - and that what forms are for. You provided no context and I cannot comment yet to ask for details, but I suppose you are using a view to construct this form. If it is true, let's say we have some view, then we can do it this way:
def gallery_view(request, *args, **kwargs):
....
room = # get your room (maybe you pass ID via request or via arg or kwarg)
pictures = Picture.objects.filter(room=room) # or you can do room.picture_set.all()
form = RoomsForm(initial={'gallery': pictures})
....
If you are using this form with Django admin, a form has instance attribute and you can code like this:
class RoomsForm(forms.ModelForm):
gallery = forms.ImageField(widget=MultyImageWidget, required=False)
class Meta:
model = Room
fields = '__all__'
def __init__(self, *args, **kwargs):
super(RoomsForm, self).__init__(*args, **kwargs) # you may omit parameters to super in python 3
if self.instance:
self.fields['gallery'].initial = Picture.objects.filter(room=self.instance)
I hope this solves your problem

Modify UpdateView Form Data Before Presenting To User

I'm using a CreateView and UpdateView for managing saving and updating. Before my data is saved I need combine 3 form fields into one field for storing it in my model. Basically, I'm taking a longitude, latitude, and range and converting it to a single value that is stored in my database. In my ModelForm I create the extra fields that I need and remove the one field that I don't:
class FilterForm(ModelForm):
lat = forms.FloatField()
lgt = forms.FloatField()
range = forms.FloatField()
class Meta:
model = AdFilter
fields = ['title', 'tags', 'start_date', 'end_date', 'start_time', 'end_time', 'week_days', 'ad']
To create new I implement form_valid() in order to combine the longitude, latitude, and range and store it in my model correctly:
class FilterCreate(CreateView):
form_class = FilterForm
template_name = 'filter_form.html'
#method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(FilterCreate, self).dispatch(*args, **kwargs)
def form_valid(self, form):
new_filter = form.save(commit=False)
new_filter.creator = self.request.user
utm_coordinates = utm.from_latlon(float(form.data['lat']), float(form.data['lgt']))
center = geos.Point(utm_coordinates[0], utm_coordinates[1])
broadcast_area_geometry = center.buffer(float(form.data['range']))
# Set the right SRID
utm_epsg = int('326' + str(utm_coordinates[2]))
broadcast_area_geometry.srid = utm_epsg
new_filter.filter_geometry = broadcast_area_geometry
new_filter.save()
return super(FilterCreate, self).form_valid(new_filter)
This all works fine. Now I'm trying to do the opposite of what form_valid() does in my UpdateView for the situation when someone gets the form. So I need to go from my single model value and create a longitude, latitude, and range values. I tried doing this inside get_context_data() but I've only been able to figure out how to add fields and not how to modify existing ones.
This seems like a very common problem but I can't seem to find an example of how to implement this. Maybe I'm looking in the wrong place.
You probably want to override the get_initial method to provide default values. The method should return a dictionary mapping field names to their default values.

How to save Many To Many field with Multiple Checkboxes in Django Form

I would like to know how in the following form color (many-to-many field) can be populated by values from CheckboxSelectMultiple widget.
#models.py
class Color(models.Model):
RED = 1
BLACK = 2
COLOR_CHOICES = (
(RED, _('Red')),
(BLACK, _('Black')),
)
name = models.CharField(_('Color'), max_length=512,
choices=COLOR_CHOICES, blank=True)
class Car(models.Model):
color = models.ManyToManyField(Color, blank=True, null=True)
def save(self):
self.slug = slugify(self.name)
super(Car, self).save()
#forms.py
class AddCar(forms.ModelForm):
color = forms.MultipleChoiceField(
choices=Color.COLOR_CHOICES,
widget=forms.CheckboxSelectMultiple(),
required=False
)
#view.py
def add(request):
if request.method == 'POST':
form = AddCar(request.POST)
...
if form.is_valid():
car = form.save(commit=False)
for c in request.POST.getlist('color'):
car.color.add(c)
car.save()
form.save_m2m()
return redirect('/')
#error
'Car' instance needs to have a primary key value before a many-to-many relationship can be used.
You are doing form.save(commit=False) in which does not actually creates record in DB and due to which it cannot store M2M fields. Do form.save_m2m() after you save form.
Or from your code, you can move car.color.add() after you have saved the car. And also you don't need to have form.save(commit=False).
Are you not getting the checkboxes to show, or is it the error you're trying to get rid of? If the latter, try removing the commit=False when saving the form.
Update:
The Color model is not specifying any fields. Give it one, e.g. color = IntegerField(choices=COLOR_CHOICES).
In AddCar form, giving choices=Color.COLOR_CHOICES if wrong - you must give it a tuple of objects that actually exists (Color.COLOR_CHOICES are just code constants). Also you probably should use ModelMultipleChoiceField, which takes a queryset parameter, e.g.:
colors = forms.ModelMultipleChoiceField(queryset=Color.objects, widget=forms.CheckboxSelectMultiple(), required=False)
https://docs.djangoproject.com/en/dev/ref/forms/fields/#modelmultiplechoicefield
This error is because, you are trying to save related objects to an object that isnt saved,
you are two options:
put commit=True
or before:
for c in request.POST.getlist('color'):
car.color.add(c)
put: car.save()
If you use commit=False, that objects is not beign saved.
But, you dont need save manually the "colors",
doing form.save_m2m() will do it for you, well, only if your form has
a manytomany field to choise.
EDIT:
Your color field within form, isnt well formed, must be a ModelMultipleChoiceField
color = forms.ModelMultipleChoiceField(queryset=Color.objects.all())
see docs:
https://docs.djangoproject.com/en/1.3/topics/forms/modelforms/#inline-formsets

field choices() as queryset?

I need to make a form, which have 1 select and 1 text input. Select must be taken from database.
model looks like this:
class Province(models.Model):
name = models.CharField(max_length=30)
slug = models.SlugField(max_length=30)
def __unicode__(self):
return self.name
It's rows to this are added only by admin, but all users can see it in forms.
I want to make a ModelForm from that. I made something like this:
class ProvinceForm(ModelForm):
class Meta:
CHOICES = Province.objects.all()
model = Province
fields = ('name',)
widgets = {
'name': Select(choices=CHOICES),
}
but it doesn't work. The select tag is not displayed in html. What did I wrong?
UPDATE:
This solution works as I wanto it to work:
class ProvinceForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ProvinceForm, self).__init__(*args, **kwargs)
user_provinces = UserProvince.objects.select_related().filter(user__exact=self.instance.id).values_list('province')
self.fields['name'].queryset = Province.objects.exclude(id__in=user_provinces).only('id', 'name')
name = forms.ModelChoiceField(queryset=None, empty_label=None)
class Meta:
model = Province
fields = ('name',)
Read Maersu's answer for the method that just "works".
If you want to customize, know that choices takes a list of tuples, ie (('val','display_val'), (...), ...)
Choices doc:
An iterable (e.g., a list or tuple) of
2-tuples to use as choices for this
field.
from django.forms.widgets import Select
class ProvinceForm(ModelForm):
class Meta:
CHOICES = Province.objects.all()
model = Province
fields = ('name',)
widgets = {
'name': Select(choices=( (x.id, x.name) for x in CHOICES )),
}
ModelForm covers all your needs (Also check the Conversion List)
Model:
class UserProvince(models.Model):
user = models.ForeignKey(User)
province = models.ForeignKey(Province)
Form:
class ProvinceForm(ModelForm):
class Meta:
model = UserProvince
fields = ('province',)
View:
if request.POST:
form = ProvinceForm(request.POST)
if form.is_valid():
obj = form.save(commit=True)
obj.user = request.user
obj.save()
else:
form = ProvinceForm()
If you need to use a query for your choices then you'll need to overwrite the __init__ method of your form.
Your first guess would probably be to save it as a variable before your list of fields but you shouldn't do that since you want your queries to be updated every time the form is accessed. You see, once you run the server the choices are generated and won't change until your next server restart. This means your query will be executed only once and forever hold your peace.
# Don't do this
class MyForm(forms.Form):
# Making the query
MYQUERY = User.objects.values_list('id', 'last_name')
myfield = forms.ChoiceField(choices=(*MYQUERY,))
class Meta:
fields = ('myfield',)
The solution here is to make use of the __init__ method which is called on every form load. This way the result of your query will always be updated.
# Do this instead
class MyForm(forms.Form):
class Meta:
fields = ('myfield',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make the query here
MYQUERY = User.objects.values_list('id', 'last_name')
self.fields['myfield'] = forms.ChoiceField(choices=(*MYQUERY,))
Querying your database can be heavy if you have a lot of users so in the future I suggest some caching might be useful.
the two solutions given by maersu and Yuji 'Tomita' Tomita perfectly works, but there are cases when one cannot use ModelForm (django3 link), ie the form needs sources from several models / is a subclass of a ModelForm class and one want to add an extra field with choices from another model, etc.
ChoiceField is to my point of view a more generic way to answer the need.
The example below provides two choice fields from two models and a blank choice for each :
class MixedForm(forms.Form):
speaker = forms.ChoiceField(choices=([['','-'*10]]+[[x.id, x.__str__()] for x in Speakers.objects.all()]))
event = forms.ChoiceField(choices=( [['','-'*10]]+[[x.id, x.__str__()] for x in Events.objects.all()]))
If one does not need a blank field, or one does not need to use a function for the choice label but the model fields or a property it can be a bit more elegant, as eugene suggested :
class MixedForm(forms.Form):
speaker = forms.ChoiceField(choices=((x.id, x.__str__()) for x in Speakers.objects.all()))
event = forms.ChoiceField(choices=(Events.objects.values_list('id', 'name')))
using values_list() and a blank field :
event = forms.ChoiceField(choices=([['','-------------']] + list(Events.objects.values_list('id', 'name'))))
as a subclass of a ModelForm, using the one of the robos85 question :
class MixedForm(ProvinceForm):
speaker = ...